Compare commits

...
Author SHA1 Message Date
Jim Miller a82a40a221 Added tag calibre-plugin-1.6.02 for changeset 8a5bb7e23d65 2012-08-04 11:31:20 -05:00
Jim Miller cbc2605804 Fix for ffnet genres with extra space. 2012-08-04 11:31:09 -05:00
Jim Miller 6c6a84f533 Added tag calibre-plugin-1.6.01 for changeset 7af88b209be3 2012-08-01 21:55:01 -05:00
Jim Miller 1be2e50beb Add 'newonly' feature for standard and custom columns in plugin. 2012-08-01 21:54:49 -05:00
Jim Miller 5c37bddca9 'Fix' for yourfanfiction.com on web service issue. 2012-07-31 23:36:28 -05:00
Jim Miller 42473d4f1d Remove extra html body from ancient ffnet chapters. 2012-07-30 17:00:13 -05:00
Jim Miller 850567afde Added tag calibre-plugin-1.6.00 for changeset 0b238d04d0fe 2012-07-27 10:56:04 -05:00
Jim Miller 30aa321c74 Change FFDL to store settings in library db, not json file. 2012-07-27 10:55:43 -05:00
Jim Miller e7972b3d8e Moved tag FanFictionDownLoader-4.4.21 to changeset bc843c796c94 (from changeset 8ba4b306136c) 2012-07-22 17:35:33 -05:00
Jim Miller f538753b69 Added tag calibre-plugin-1.5.46 for changeset bc843c796c94 2012-07-22 17:35:15 -05:00
Jim Miller 3829c05a5b Really intergrate new adapters (forgot to save adapters/__init__.py) 2012-07-22 17:35:00 -05:00
Jim Miller 6b6fdf078e Added tag FanFictionDownLoader-4.4.21 for changeset 8ba4b306136c 2012-07-22 11:08:10 -05:00
Jim Miller d52da6bcd6 Added tag calibre-plugin-1.5.45 for changeset 8ba4b306136c 2012-07-22 11:07:56 -05:00
Jim Miller 7eb890e231 Integrate new adapters, bump versions. 2012-07-22 11:02:38 -05:00
Jim Miller eeb4204797 Merge changes 2012-07-22 10:23:15 -05:00
Jim Miller 4f63c646b7 Make plugin update calibre library safely, before risked corruption. 2012-07-22 10:22:33 -05:00
Ida a91aac8d2d Forgot to correct examples for grangerenchanted.com 2012-07-21 15:18:23 -04:00
Ida 7f50b87842 New adapters for nha.magical-worlds.us, grangerenchanted.com and hlfiction.net 2012-07-21 15:15:11 -04:00
Jim Miller 333c369b0d thewriterscoffeeshopcom: stripHTML on title 2012-07-17 13:42:27 -05:00
Ida dd5817540b Yet another fix for fnst 2012-07-16 21:05:40 -04:00
Ida 609ebe71b8 Fix for stories with empty category in finestories.com 2012-07-15 17:45:53 -04:00
Jim Miller b34b8fa9db Added tag calibre-plugin-1.5.44 for changeset 590b72ffb9d2 2012-07-15 10:43:29 -05:00
Jim Miller 9e74ccb109 Added tag FanFictionDownLoader-4.4.20 for changeset 590b72ffb9d2 2012-07-15 10:43:12 -05:00
18 changed files with 1256 additions and 197 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# ffd-retief-hrd fanfictiondownloader
application: fanfictiondownloader
version: 4-4-20
version: 4-4-21
runtime: python27
api_version: 1
threadsafe: true
+2 -2
View File
@@ -27,8 +27,8 @@ class FanFictionDownLoaderBase(InterfaceActionBase):
description = 'UI plugin to download FanFiction stories from various sites.'
supported_platforms = ['windows', 'osx', 'linux']
author = 'Jim Miller'
version = (1, 5, 44)
minimum_calibre_version = (0, 8, 30)
version = (1, 6, 2)
minimum_calibre_version = (0, 8, 57)
#: This field defines the GUI plugin class that contains all the code
#: that actually does something. Its format is module_path:class_name
+98 -2
View File
@@ -11,9 +11,11 @@ import os
from PyQt4 import QtGui
from PyQt4.Qt import (Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout,
QTableWidgetItem, QFont, QLineEdit, QComboBox,
QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime)
QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime,
QTextEdit,
QListWidget, QAbstractItemView)
from calibre.constants import iswindows
from calibre.gui2 import gprefs, error_dialog, UNDEFINED_QDATETIME
from calibre.gui2 import gprefs, error_dialog, UNDEFINED_QDATETIME, info_dialog
from calibre.gui2.actions import menu_action_unique_name
from calibre.gui2.keyboard import ShortcutConfig
from calibre.utils.config import config_dir
@@ -445,3 +447,97 @@ class DateDelegate(QStyledItemDelegate):
model.setData(index, UNDEFINED_QDATETIME, Qt.EditRole)
else:
model.setData(index, QDateTime(val), Qt.EditRole)
class PrefsViewerDialog(SizePersistedDialog):
def __init__(self, gui, namespace):
SizePersistedDialog.__init__(self, gui, 'Prefs Viewer dialog')
self.setWindowTitle('Preferences for: '+namespace)
self.gui = gui
self.db = gui.current_db
self.namespace = namespace
self._init_controls()
self.resize_dialog()
self._populate_settings()
if self.keys_list.count():
self.keys_list.setCurrentRow(0)
def _init_controls(self):
layout = QVBoxLayout(self)
self.setLayout(layout)
ml = QHBoxLayout()
layout.addLayout(ml, 1)
self.keys_list = QListWidget(self)
self.keys_list.setSelectionMode(QAbstractItemView.SingleSelection)
self.keys_list.setFixedWidth(150)
self.keys_list.setAlternatingRowColors(True)
ml.addWidget(self.keys_list)
self.value_text = QTextEdit(self)
self.value_text.setTabStopWidth(24)
self.value_text.setReadOnly(True)
ml.addWidget(self.value_text, 1)
button_box = QDialogButtonBox(QDialogButtonBox.Ok)
button_box.accepted.connect(self.accept)
self.clear_button = button_box.addButton('Clear', QDialogButtonBox.ResetRole)
self.clear_button.setIcon(get_icon('trash.png'))
self.clear_button.setToolTip('Clear all settings for this plugin')
self.clear_button.clicked.connect(self._clear_settings)
layout.addWidget(button_box)
def _populate_settings(self):
self.keys_list.clear()
ns_prefix = self._get_ns_prefix()
keys = sorted([k[len(ns_prefix):] for k in self.db.prefs.iterkeys()
if k.startswith(ns_prefix)])
for key in keys:
self.keys_list.addItem(key)
self.keys_list.setMinimumWidth(self.keys_list.sizeHintForColumn(0))
self.keys_list.currentRowChanged[int].connect(self._current_row_changed)
def _current_row_changed(self, new_row):
if new_row < 0:
self.value_text.clear()
return
key = unicode(self.keys_list.currentItem().text())
val = self.db.prefs.get_namespaced(self.namespace, key, '')
self.value_text.setPlainText(self.db.prefs.to_raw(val))
def _get_ns_prefix(self):
return 'namespaced:%s:'% self.namespace
def _clear_settings(self):
from calibre.gui2.dialogs.confirm_delete import confirm
message = '<p>Are you sure you want to clear your settings in this library for this plugin?</p>' \
'<p>Any settings in other libraries or stored in a JSON file in your calibre plugins ' \
'folder will not be touched.</p>' \
'<p>You must restart calibre afterwards.</p>'
if not confirm(message, self.namespace+'_clear_settings', self):
return
ns_prefix = self._get_ns_prefix()
keys = [k for k in self.db.prefs.iterkeys() if k.startswith(ns_prefix)]
for k in keys:
del self.db.prefs[k]
self._populate_settings()
d = info_dialog(self, 'Settings deleted',
'<p>All settings for this plugin in this library have been cleared.</p>'
'<p>Please restart calibre now.</p>',
show_copy_button=False)
b = d.bb.addButton(_('Restart calibre now'), d.bb.AcceptRole)
b.setIcon(QIcon(I('lt.png')))
d.do_restart = False
def rf():
d.do_restart = True
b.clicked.connect(rf)
d.set_details('')
d.exec_()
b.clicked.disconnect()
self.close()
if d.do_restart:
self.gui.quit(restart=True)
+191 -99
View File
@@ -4,10 +4,11 @@ from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2011, Jim Miller'
__copyright__ = '2012, Jim Miller'
__docformat__ = 'restructuredtext en'
import traceback, copy
from collections import OrderedDict
from PyQt4.Qt import (QDialog, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QFont, QWidget,
QTextEdit, QComboBox, QCheckBox, QPushButton, QTabWidget, QVariant, QScrollArea)
@@ -22,118 +23,121 @@ from calibre_plugins.fanfictiondownloader_plugin.dialogs \
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.adapters import getConfigSections
from calibre_plugins.fanfictiondownloader_plugin.common_utils \
import ( get_library_uuid, KeyboardConfigDialog )
import ( get_library_uuid, KeyboardConfigDialog, PrefsViewerDialog )
from calibre.gui2.complete import MultiCompleteLineEdit
# This is where all preferences for this plugin will be stored
PREFS_NAMESPACE = 'FanFictionDownLoaderPlugin'
PREFS_KEY_SETTINGS = 'settings'
# Set defaults used by all. Library specific settings continue to
# take from here.
default_prefs = {}
default_prefs['personal.ini'] = get_resources('plugin-example.ini')
default_prefs['updatemeta'] = True
default_prefs['updatecover'] = False
default_prefs['updateepubcover'] = False
default_prefs['keeptags'] = False
default_prefs['urlsfromclip'] = True
default_prefs['updatedefault'] = True
default_prefs['fileform'] = 'epub'
default_prefs['collision'] = OVERWRITE
default_prefs['deleteotherforms'] = False
default_prefs['adddialogstaysontop'] = False
default_prefs['includeimages'] = False
default_prefs['lookforurlinhtml'] = False
default_prefs['injectseries'] = False
default_prefs['send_lists'] = ''
default_prefs['read_lists'] = ''
default_prefs['addtolists'] = False
default_prefs['addtoreadlists'] = False
default_prefs['addtolistsonread'] = False
default_prefs['gcnewonly'] = False
default_prefs['gc_site_settings'] = {}
default_prefs['allow_gc_from_ini'] = True
default_prefs['countpagesstats'] = []
default_prefs['errorcol'] = ''
default_prefs['custom_cols'] = {}
default_prefs['custom_cols_newonly'] = {}
default_prefs['std_cols_newonly'] = {}
def set_library_config(library_config):
get_gui().current_db.prefs.set_namespaced(PREFS_NAMESPACE,
PREFS_KEY_SETTINGS,
library_config)
def get_library_config():
db = get_gui().current_db
library_id = get_library_uuid(db)
library_config = None
# Check whether this is a configuration needing to be migrated
# from json into database. If so: get it, set it, rename it in json.
if library_id in old_prefs:
#print("get prefs from old_prefs")
library_config = old_prefs[library_id]
set_library_config(library_config)
old_prefs["migrated to library db %s"%library_id] = old_prefs[library_id]
del old_prefs[library_id]
if library_config is None:
#print("get prefs from db")
library_config = db.prefs.get_namespaced(PREFS_NAMESPACE, PREFS_KEY_SETTINGS,
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
all_prefs = JSONConfig('plugins/fanfictiondownloader_plugin')
# Set defaults used by all. Library specific settings continue to
# take from here.
all_prefs.defaults['personal.ini'] = get_resources('plugin-example.ini')
all_prefs.defaults['updatemeta'] = True
all_prefs.defaults['updatecover'] = False
all_prefs.defaults['updateepubcover'] = False
all_prefs.defaults['keeptags'] = False
all_prefs.defaults['urlsfromclip'] = True
all_prefs.defaults['updatedefault'] = True
all_prefs.defaults['fileform'] = 'epub'
all_prefs.defaults['collision'] = OVERWRITE
all_prefs.defaults['deleteotherforms'] = False
all_prefs.defaults['adddialogstaysontop'] = False
all_prefs.defaults['includeimages'] = False
all_prefs.defaults['lookforurlinhtml'] = False
all_prefs.defaults['injectseries'] = False
all_prefs.defaults['send_lists'] = ''
all_prefs.defaults['read_lists'] = ''
all_prefs.defaults['addtolists'] = False
all_prefs.defaults['addtoreadlists'] = False
all_prefs.defaults['addtolistsonread'] = False
all_prefs.defaults['gcnewonly'] = False
all_prefs.defaults['gc_site_settings'] = {}
all_prefs.defaults['allow_gc_from_ini'] = True
all_prefs.defaults['countpagesstats'] = []
all_prefs.defaults['errorcol'] = ''
all_prefs.defaults['custom_cols'] = {}
# The list of settings to copy from all_prefs or the previous library
# when config is called for the first time on a library.
copylist = ['personal.ini',
'updatemeta',
'updatecover',
'updateepubcover',
'keeptags',
'urlsfromclip',
'updatedefault',
'fileform',
'collision',
'deleteotherforms',
'adddialogstaysontop',
'includeimages',
'lookforurlinhtml',
'injectseries',
'gcnewonly',
'gc_site_settings',
'allow_gc_from_ini']
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.
class PrefsFacade():
def __init__(self,all_prefs):
self.all_prefs = all_prefs
self.lastlibid = None
def _get_copylist_prefs(self,frompref):
return filter( lambda x : x[0] in copylist, frompref.items() )
def __init__(self,default_prefs):
self.default_prefs = default_prefs
self.libraryid = None
self.current_prefs = None
def _get_prefs(self):
libraryid = get_library_uuid(get_gui().current_db)
if libraryid not in self.all_prefs:
if self.lastlibid == None:
self.all_prefs[libraryid] = dict(self._get_copylist_prefs(self.all_prefs))
else:
self.all_prefs[libraryid] = dict(self._get_copylist_prefs(self.all_prefs[self.lastlibid]))
self.lastlibid = libraryid
return self.all_prefs[libraryid]
def _save_prefs(self,prefs):
libraryid = get_library_uuid(get_gui().current_db)
self.all_prefs[libraryid] = prefs
if self.current_prefs == None or self.libraryid != libraryid:
#print("self.current_prefs == None(%s) or self.libraryid != libraryid(%s)"%(self.current_prefs == None,self.libraryid != libraryid))
self.libraryid = libraryid
self.current_prefs = get_library_config()
return self.current_prefs
def __getitem__(self,k):
prefs = self._get_prefs()
if k not in prefs:
# pulls from all_prefs.defaults automatically if not set
# in all_prefs
return self.all_prefs[k]
# pulls from default_prefs.defaults automatically if not set
# in default_prefs
return self.default_prefs[k]
return prefs[k]
def __setitem__(self,k,v):
prefs = self._get_prefs()
prefs[k]=v
self._save_prefs(prefs)
# self._save_prefs(prefs)
# to be avoided--can cause unexpected results as possibly ancient
# all_pref settings may be pulled.
def __delitem__(self,k):
prefs = self._get_prefs()
del prefs[k]
self._save_prefs(prefs)
if k in prefs:
del prefs[k]
prefs = PrefsFacade(all_prefs)
def save_to_db(self):
set_library_config(self._get_prefs())
prefs = PrefsFacade(default_prefs)
class ConfigWidget(QWidget):
@@ -172,8 +176,11 @@ class ConfigWidget(QWidget):
if 'Count Pages' not in plugin_action.gui.iactions:
self.countpages_tab.setEnabled(False)
self.columns_tab = CustomColumnsTab(self, plugin_action)
tab_widget.addTab(self.columns_tab, 'Custom Columns')
self.std_columns_tab = StandardColumnsTab(self, plugin_action)
tab_widget.addTab(self.std_columns_tab, 'Standard Columns')
self.cust_columns_tab = CustomColumnsTab(self, plugin_action)
tab_widget.addTab(self.cust_columns_tab, 'Custom Columns')
self.other_tab = OtherTab(self, plugin_action)
tab_widget.addTab(self.other_tab, 'Other')
@@ -241,19 +248,32 @@ class ConfigWidget(QWidget):
prefs['countpagesstats'] = countpagesstats
# Standard Columns tab
colsnewonly = {}
for (col,checkbox) in self.std_columns_tab.stdcol_newonlycheck.iteritems():
colsnewonly[col] = checkbox.isChecked()
prefs['std_cols_newonly'] = colsnewonly
# Custom Columns tab
# error column
prefs['errorcol'] = unicode(self.columns_tab.errorcol.itemData(self.columns_tab.errorcol.currentIndex()).toString())
prefs['errorcol'] = unicode(self.cust_columns_tab.errorcol.itemData(self.cust_columns_tab.errorcol.currentIndex()).toString())
# cust cols
colsmap = {}
for (col,combo) in self.columns_tab.custcol_dropdowns.iteritems():
for (col,combo) in self.cust_columns_tab.custcol_dropdowns.iteritems():
val = unicode(combo.itemData(combo.currentIndex()).toString())
if val != 'none':
colsmap[col] = val
#print("colsmap[%s]:%s"%(col,colsmap[col]))
prefs['custom_cols'] = colsmap
colsnewonly = {}
for (col,checkbox) in self.cust_columns_tab.custcol_newonlycheck.iteritems():
colsnewonly[col] = checkbox.isChecked()
prefs['custom_cols_newonly'] = colsnewonly
prefs.save_to_db()
def edit_shortcuts(self):
self.save_settings()
# Force the menus to be rebuilt immediately, so we have all our actions registered
@@ -311,7 +331,7 @@ class BasicTab(QWidget):
self.l.addLayout(horz)
self.updatemeta = QCheckBox('Default Update Calibre &Metadata?',self)
self.updatemeta.setToolTip("On each download, FFDL offers an option to update Calibre's metadata (title, author, URL, tags, custom columns, etc) from the web site. <br />This sets whether that will default to on or off.")
self.updatemeta.setToolTip("On each download, FFDL offers an option to update Calibre's metadata (title, author, URL, tags, custom columns, etc) from the web site. <br />This sets whether that will default to on or off. <br />Columns set to 'New Only' in the column tabs will only be set for new books.")
self.updatemeta.setChecked(prefs['updatemeta'])
self.l.addWidget(self.updatemeta)
@@ -320,16 +340,25 @@ class BasicTab(QWidget):
self.updateepubcover.setChecked(prefs['updateepubcover'])
self.l.addWidget(self.updateepubcover)
self.l.addSpacing(10)
self.deleteotherforms = QCheckBox('Delete other existing formats?',self)
self.deleteotherforms.setToolTip('Check this to automatically delete all other ebook formats when updating an existing book.\nHandy if you have both a Nook(epub) and Kindle(mobi), for example.')
self.deleteotherforms.setChecked(prefs['deleteotherforms'])
self.l.addWidget(self.deleteotherforms)
self.updatecover = QCheckBox('Update Calibre Cover when Updating Metadata?',self)
self.updatecover.setToolTip("Update calibre book cover image from EPUB when metadata is updated. (EPUB only.)\nDoesn't go looking for new images on 'Update Calibre Metadata Only'.")
self.updatecover.setChecked(prefs['updatecover'])
self.l.addWidget(self.updatecover)
self.keeptags = QCheckBox('Keep Existing Tags when Updating Metadata?',self)
self.keeptags.setToolTip('Existing tags will be kept and any new tags added.\nCompleted and In-Progress tags will be still be updated, if known.\nLast Updated tags will be updated if lastupdate in include_subject_tags.')
self.keeptags.setToolTip("Existing tags will be kept and any new tags added.\nCompleted and In-Progress tags will be still be updated, if known.\nLast Updated tags will be updated if lastupdate in include_subject_tags.\n(If Tags is set to 'New Only' in the Standard Columns tab, this has no effect.)")
self.keeptags.setChecked(prefs['keeptags'])
self.l.addWidget(self.keeptags)
self.l.addSpacing(10)
self.urlsfromclip = QCheckBox('Take URLs from Clipboard?',self)
self.urlsfromclip.setToolTip('Prefill URLs from valid URLs in Clipboard when Adding New.')
self.urlsfromclip.setChecked(prefs['urlsfromclip'])
@@ -341,16 +370,13 @@ class BasicTab(QWidget):
self.updatedefault.setChecked(prefs['updatedefault'])
self.l.addWidget(self.updatedefault)
self.deleteotherforms = QCheckBox('Delete other existing formats?',self)
self.deleteotherforms.setToolTip('Check this to automatically delete all other ebook formats when updating an existing book.\nHandy if you have both a Nook(epub) and Kindle(mobi), for example.')
self.deleteotherforms.setChecked(prefs['deleteotherforms'])
self.l.addWidget(self.deleteotherforms)
self.adddialogstaysontop = QCheckBox("Keep 'Add New from URL(s)' dialog on top?",self)
self.adddialogstaysontop.setToolTip("Instructs the OS and Window Manager to keep the 'Add New from URL(s)'\ndialog on top of all other windows. Useful for dragging URLs onto it.")
self.adddialogstaysontop.setChecked(prefs['adddialogstaysontop'])
self.l.addWidget(self.adddialogstaysontop)
self.l.addSpacing(10)
# this is a cheat to make it easier for users to realize there's a new include_images features.
self.includeimages = QCheckBox("Include images in EPUBs?",self)
self.includeimages.setToolTip("Download and include images in EPUB stories. This is equivalent to adding:\n\n[epub]\ninclude_images:true\nkeep_summary_html:true\nmake_firstimage_cover:true\n\n ...to the top of personal.ini. Your settings in personal.ini will override this.")
@@ -656,6 +682,12 @@ class OtherTab(QWidget):
reset_confirmation_button.clicked.connect(self.reset_dialogs)
self.l.addWidget(reset_confirmation_button)
view_prefs_button = QPushButton('&View library preferences...', self)
view_prefs_button.setToolTip(_(
'View data stored in the library database for this plugin'))
view_prefs_button.clicked.connect(self.view_prefs)
self.l.addWidget(view_prefs_button)
self.l.insertStretch(-1)
def reset_dialogs(self):
@@ -667,6 +699,10 @@ class OtherTab(QWidget):
_('Confirmation dialogs have all been reset'),
show=True,
show_copy_button=False)
def view_prefs(self):
d = PrefsViewerDialog(self.plugin_action.gui, PREFS_NAMESPACE)
d.exec_()
permitted_values = {
'int' : ['numWords','numChapters'],
@@ -755,6 +791,7 @@ class CustomColumnsTab(QWidget):
self.l.addSpacing(5)
self.custcol_dropdowns = {}
self.custcol_newonlycheck = {}
for key, column in custom_columns.iteritems():
@@ -763,8 +800,8 @@ class CustomColumnsTab(QWidget):
# for (k,v) in column.iteritems():
# print("column['%s'] => %s"%(k,v))
horz = QHBoxLayout()
label = QLabel('%s(%s)'%(column['name'],key))
label.setToolTip("Update this %s column with..."%column['datatype'])
label = QLabel(column['name'])
label.setToolTip("Update this %s column(%s) with..."%(key,column['datatype']))
horz.addWidget(label)
dropdown = QComboBox(self)
dropdown.addItem('',QVariant('none'))
@@ -777,8 +814,15 @@ class CustomColumnsTab(QWidget):
dropdown.setToolTip("Metadata values valid for this type of column.\nValues that aren't valid for this enumeration column will be ignored.")
else:
dropdown.setToolTip("Metadata values valid for this type of column.")
horz.addWidget(dropdown)
newonlycheck = QCheckBox("New Only",self)
newonlycheck.setToolTip("Write to %s(%s) only for new\nbooks, not updates to existing books."%(column['name'],key))
self.custcol_newonlycheck[key] = newonlycheck
if key in prefs['custom_cols_newonly']:
newonlycheck.setChecked(prefs['custom_cols_newonly'][key])
horz.addWidget(newonlycheck)
self.l.addLayout(horz)
self.l.insertStretch(-1)
@@ -806,3 +850,51 @@ class CustomColumnsTab(QWidget):
#print("prefs['custom_cols'] %s"%prefs['custom_cols'])
class StandardColumnsTab(QWidget):
def __init__(self, parent_dialog, plugin_action):
self.parent_dialog = parent_dialog
self.plugin_action = plugin_action
QWidget.__init__(self)
columns=OrderedDict()
columns["title"]="Title"
columns["authors"]="Author(s)"
columns["publisher"]="Publisher"
columns["tags"]="Tags"
columns["languages"]="Languages"
columns["pubdate"]="Published Date"
columns["timestamp"]="Date"
columns["comments"]="Comments"
columns["series"]="Series"
columns["identifiers"]="Ids(url id only)"
self.l = QVBoxLayout()
self.setLayout(self.l)
label = QLabel("The standard calibre metadata columns are listed below. You may choose whether FFDL will fill each column automatically on updates or only for new books.")
label.setWordWrap(True)
self.l.addWidget(label)
self.l.addSpacing(5)
self.stdcol_newonlycheck = {}
for key, column in columns.iteritems():
horz = QHBoxLayout()
label = QLabel(column)
#label.setToolTip("Update this %s column(%s) with..."%(key,column['datatype']))
horz.addWidget(label)
newonlycheck = QCheckBox("New Only",self)
newonlycheck.setToolTip("Write to %s only for new\nbooks, not updates to existing books."%column)
self.stdcol_newonlycheck[key] = newonlycheck
if key in prefs['std_cols_newonly']:
newonlycheck.setChecked(prefs['std_cols_newonly'][key])
horz.addWidget(newonlycheck)
self.l.addLayout(horz)
self.l.insertStretch(-1)
+4 -9
View File
@@ -106,26 +106,21 @@ class AddNewDialog(SizePersistedDialog):
horz = QHBoxLayout()
label = QLabel('If Story Already Exists?')
label.setToolTip("What to do if there's already an existing story with the same title and author.")
horz.addWidget(label)
self.collision = QComboBox(self)
self.collision.setToolTip("What to do if there's already an existing story with the same URL or title and author.")
# add collision options
self.set_collisions()
i = self.collision.findText(prefs['collision'])
if i > -1:
self.collision.setCurrentIndex(i)
# self.collision.setToolTip(OVERWRITE+' will replace the existing story.\n'+
# UPDATE+' will download new chapters only and add to existing EPUB.\n'+
# ADDNEW+' will create a new story with the same title and author.\n'+
# SKIP+' will not download existing stories.\n'+
# CALIBREONLY+' will not download stories, but will update Calibre metadata.')
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 story in Calibre from web site?')
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)
@@ -431,9 +426,9 @@ class UpdateExistingDialog(SizePersistedDialog):
options_layout.addWidget(self.fileform)
label = QLabel('Update Mode:')
label.setToolTip("What sort of update to perform. May set default from plugin configuration.")
options_layout.addWidget(label)
self.collision = QComboBox(self)
self.collision.setToolTip("What sort of update to perform. May set default from plugin configuration.")
# add collision options
self.set_collisions()
i = self.collision.findText(prefs['collision'])
@@ -444,7 +439,7 @@ class UpdateExistingDialog(SizePersistedDialog):
options_layout.addWidget(self.collision)
self.updatemeta = QCheckBox('Update Calibre &Metadata?',self)
self.updatemeta.setToolTip('Update metadata for story in Calibre from web site? May set default from plugin configuration.')
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'])
options_layout.addWidget(self.updatemeta)
+101 -56
View File
@@ -116,9 +116,8 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
# items to prevent GC removing it.
self.menu_actions = []
self.qaction.setMenu(self.menu)
self.menu.aboutToShow.connect(self.about_to_show_menu)
self.menus_lock = threading.RLock()
self.menu.aboutToShow.connect(self.about_to_show_menu)
def initialization_complete(self):
# otherwise configured hot keys won't work until the menu's
@@ -134,10 +133,6 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
def rebuild_menus(self):
with self.menus_lock:
# Show the config dialog
# The config dialog can also be shown from within
# Preferences->Plugins, which is why the do_user_config
# method is defined on the base plugin class
do_user_config = self.interface_action_base_plugin.do_user_config
self.menu.clear()
self.actions_unique_map = {}
@@ -179,15 +174,6 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
shortcut_name=rmmenutxt,
triggered=partial(self.update_lists,add=False))
# try:
# self.add_send_action.setEnabled( len(self.gui.library_view.get_selected_ids()) > 0 )
# except:
# pass
# try:
# self.add_remove_action.setEnabled( len(self.gui.library_view.get_selected_ids()) > 0 )
# except:
# pass
self.menu.addSeparator()
self.get_list_action = self.create_menu_item_ex(self.menu, 'Get URLs from Selected Books', image='bookmarks.png',
unique_name='Get URLs from Selected Books',
@@ -203,13 +189,11 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
self.config_action = create_menu_action_unique(self, self.menu, '&Configure Plugin', shortcut=False,
image= 'config.png',
unique_name='Configure FanFictionDownLoader',
shortcut_name='Configure FanFictionDownLoader',
triggered=partial(do_user_config,parent=self.gui))
self.about_action = create_menu_action_unique(self, self.menu, '&About Plugin', shortcut=False,
self.about_action = create_menu_action_unique(self, self.menu, 'About Plugin', shortcut=False,
image= 'images/icon.png',
unique_name='About FanFictionDownLoader',
shortcut_name='About FanFictionDownLoader',
triggered=self.about)
# Before we finalize, make sure we delete any actions for menus that are no longer displayed
@@ -497,6 +481,7 @@ make_firstimage_cover:true
book['password'] = adapter.password
book['icon'] = 'plus.png'
book['status'] = 'Add'
if story.getMetadataRaw('datePublished'):
# should only happen when an adapter is broken, but better to
# fail gracefully.
@@ -505,6 +490,7 @@ make_firstimage_cover:true
if collision in (CALIBREONLY):
book['icon'] = 'metadata.png'
book['status'] = 'Meta'
# Dialogs should prevent this case now.
if collision in (UPDATE,UPDATEALWAYS) and fileform != 'epub':
@@ -549,7 +535,7 @@ make_firstimage_cover:true
raise NotGoingToDownload("Skipping duplicate story.","list_remove.png")
if len(identicalbooks) > 1:
raise NotGoingToDownload("More than one identical book--can't tell which to update/overwrite.","minusminus.png")
raise NotGoingToDownload("More than one identical book by Identifer URL or title/author(s)--can't tell which book to update/overwrite.","minusminus.png")
## changed: add new book when CALIBREONLY if none found.
if collision == CALIBREONLY and not identicalbooks:
@@ -560,6 +546,7 @@ make_firstimage_cover:true
book_id = identicalbooks.pop()
book['calibre_id'] = book_id
book['icon'] = 'edit-redo.png'
book['status'] = 'Update'
if book_id != None and collision != ADDNEW:
if collision in (CALIBREONLY):
@@ -705,7 +692,7 @@ make_firstimage_cover:true
self._add_or_update_book(book,options,prefs,mi)
if options['collision'] == CALIBREONLY or \
(options['updatemeta'] and book['good']):
( (options['updatemeta'] or book['added']) and book['good'] ):
self._update_metadata(db, book['calibre_id'], book, mi, options)
def _update_bad_book(self,book,db=None,label='errorcol',
@@ -768,46 +755,72 @@ make_firstimage_cover:true
self.previous = self.gui.library_view.currentIndex()
db = self.gui.current_db
if display_story_list(self.gui,
'Downloads finished, confirm to update Calibre',
prefs,
self.qaction.icon(),
job.result,
label_text='Stories will not be added or updated in Calibre without confirmation.',
offer_skip=True):
book_list = job.result
good_list = filter(lambda x : x['good'], book_list)
total_good = len(good_list)
book_list = job.result
good_list = filter(lambda x : x['good'], book_list)
bad_list = filter(lambda x : x['calibre_id'] and not x['good'], book_list)
payload = (good_list, bad_list, options)
msg = '''
<p>FFDL found <b>%s</b> good and <b>%s</b> bad updates.</p>
<p>See log for details.</p>
<p>Proceed with updating your library?</p>
'''%(len(good_list),len(bad_list))
self.gui.status_bar.show_message(_('Adding/Updating %s books.'%total_good))
htmllog='<html><body><table border="1"><tr><th>Status</th><th>Title</th><th>Author</th><th>URL</th><th>Comment</th></tr>'
for book in good_list:
if 'status' in book:
status = book['status']
else:
status = 'Good'
htmllog = htmllog + '<tr><td>' + '</td><td>'.join([status,book['title'],", ".join(book['author']),book['url'],book['comment']]) + '</td></tr>'
for book in bad_list:
if 'status' in book:
status = book['status']
else:
status = 'Bad'
htmllog = htmllog + '<tr><td>' + '</td><td>'.join([status,book['title'],", ".join(book['author']),book['url'],book['comment']]) + '</td></tr>'
htmllog = htmllog + '</table></body></html>'
self.gui.proceed_question(self._do_download_list_update,
payload, htmllog,
'FFDL log', 'FFDL download complete', msg,
show_copy_button=False)
def _do_download_list_update(self, payload):
(good_list,bad_list,options) = payload
total_good = len(good_list)
if total_good > 0:
self.gui.status_bar.show_message(_('Adding/Updating %s books.'%total_good))
if total_good > 0:
LoopProgressDialog(self.gui,
good_list,
partial(self._update_book, options=options, db=self.gui.current_db),
partial(self._update_books_completed, options=options),
init_label="Updating calibre for FanFiction stories...",
win_title="Update calibre for FanFiction stories",
status_prefix="Updated")
total_bad = len(bad_list)
if total_bad > 0:
custom_columns = self.gui.library_view.model().custom_columns
if prefs['errorcol'] != '' and prefs['errorcol'] in custom_columns:
self.gui.status_bar.show_message(_('Adding/Updating %s BAD books.'%total_bad))
label = custom_columns[prefs['errorcol']]['label']
## if error column and all bad.
LoopProgressDialog(self.gui,
good_list,
partial(self._update_book, options=options, db=self.gui.current_db),
partial(self._update_books_completed, options=options),
init_label="Updating calibre for FanFiction stories...",
win_title="Update calibre for FanFiction stories",
bad_list,
partial(self._update_bad_book, label=label, options=options, db=self.gui.current_db),
partial(self._update_books_completed, options=options, showlist=False),
init_label="Updating calibre for BAD FanFiction stories...",
win_title="Update calibre for BAD FanFiction stories",
status_prefix="Updated")
bad_list = filter(lambda x : x['calibre_id'] and not x['good'], book_list)
total_bad = len(bad_list)
if total_bad > 0:
custom_columns = self.gui.library_view.model().custom_columns
if prefs['errorcol'] != '' and prefs['errorcol'] in custom_columns:
self.gui.status_bar.show_message(_('Adding/Updating %s BAD books.'%total_bad))
label = custom_columns[prefs['errorcol']]['label']
## if error column and all bad.
LoopProgressDialog(self.gui,
bad_list,
partial(self._update_bad_book, label=label, options=options, db=self.gui.current_db),
partial(self._update_books_completed, options=options, showlist=False),
init_label="Updating calibre for BAD FanFiction stories...",
win_title="Update calibre for BAD FanFiction stories",
status_prefix="Updated")
def _add_or_update_book(self,book,options,prefs,mi=None):
db = self.gui.current_db
@@ -830,6 +843,7 @@ make_firstimage_cover:true
book['comment'] = "Adding format to book failed for some reason..."
book['good']=False
book['icon']='dialog_error.png'
book['status'] = 'Error'
if prefs['deleteotherforms']:
fmts = db.formats(book['calibre_id'], index_is_id=True).split(',')
@@ -844,6 +858,7 @@ make_firstimage_cover:true
return book_id
def _update_metadata(self, db, book_id, book, mi, options):
oldmi = db.get_metadata(book_id,index_is_id=True)
if prefs['keeptags']:
old_tags = db.get_tags(book_id)
# remove old Completed/In-Progress only if there's a new one.
@@ -859,7 +874,6 @@ make_firstimage_cover:true
mi.languages=[book['all_metadata']['langcode']]
else:
# Set language english, but only if not already set.
oldmi = db.get_metadata(book_id,index_is_id=True)
if not oldmi.languages:
mi.languages=['eng']
@@ -876,6 +890,32 @@ make_firstimage_cover:true
autid=db.get_author_id(auth)
db.set_link_field_for_author(autid, unicode(authurls[i]),
commit=False, notify=False)
# mi.title = oldmi.title
# mi.authors = oldmi.authors
# mi.publisher = oldmi.publisher
# mi.tags = oldmi.tags
# mi.languages = oldmi.languages
# mi.pubdate = oldmi.pubdate
# mi.timestamp = oldmi.timestamp
# mi.comments = oldmi.comments
# mi.series = oldmi.series
# mi.set_identifiers(oldmi.get_identifiers())
# implement 'newonly' flags here by setting to the current
# value again.
if not book['added']:
for (col,newonly) in prefs['std_cols_newonly'].iteritems():
if newonly:
if col == "identifiers":
mi.set_identifiers(oldmi.get_identifiers())
else:
try:
mi.__setattr__(col,oldmi.__getattribute__(col))
except AttributeError:
print("AttributeError? %s"%col)
pass
db.set_metadata(book_id,mi)
@@ -890,6 +930,9 @@ make_firstimage_cover:true
print("%s not an existing column, skipping."%col)
continue
coldef = custom_columns[col]
if col in prefs['custom_cols_newonly'] and prefs['custom_cols_newonly'][col] and not book['added']:
print("Skipping custom column(%s) update, set to New Books Only"%coldef['name'])
continue
if not meta.startswith('status-') and meta not in book['all_metadata'] or \
meta.startswith('status-') and 'status' not in book['all_metadata']:
print("No value for %s, skipping custom column(%s) update."%(meta,coldef['name']))
@@ -1117,6 +1160,7 @@ make_firstimage_cover:true
book['comment'] = "No story URL found."
book['good'] = False
book['icon'] = 'search_delete_saved.png'
book['status'] = 'Not Found'
else:
# get normalized url or None.
book['url'] = self._is_good_downloader_url(url)
@@ -1125,6 +1169,7 @@ make_firstimage_cover:true
book['comment'] = "URL is not a valid story URL."
book['good'] = False
book['icon']='dialog_error.png'
book['status'] = 'Bad URL'
def _get_story_url(self, db, book_id):
identifiers = db.get_identifiers(book_id,index_is_id=True)
+1
View File
@@ -177,6 +177,7 @@ def do_download_for_worker(book,options):
book['good']=False
book['comment']=unicode(e)
book['icon']='dialog_error.png'
book['status'] = 'Error'
print("Exception: %s:%s"%(book,unicode(e)))
traceback.print_exc()
+17
View File
@@ -423,6 +423,21 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#username:YourName
#password:yourpassword
[grangerenchanted.com]
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
## 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
[hlfiction.net]
[lumos.sycophanthex.com]
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
@@ -439,6 +454,8 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[nha.magical-worlds.us]
[occlumency.sycophanthex.com]
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
+3
View File
@@ -89,6 +89,9 @@ import adapter_hpfanficarchivecom
import adapter_svufictioncom
import adapter_twilightarchivescom
import adapter_wizardtalesnet
import adapter_nhamagicalworldsus
import adapter_hlfictionnet
import adapter_grangerenchantedcom
## This bit of complexity allows adapters to be added by just adding
## importing. It eliminates the long if/else clauses we used to need
@@ -176,7 +176,9 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
genrelist = metalist[0].split('/') # Hurt/Comfort already changed above.
goodgenres=True
for g in genrelist:
if g not in ffnetgenres:
print("g:(%s)"%g)
if g.strip() not in ffnetgenres:
print("g not in ffnetgenres")
goodgenres=False
if goodgenres:
self.story.extendList('genre',genrelist)
@@ -239,6 +241,15 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
## additional to what ever the
## slow_down_sleep_time setting is.
data = self._fetchUrl(url)
# some ancient stories have body tags inside them that cause
# soup parsing to discard the content. For story text we
# don't care about anything before "<div class='storytextp"
# (there's a space after storytextp, so no close quote(')) and
# this kills any body tags.
data = data[data.index("<div class='storytextp"):]
data.replace("<body","<notbody").replace("<BODY","<NOTBODY")
soup = bs.BeautifulSoup(data)
## Remove the 'share' button.
@@ -67,10 +67,10 @@ class FineStoriesComAdapter(BaseSiteAdapter):
return 'finestories.com'
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/s/10537 http://"+self.getSiteDomain()+"/s/10537:4010 http://"+self.getSiteDomain()+"/s/toryInfo.php?id=10537"
return "http://"+self.getSiteDomain()+"/s/10537 http://"+self.getSiteDomain()+"/s/10537:4010 http://"+self.getSiteDomain()+"/library/storyInfo.php?id=10537"
def getSiteURLPattern(self):
return re.escape("http://"+self.getSiteDomain()+"/s/")+r"(storyInfo.php\?id=)?\d+(:\d+)?(;\d+)?$"
return re.escape("http://"+self.getSiteDomain())+r"/(s|library)?/(storyInfo.php\?id=)?\d+(:\d+)?(;\d+)?$"
## Login seems to be reasonably standard across eFiction sites.
def needToLoginCheck(self, data):
@@ -160,13 +160,22 @@ class FineStoriesComAdapter(BaseSiteAdapter):
self.story.setMetadata('numChapters',len(self.chapterUrls))
# surprisingly, the detailed page does not give enough details, so go to author's page
asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')))
for lc2 in asoup.findAll('td', {'class' : 'lc2'}):
if lc2.find('a')['href'] == '/s/'+self.story.getMetadata('storyId'):
break
skip=0
i=0
while i == 0:
asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')+"&skip="+str(skip)))
self.story.addToList('category',lc2.find('div', {'class' : 'typediv'}).text)
a = asoup.findAll('td', {'class' : 'lc2'})
for lc2 in a:
if lc2.find('a')['href'] == '/s/'+self.story.getMetadata('storyId'):
i=1
break
if a[len(a)-1] == lc2:
skip=skip+10
for cat in lc2.findAll('div', {'class' : 'typediv'}):
self.story.addToList('category',cat.text)
self.story.setMetadata('numWords', lc2.findNext('td', {'class' : 'num'}).text)
@@ -202,14 +211,14 @@ class FineStoriesComAdapter(BaseSiteAdapter):
self.story.addToList('genre',genre)
if 'Posted' in label:
self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
self.story.setMetadata('datePublished', makeDate(stripHTML(value.split('/ (')[0]), self.dateformat))
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value.split('/ (')[0]), self.dateformat))
if 'Concluded' in label:
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value.split('/ (')[0]), self.dateformat))
if 'Updated' in label:
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value.split('/ (')[0]), self.dateformat))
status = lc4.find('span', {'class' : 'ab'})
if status != None:
@@ -0,0 +1,302 @@
# -*- coding: utf-8 -*-
# Copyright 2012 Fanficdownloader team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import time
import logging
import re
import urllib2
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, makeDate
def getClass():
return GrangerEnchantedCom
# Class name has to be unique. Our convention is camel case the
# sitename with Adapter at the end. www is skipped.
class GrangerEnchantedCom(BaseSiteAdapter):
def __init__(self, config, url):
BaseSiteAdapter.__init__(self, config, url)
self.decode = ["Windows-1252",
"utf8"] # 1252 is a superset of iso-8859-1.
# Most sites that claim to be
# iso-8859-1 (and some that claim to be
# utf8) are really windows-1252.
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
self.password = ""
self.is_adult=False
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
self.story.setMetadata('section',self.parsedUrl.path.split('/',)[1])
# normalized story URL.
if "malfoymanor" in self.parsedUrl.netloc:
self._setURL('http://malfoymanor.' + self.getSiteDomain() + '/themanor/viewstory.php?sid='+self.story.getMetadata('storyId'))
self.story.addToList("category","The Manor")
else:
self._setURL('http://' + self.getSiteDomain() + '/enchant/viewstory.php?sid='+self.story.getMetadata('storyId'))
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','gech')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Harry Potter")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%d/%b/%Y"
@staticmethod # must be @staticmethod, don't remove it.
def getSiteDomain():
# The site domain. Does have www here, if it uses it.
return 'grangerenchanted.com'
@classmethod
def getAcceptDomains(cls):
return ['grangerenchanted.com','malfoymanor.grangerenchanted.com']
def getSiteExampleURLs(self):
return "http://grangerenchanted.com/enchant/viewstory.php?sid=1234 http://malfoymanor.grangerenchanted.com/themanor/viewstory.php?sid=1234"
def getSiteURLPattern(self):
return r"http://(malfoymanor.)?grangerenchanted.com/(enchant|themanor)?/viewstory.php\?sid=\d+$"
## Login seems to be reasonably standard across eFiction sites.
def needToLoginCheck(self, data):
if 'Registered Users Only' in data \
or 'There is no such account on our website' in data \
or "That password doesn't match the one in our database" in data:
return True
else:
return False
def performLogin(self, url):
params = {}
if self.password:
params['penname'] = self.username
params['password'] = self.password
else:
params['penname'] = self.getConfig("username")
params['password'] = self.getConfig("password")
params['cookiecheck'] = '1'
params['submit'] = 'Submit'
if "enchant" in self.story.getMetadata('section'):
loginUrl = 'http://grangerenchanted.com/enchant/user.php?action=login'
else:
loginUrl = 'http://malfoymanor.grangerenchanted.com/themanor/user.php?action=login'
logging.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
logging.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=1"
else:
addurl=""
# index=1 makes sure we see the story chapter index. Some
# sites skip that for one-chapter stories.
url = self.url+addurl
logging.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)
m = re.search(r"'viewstory.php\?sid=\d+((?:&amp;ageconsent=ok)?&amp;warning=\d+)'",data)
if m != None:
if self.is_adult or self.getConfig("is_adult"):
# We tried the default and still got a warning, so
# let's pull the warning number from the 'continue'
# link and reload data.
addurl = m.group(1)
# correct stupid &amp; error in url.
addurl = addurl.replace("&amp;","&")
url = self.url+'&index=1'+addurl
logging.debug("URL 2nd try: "+url)
try:
data = self._fetchUrl(url)
except urllib2.HTTPError, e:
if e.code == 404:
raise exceptions.StoryDoesNotExist(self.url)
else:
raise e
else:
raise exceptions.AdultCheckRequired(self.url)
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
# use BeautifulSoup HTML parser to make everything easier to find.
soup = 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',a.string)
# 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+'/'+self.story.getMetadata('section')+'/'+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'))
for cat in cats:
self.story.addToList('category',cat.string)
if 'Characters' in label:
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
for char in chars:
self.story.addToList('characters',char.string)
if 'Genre' in label:
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=4'))
for genre in genres:
self.story.addToList('genre',genre.string)
if 'Warnings' in label:
warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2'))
for warning in warnings:
self.story.addToList('warnings',warning.string)
if 'Completed' in label:
if 'Yes' in value:
self.story.setMetadata('status', 'Completed')
else:
self.story.setMetadata('status', 'In-Progress')
if 'Published' in label:
self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
if 'Updated' in label:
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
try:
# Find Series name from series URL.
a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+"))
series_name = a.string
series_url = 'http://'+self.host+'/'+self.story.getMetadata('section')+'/'+a['href']
# use BeautifulSoup HTML parser to make everything easier to find.
seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url))
# can't use ^viewstory...$ in case of higher rated stories with javascript href.
storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+'))
i=1
for a in storyas:
# skip 'report this' and 'TOC' links
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
self.setSeries(series_name, i)
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):
logging.debug('Getting chapter text from: %s' % url)
soup = bs.BeautifulStoneSoup(self._fetchUrl(url),
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
div = soup.find('div', {'id' : 'story1'})
if None == div:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
return self.utf8FromSoup(url,div)
@@ -0,0 +1,233 @@
# -*- coding: utf-8 -*-
# Copyright 2012 Fanficdownloader team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import time
import logging
import re
import urllib2
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, makeDate
def getClass():
return HLFictionNetAdapter
# Class name has to be unique. Our convention is camel case the
# sitename with Adapter at the end. www is skipped.
class HLFictionNetAdapter(BaseSiteAdapter):
def __init__(self, config, url):
BaseSiteAdapter.__init__(self, config, url)
self.decode = ["Windows-1252",
"utf8"] # 1252 is a superset of iso-8859-1.
# Most sites that claim to be
# iso-8859-1 (and some that claim to be
# utf8) are really windows-1252.
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
self.password = ""
self.is_adult=False
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','hlf')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Highlander")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%m/%d/%y"
@staticmethod # must be @staticmethod, don't remove it.
def getSiteDomain():
# The site domain. Does have www here, if it uses it.
return 'hlfiction.net'
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
def getSiteURLPattern(self):
return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$"
## Getting the chapter list and the meta data, plus 'is adult' checking.
def extractChapterUrlsAndMetadata(self):
# index=1 makes sure we see the story chapter index. Some
# sites skip that for one-chapter stories.
url = self.url
logging.debug("URL: "+url)
try:
data = self._fetchUrl(url)
except urllib2.HTTPError, e:
if e.code == 404:
raise exceptions.StoryDoesNotExist(self.url)
else:
raise e
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
raise exceptions.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 and author
a = soup.find('div', {'id' : 'pagetitle'})
aut = a.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
self.story.setMetadata('authorId',aut['href'].split('=')[1])
self.story.setMetadata('authorUrl','http://'+self.host+'/'+aut['href'])
self.story.setMetadata('author',aut.string)
aut.extract()
self.story.setMetadata('title',a.string[:(len(a.string)-3)])
# Find the chapters:
chapters=soup.find('select')
if chapters != None:
for chapter in chapters.findAll('option'):
# just in case there's tags, like <i> in chapter titles.
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/viewstory.php?sid='+self.story.getMetadata('storyId')+'&chapter='+chapter['value']))
else:
self.chapterUrls.append((self.story.getMetadata('title'),url))
self.story.setMetadata('numChapters',len(self.chapterUrls))
asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')))
for list in asoup.findAll('div', {'class' : re.compile('listbox\s+')}):
a = list.find('a')
if ('viewstory.php?sid='+self.story.getMetadata('storyId')) in a['href']:
break
# 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 = list.findAll('span', {'class' : 'classification'})
for labelspan in labels:
label = labelspan.string
value = labelspan.nextSibling
if 'Summary' in label:
## Everything until the next span class='label'
svalue = ""
while not defaultGetattr(value,'class') == 'classification':
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[:len(value)-2])
if 'Word count' in label:
self.story.setMetadata('numWords', value)
if 'Categories' in label:
cats = labelspan.parent.findAll('a',href=re.compile(r'categories.php\?catid=\d+'))
for cat in cats:
self.story.addToList('category',cat.string)
if 'Characters' in label:
for char in value.string.split(', '):
if not 'None' in char:
self.story.addToList('characters',char)
if 'Genre' in label:
for genre in value.string.split(', '):
if not 'None' in genre:
self.story.addToList('genre',genre)
if 'Warnings' in label:
for warning in value.string.split(', '):
if not 'None' in warning:
self.story.addToList('warnings',warning)
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 = list.find('a', href=re.compile(r"series.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))
# can't use ^viewstory...$ in case of higher rated stories with javascript href.
storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+'))
i=1
for a in storyas:
# skip 'report this' and 'TOC' links
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if ('viewstory.php?sid='+self.story.getMetadata('storyId')) in a['href']:
self.setSeries(series_name, i)
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):
logging.debug('Getting chapter text from: %s' % url)
soup = bs.BeautifulStoneSoup(self._fetchUrl(url),
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
div = soup.find('div', {'id' : 'story'})
if None == div:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
return self.utf8FromSoup(url,div)
@@ -0,0 +1,216 @@
# -*- coding: utf-8 -*-
# Copyright 2012 Fanficdownloader team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import time
import logging
import re
import urllib2
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, makeDate
def getClass():
return NHAMagicalWorldsUsAdapter
# Class name has to be unique. Our convention is camel case the
# sitename with Adapter at the end. www is skipped.
class NHAMagicalWorldsUsAdapter(BaseSiteAdapter):
def __init__(self, config, url):
BaseSiteAdapter.__init__(self, config, url)
self.decode = ["Windows-1252",
"utf8"] # 1252 is a superset of iso-8859-1.
# Most sites that claim to be
# iso-8859-1 (and some that claim to be
# utf8) are really windows-1252.
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
self.password = ""
self.is_adult=False
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','nha')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Buffy the Vampire Slayer")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = " %m/%d/%y"
@staticmethod # must be @staticmethod, don't remove it.
def getSiteDomain():
# The site domain. Does have www here, if it uses it.
return 'nha.magical-worlds.us'
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
def getSiteURLPattern(self):
return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$"
## Getting the chapter list and the meta data, plus 'is adult' checking.
def extractChapterUrlsAndMetadata(self):
url = self.url
logging.debug("URL: "+url)
try:
data = self._fetchUrl(url)
except urllib2.HTTPError, e:
if e.code == 404:
raise exceptions.StoryDoesNotExist(self.url)
else:
raise e
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
raise exceptions.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.
# 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)
asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')))
try:
# in case link points somewhere other than the first chapter
a = soup.findAll('option')[1]['value']
self.story.setMetadata('storyId',a.split('=',)[1])
url = 'http://'+self.host+'/'+a
soup = bs.BeautifulSoup(self._fetchUrl(url))
except:
pass
for info in asoup.findAll('table', {'width' : '100%', 'bordercolor' : re.compile(r'#')}):
a = info.find('a')
if ('viewstory.php?sid='+self.story.getMetadata('storyId')) in a['href']:
self.story.setMetadata('title',a.string)
break
# Find the chapters:
chapters=soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+'&chapter=\d$'))
if len(chapters) == 0:
self.chapterUrls.append((self.story.getMetadata('title'),url))
else:
for chapter in chapters:
# just in case there's tags, like <i> in chapter titles.
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href']))
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):
try:
return d.name
except:
return ""
cats = info.findAll('a',href=re.compile('categories.php'))
for cat in cats:
self.story.addToList('category',cat.string)
a = info.find('a', href=re.compile(r'reviews.php\?sid='+self.story.getMetadata('storyId')))
val = a.nextSibling
svalue = ""
while not defaultGetattr(val) == 'br':
val = val.nextSibling
val = val.nextSibling
while not defaultGetattr(val) == 'br':
svalue += str(val)
val = val.nextSibling
self.setDescription(url,svalue)
#does not provide convenient way to get word count
labels = info.findAll('i')
for labelspan in labels:
value = labelspan.nextSibling
label = stripHTML(labelspan)
if 'Rating' in label:
self.story.setMetadata('rating', value.split(' -')[0])
if 'Genres' in label:
genres = value.string.split(', ')
for genre in genres:
if 'None' not in genre:
self.story.addToList('genre',genre.split(' -')[0])
if 'Characters' in label:
chars = value.string.split(', ')
for char in chars:
if 'None' not in char:
self.story.addToList('characters',char.split(' -')[0])
if 'Warnings' in label:
warnings = value.string.split(', ')
for warning in warnings:
if 'None' not in warning:
self.story.addToList('warnings',warning.split(' -')[0])
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(value.split(' -')[0], self.dateformat))
if 'Updated' in label:
# there's a stray [ at the end.
#value = value[0:-1]
self.story.setMetadata('dateUpdated', makeDate(value.split(' -')[0], self.dateformat))
# grab the text for an individual chapter.
def getChapterText(self, url):
logging.debug('Getting chapter text from: %s' % url)
data = self._fetchUrl(url)
soup = bs.BeautifulSoup(data, selfClosingTags=('br','hr','span','center')) # some chapters seem to be hanging up on those tags, so it is safer to close them
story = soup.find('div', {"id" : "story"})
if None == story:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
return self.utf8FromSoup(url,story)
@@ -133,7 +133,7 @@ class TheWritersCoffeeShopComSiteAdapter(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
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+"))
@@ -36,6 +36,13 @@ class YourFanfictionComAdapter(BaseSiteAdapter):
def __init__(self, config, url):
BaseSiteAdapter.__init__(self, config, url)
# yourfanfiction.com blocks the default user-agent. However,
# when asked, they said it was just general anti-spam, not
# targeted as us and offered to 'whitelist our IP'. Clearly,
# that wouldn't work, but it does let me do this in good
# conscience:
self.opener.addheaders = [('User-agent', 'FFDL/1.6')]
self.decode = ["Windows-1252",
"utf8"] # 1252 is a superset of iso-8859-1.
# Most sites that claim to be
@@ -108,7 +115,7 @@ class YourFanfictionComAdapter(BaseSiteAdapter):
# viewstory.php?sid=1654&amp;ageconsent=ok&amp;warning=5
#print data
#m = re.search(r"'viewstory.php\?sid=1882(&amp;warning=4)'",data)
m = re.search(r"'viewstory.php\?sid=\d+((?:&amp;ageconsent=ok)?&amp;warning=\d+)'",data)
m = re.search(r"'viewstory.php\?sid=\d+((&amp;ageconsent=ok)?&amp;warning=\d+)'",data)
if m != None:
if self.is_adult or self.getConfig("is_adult"):
# We tried the default and still got a warning, so
@@ -116,7 +123,8 @@ class YourFanfictionComAdapter(BaseSiteAdapter):
# link and reload data.
addurl = m.group(1)
# correct stupid &amp; error in url.
addurl = addurl.replace("&amp;","&")
# explicitly put ageconsent because google appengine regexp doesn't include it for some reason.
addurl = addurl.replace("&amp;","&")+'&ageconsent=ok'
url = self.url+'&index=1'+addurl
logging.debug("URL 2nd try: "+url)
@@ -134,8 +142,16 @@ class YourFanfictionComAdapter(BaseSiteAdapter):
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.
# because for some reason, this works while simple 'print data' errors on ascii conversion.
# loopdata = data
# chklen=5000
# while len(loopdata) > 0:
# if len(loopdata) < 5000:
# chklen = len(loopdata)
# logging.info("loopdata: %s" % loopdata[:chklen])
# loopdata = loopdata[chklen:]
soup = bs.BeautifulSoup(data)
# print data
# Now go hunting for all the meta data and the chapter list.
@@ -238,7 +254,6 @@ class YourFanfictionComAdapter(BaseSiteAdapter):
storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+'))
i=1
for a in storyas:
print("series a['href']:%s"%a['href'])
# skip 'report this' and 'TOC' links
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
+18 -11
View File
@@ -57,18 +57,9 @@
<h3>New Sites</h3>
<p>
New sites finestories.com, www.hpfanficarchive.com, svufiction.com, www.twilightarchives.com and www.wizardtales.net.
New sites grangerenchanted.com, hlfiction.net and nha.magical-worlds.us.
<br />Thanks, Ida!
</p>
<h3>New Feature</h3>
<p>
FFDL now supports multiple authors for AO3, TtH and
wraithbait.com. If you know of other sites we support
that can have more than one author per story, please let
us know.
</p>
<p>
Questions? Check out our
@@ -78,7 +69,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-19.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
<a href="http://4-4-20.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
</p>
<div id='error'>
{{ error_message }}
@@ -499,6 +490,22 @@
Use the URL of the story's first chapter, such as
<br /><a href="http://www.wizardtales.net/viewstory.php?sid=1234">http://www.wizardtales.net/viewstory.php?sid=1234</a>
</dd>
<dt>grangerenchanted.com</dt>
<dd>
Use the URL of the story's chapter list, such as
<br /><a href="http://grangerenchanted.com/enchant/viewstory.php?sid=1234">http://grangerenchanted.com/enchant/viewstory.php?sid=1234</a> or
<br /><a href="http://malfoymanor.grangerenchanted.com/enchant/viewstory.php?sid=1234">http://grangerenchanted.com/enchant/viewstory.php?sid=1234</a>
</dd>
<dt>hlfiction.net</dt>
<dd>
Use the URL of the story's first chapter, such as
<br /><a href="http://hlfiction.net/viewstory.php?sid=1234">http://hlfiction.net/viewstory.php?sid=1234</a>
</dd>
<dt>nha.magical-worlds.us</dt>
<dd>
Use the URL of the story's chapter list, such as
<br /><a href="http://nha.magical-worlds.us/viewstory.php?sid=1234">http://nha.magical-worlds.us/viewstory.php?sid=1234</a>
</dd>
</dl>
<p>
A few additional things to know, which will make your life substantially easier:
+17
View File
@@ -409,6 +409,21 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#username:YourName
#password:yourpassword
[grangerenchanted.com]
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
## 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
[hlfiction.net]
[lumos.sycophanthex.com]
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
@@ -425,6 +440,8 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[nha.magical-worlds.us]
[occlumency.sycophanthex.com]
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In