mirror of
https://github.com/wassname/FanFicFare.git
synced 2026-09-13 12:11:20 +08:00
Compare commits
43
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4e5e4a47a | ||
|
|
8632cfdbd6 | ||
|
|
c943ce7fce | ||
|
|
7ccddd5a07 | ||
|
|
3678bf6bf1 | ||
|
|
33bb1b2d29 | ||
|
|
3141191e43 | ||
|
|
536ea0b027 | ||
|
|
34e03bf4eb | ||
|
|
48c81b1d1e | ||
|
|
381d3031e6 | ||
|
|
a193e80f88 | ||
|
|
3410e20412 | ||
|
|
7f568d54bf | ||
|
|
619141ef94 | ||
|
|
e6128b3174 | ||
|
|
875e139bcd | ||
|
|
db97d126bf | ||
|
|
92597efce6 | ||
|
|
6b9cb35761 | ||
|
|
6630e6bb9c | ||
|
|
c26bca0f6d | ||
|
|
8cd3663d82 | ||
|
|
fdb45183c7 | ||
|
|
284ef7578e | ||
|
|
6589dcd4b6 | ||
|
|
81a75d2097 | ||
|
|
df09eadf81 | ||
|
|
abde9fdf8d | ||
|
|
b76e50719b | ||
|
|
053b629d4b | ||
|
|
60a2e22c93 | ||
|
|
c6127b2087 | ||
|
|
e53661bb06 | ||
|
|
bb86f55c4a | ||
|
|
e2e086f2e5 | ||
|
|
101e3d9866 | ||
|
|
6c9cfa49f8 | ||
|
|
2db927665a | ||
|
|
cd8f5f2769 | ||
|
|
020e588527 | ||
|
|
5a63bdff8f | ||
|
|
f742e581c9 |
@@ -1,6 +1,6 @@
|
||||
# ffd-retief-hrd fanfictiondownloader
|
||||
application: fanfictiondownloader
|
||||
version: 4-4-43
|
||||
version: 4-4-48
|
||||
runtime: python27
|
||||
api_version: 1
|
||||
threadsafe: true
|
||||
|
||||
@@ -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, 9)
|
||||
version = (1, 7, 15)
|
||||
minimum_calibre_version = (0, 8, 57)
|
||||
|
||||
#: This field defines the GUI plugin class that contains all the code
|
||||
@@ -80,11 +80,29 @@ class FanFictionDownLoaderBase(InterfaceActionBase):
|
||||
if ac is not None:
|
||||
ac.apply_settings()
|
||||
|
||||
# For testing, run from command line with this:
|
||||
# calibre-debug -e __init__.py
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
from PyQt4.Qt import QApplication
|
||||
from calibre.gui2.preferences import test_widget
|
||||
app = QApplication([])
|
||||
test_widget('Advanced', 'Plugins')
|
||||
def cli_main(self,argv):
|
||||
# I believe there's no performance hit loading these here when
|
||||
# CLI--it would load everytime anyway.
|
||||
from StringIO import StringIO
|
||||
from calibre.library import db
|
||||
from calibre_plugins.fanfictiondownloader_plugin.downloader import main as ffdl_main
|
||||
from calibre_plugins.fanfictiondownloader_plugin.prefs import PrefsFacade
|
||||
from calibre.utils.config import prefs as calibre_prefs
|
||||
from optparse import OptionParser
|
||||
|
||||
parser = OptionParser('%prog --run-plugin '+self.name+' -- [options] <storyurl>')
|
||||
parser.add_option('--library-path', '--with-library', default=None, help=_('Path to the calibre library. Default is to use the path stored in the settings.'))
|
||||
# parser.add_option('--dont-notify-gui', default=False, action='store_true',
|
||||
# help=_('Do not notify the running calibre GUI (if any) that the database has'
|
||||
# ' changed. Use with care, as it can lead to database corruption!'))
|
||||
|
||||
pargs = [x for x in argv if x.startswith('--with-library') or x.startswith('--library-path')
|
||||
or not x.startswith('-')]
|
||||
opts, args = parser.parse_args(pargs)
|
||||
|
||||
ffdl_prefs = PrefsFacade(db(path=opts.library_path,
|
||||
read_only=True))
|
||||
ffdl_main(argv[1:],
|
||||
parser=parser,
|
||||
passed_defaultsini=StringIO(get_resources("defaults.ini")),
|
||||
passed_personalini=StringIO(ffdl_prefs["personal.ini"]))
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
'<a href="http://www.mobileread.com/forums/showthread.php?t=134856">Reading List</a>',
|
||||
'<a href="http://www.mobileread.com/forums/showthread.php?t=126727">Extract ISBN</a>' and
|
||||
'<a href="http://www.mobileread.com/forums/showthread.php?t=134000">Count Pages</a>'
|
||||
plugins. bbcodeutils code contributed by Pau Sanchez.</p>
|
||||
plugins.</p>
|
||||
|
||||
<p>
|
||||
Calibre officially distributes plugins from the mobileread.com forum site.
|
||||
|
||||
+57
-165
@@ -15,160 +15,38 @@ from PyQt4.Qt import (QDialog, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QCheckBox, QPushButton, QTabWidget, QVariant, QScrollArea,
|
||||
QDialogButtonBox )
|
||||
|
||||
from calibre.gui2 import dynamic, info_dialog
|
||||
from calibre.utils.config import JSONConfig
|
||||
from calibre.gui2.ui import get_gui
|
||||
|
||||
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)
|
||||
|
||||
from calibre_plugins.fanfictiondownloader_plugin.common_utils \
|
||||
import ( get_library_uuid, KeyboardConfigDialog, PrefsViewerDialog )
|
||||
import ( KeyboardConfigDialog, PrefsViewerDialog )
|
||||
|
||||
from calibre.gui2.complete import MultiCompleteLineEdit
|
||||
|
||||
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['rejecturls'] = ''
|
||||
default_prefs['rejectreasons'] = '''Sucked
|
||||
Boring
|
||||
Dup from another site'''
|
||||
|
||||
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['allow_custcol_from_ini'] = True
|
||||
|
||||
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
|
||||
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,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 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 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)
|
||||
|
||||
def __delitem__(self,k):
|
||||
prefs = self._get_prefs()
|
||||
if k in prefs:
|
||||
del prefs[k]
|
||||
|
||||
def save_to_db(self):
|
||||
set_library_config(self._get_prefs())
|
||||
|
||||
prefs = PrefsFacade(default_prefs)
|
||||
|
||||
class RejectURLList:
|
||||
def __init__(self,prefs):
|
||||
self.prefs = prefs
|
||||
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:
|
||||
@@ -176,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:
|
||||
@@ -205,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):
|
||||
@@ -282,12 +168,14 @@ class ConfigWidget(QWidget):
|
||||
prefs['updatecover'] = self.basic_tab.updatecover.isChecked()
|
||||
prefs['updateepubcover'] = self.basic_tab.updateepubcover.isChecked()
|
||||
prefs['keeptags'] = self.basic_tab.keeptags.isChecked()
|
||||
prefs['showmarked'] = self.basic_tab.showmarked.isChecked()
|
||||
prefs['urlsfromclip'] = self.basic_tab.urlsfromclip.isChecked()
|
||||
prefs['updatedefault'] = self.basic_tab.updatedefault.isChecked()
|
||||
prefs['deleteotherforms'] = self.basic_tab.deleteotherforms.isChecked()
|
||||
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:
|
||||
@@ -448,6 +336,13 @@ class BasicTab(QWidget):
|
||||
|
||||
self.l.addSpacing(10)
|
||||
|
||||
self.showmarked = QCheckBox("Show added/updated books when finished?",self)
|
||||
self.showmarked.setToolTip("Show added/updated books only when finished.\nYou can also manually search for 'marked:ffdl_success'.\n'marked:ffdl_failed' is also available, or search 'marked:ffdl' for both.")
|
||||
self.showmarked.setChecked(prefs['showmarked'])
|
||||
self.l.addWidget(self.showmarked)
|
||||
|
||||
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'])
|
||||
@@ -477,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'])
|
||||
@@ -520,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,
|
||||
@@ -535,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,
|
||||
@@ -554,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_()
|
||||
@@ -911,7 +803,7 @@ titleLabels = {
|
||||
'ships':'Relationships',
|
||||
'datePublished':'Published',
|
||||
'dateUpdated':'Updated',
|
||||
'dateCreated':'Packaged',
|
||||
'dateCreated':'Created',
|
||||
'rating':'Rating',
|
||||
'warnings':'Warnings',
|
||||
'numChapters':'Chapters',
|
||||
@@ -922,7 +814,7 @@ titleLabels = {
|
||||
'extratags':'Extra Tags',
|
||||
'title':'Title',
|
||||
'storyUrl':'Story URL',
|
||||
'description':'Summary',
|
||||
'description':'Description',
|
||||
'author':'Author',
|
||||
'authorUrl':'Author URL',
|
||||
'formatname':'File Format',
|
||||
|
||||
+288
-310
@@ -7,36 +7,35 @@ __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)
|
||||
|
||||
SKIP='Skip'
|
||||
ADDNEW='Add New Book'
|
||||
UPDATE='Update EPUB if New Chapters'
|
||||
UPDATEALWAYS='Update EPUB Always'
|
||||
OVERWRITE='Overwrite if Newer'
|
||||
OVERWRITEALWAYS='Overwrite Always'
|
||||
CALIBREONLY='Update Calibre Metadata Only'
|
||||
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'
|
||||
UPDATE=u'Update EPUB if New Chapters'
|
||||
UPDATEALWAYS=u'Update EPUB Always'
|
||||
OVERWRITE=u'Overwrite if Newer'
|
||||
OVERWRITEALWAYS=u'Overwrite Always'
|
||||
CALIBREONLY=u'Update Calibre Metadata Only'
|
||||
collision_order=[SKIP,
|
||||
ADDNEW,
|
||||
UPDATE,
|
||||
@@ -44,7 +43,66 @@ collision_order=[SKIP,
|
||||
OVERWRITE,
|
||||
OVERWRITEALWAYS,
|
||||
CALIBREONLY,]
|
||||
|
||||
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
|
||||
@@ -69,6 +127,36 @@ class NotGoingToDownload(Exception):
|
||||
class DroppableQTextEdit(QTextEdit):
|
||||
def __init__(self,parent):
|
||||
QTextEdit.__init__(self,parent)
|
||||
|
||||
def dropEvent(self,event):
|
||||
# print("event:%s"%event)
|
||||
|
||||
mimetype='text/uri-list'
|
||||
|
||||
urllist=[]
|
||||
filelist="%s"%event.mimeData().data(mimetype)
|
||||
for f in filelist.splitlines():
|
||||
#print("filename:%s"%f)
|
||||
if f.endswith(".eml"):
|
||||
fhandle = urllib.urlopen(f)
|
||||
#print("file:\n%s\n\n"%fhandle.read())
|
||||
msg = email.message_from_file(fhandle)
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
#print("part type:%s"%part.get_content_type())
|
||||
if part.get_content_type() == "text/html":
|
||||
#print("URL list:%s"%get_urls_from_data(part.get_payload(decode=True)))
|
||||
urllist.extend(get_urls_from_html(part.get_payload(decode=True)))
|
||||
if part.get_content_type() == "text/plain":
|
||||
#print("part content:text/plain")
|
||||
# print("part content:%s"%part.get_payload(decode=True))
|
||||
urllist.extend(get_urls_from_text(part.get_payload(decode=True)))
|
||||
else:
|
||||
urllist.extend(get_urls_from_text("%s"%msg))
|
||||
|
||||
if urllist:
|
||||
self.append("\n".join(urllist))
|
||||
return QTextEdit.dropEvent(self,event)
|
||||
|
||||
def canInsertFromMimeData(self, source):
|
||||
if source.hasUrls():
|
||||
@@ -84,12 +172,11 @@ class DroppableQTextEdit(QTextEdit):
|
||||
|
||||
class AddNewDialog(SizePersistedDialog):
|
||||
|
||||
def __init__(self, gui, prefs, icon, url_list_text):
|
||||
SizePersistedDialog.__init__(self, gui, 'FanFictionDownLoader plugin:add new dialog')
|
||||
self.gui = gui
|
||||
go_signal = pyqtSignal(object, object, object, object)
|
||||
|
||||
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()
|
||||
@@ -98,85 +185,189 @@ class AddNewDialog(SizePersistedDialog):
|
||||
self.setWindowTitle('FanFictionDownLoader')
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
self.l.addWidget(QLabel('Story URL(s), one per line:'))
|
||||
self.toplabel=QLabel("Toplabel")
|
||||
self.l.addWidget(self.toplabel)
|
||||
self.url = DroppableQTextEdit(self)
|
||||
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.url.setToolTip("UrlTooltip")
|
||||
self.url.setLineWrapMode(QTextEdit.NoWrap)
|
||||
self.url.setText(url_list_text)
|
||||
self.l.addWidget(self.url)
|
||||
|
||||
self.merge = self.newmerge = False
|
||||
|
||||
# elements to hide when doing merge.
|
||||
self.mergehide = []
|
||||
# elements to show again when doing *update* merge
|
||||
self.mergeupdateshow = []
|
||||
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel('Output &Format:')
|
||||
horz.addWidget(label)
|
||||
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.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)
|
||||
|
||||
horz.addWidget(label)
|
||||
label.setBuddy(self.fileform)
|
||||
horz.addWidget(self.fileform)
|
||||
self.l.addLayout(horz)
|
||||
self.mergehide.append(self.fileform)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel('If Story Already Exists?')
|
||||
horz.addWidget(label)
|
||||
self.collisionlabel = QLabel("CollisionLabel")
|
||||
horz.addWidget(self.collisionlabel)
|
||||
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.")
|
||||
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)
|
||||
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)
|
||||
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
button_box.accepted.connect(self.accept)
|
||||
button_box.rejected.connect(self.reject)
|
||||
self.l.addWidget(button_box)
|
||||
|
||||
if url_list_text:
|
||||
button_box.button(QDialogButtonBox.Ok).setFocus()
|
||||
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']))
|
||||
|
||||
if self.merge and not self.newmerge:
|
||||
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):
|
||||
prev=self.collision.currentText()
|
||||
self.collision.clear()
|
||||
for o in collision_order:
|
||||
if self.fileform.currentText() == 'epub' or o not in [UPDATE,UPDATEALWAYS]:
|
||||
if self.merge:
|
||||
order = anthology_collision_order
|
||||
else:
|
||||
order = collision_order
|
||||
for o in order:
|
||||
if self.merge or self.fileform.currentText() == 'epub' or o not in [UPDATE,UPDATEALWAYS]:
|
||||
self.collision.addItem(o)
|
||||
i = self.collision.findText(prev)
|
||||
if i > -1:
|
||||
self.collision.setCurrentIndex(i)
|
||||
|
||||
def get_ffdl_options(self):
|
||||
return {
|
||||
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:
|
||||
retval['updatemeta']=True
|
||||
retval['collision']=ADDNEW
|
||||
|
||||
return dict(retval.items() + self.extraoptions.items() )
|
||||
|
||||
def get_urlstext(self):
|
||||
return unicode(self.url.toPlainText())
|
||||
@@ -193,10 +384,10 @@ class CollectURLDialog(SizePersistedDialog):
|
||||
'''
|
||||
Collect single url for get urls.
|
||||
'''
|
||||
def __init__(self, gui, title, url_text):
|
||||
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
|
||||
|
||||
self.setMinimumWidth(300)
|
||||
|
||||
@@ -204,28 +395,40 @@ class CollectURLDialog(SizePersistedDialog):
|
||||
self.setLayout(self.l)
|
||||
|
||||
self.setWindowTitle(title)
|
||||
self.l.addWidget(QLabel(title),0,0,1,2)
|
||||
self.l.addWidget(QLabel(title),0,0,1,3)
|
||||
|
||||
self.l.addWidget(QLabel("URL:"),1,0)
|
||||
self.url = QLineEdit(self)
|
||||
self.url.setText(url_text)
|
||||
self.l.addWidget(self.url,1,1)
|
||||
self.l.addWidget(self.url,1,1,1,2)
|
||||
|
||||
self.ok_button = QPushButton('OK', self)
|
||||
self.ok_button.clicked.connect(self.ok)
|
||||
self.l.addWidget(self.ok_button,2,0)
|
||||
self.indiv_button = QPushButton('For Individual Books', self)
|
||||
self.indiv_button.setToolTip('Get URLs and go to dialog for individual story downloads.')
|
||||
self.indiv_button.clicked.connect(self.indiv)
|
||||
self.l.addWidget(self.indiv_button,2,0)
|
||||
|
||||
self.merge_button = QPushButton('For Anthology Epub', self)
|
||||
self.merge_button.setToolTip('Get URLs and go to dialog for Anthology download.\nRequires EpubMerge 1.3.1+ plugin.')
|
||||
self.merge_button.clicked.connect(self.merge)
|
||||
self.l.addWidget(self.merge_button,2,1)
|
||||
self.merge_button.setEnabled(epubmerge_plugin!=None)
|
||||
|
||||
self.cancel_button = QPushButton('Cancel', self)
|
||||
self.cancel_button.clicked.connect(self.cancel)
|
||||
self.l.addWidget(self.cancel_button,2,1)
|
||||
self.l.addWidget(self.cancel_button,2,2)
|
||||
|
||||
# restore saved size.
|
||||
self.resize_dialog()
|
||||
|
||||
def ok(self):
|
||||
def indiv(self):
|
||||
self.status=True
|
||||
self.accept()
|
||||
|
||||
def merge(self):
|
||||
self.status=True
|
||||
self.anthology=True
|
||||
self.accept()
|
||||
|
||||
def cancel(self):
|
||||
self.status=False
|
||||
self.reject()
|
||||
@@ -236,7 +439,6 @@ class UserPassDialog(QDialog):
|
||||
'''
|
||||
def __init__(self, gui, site, exception=None):
|
||||
QDialog.__init__(self, gui)
|
||||
self.gui = gui
|
||||
self.status=False
|
||||
|
||||
self.l = QGridLayout()
|
||||
@@ -295,7 +497,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
|
||||
@@ -344,7 +545,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)
|
||||
|
||||
@@ -393,7 +593,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)
|
||||
@@ -411,11 +610,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)
|
||||
@@ -425,11 +620,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()
|
||||
|
||||
@@ -455,7 +645,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)
|
||||
|
||||
@@ -505,84 +694,6 @@ class UpdateExistingDialog(SizePersistedDialog):
|
||||
'updateepubcover': self.updateepubcover.isChecked(),
|
||||
}
|
||||
|
||||
def display_story_list(gui, header, prefs, icon, books,
|
||||
label_text='',
|
||||
save_size_name='fanfictiondownloader_plugin:display list dialog',
|
||||
offer_skip=False):
|
||||
all_good = True
|
||||
for b in books:
|
||||
if not b['good']:
|
||||
all_good=False
|
||||
break
|
||||
|
||||
##
|
||||
if all_good and not dynamic.get(confirm_config_name(save_size_name), True):
|
||||
return True
|
||||
pass
|
||||
## fake accept?
|
||||
d = DisplayStoryListDialog(gui, header, prefs, icon, books,
|
||||
label_text,
|
||||
save_size_name,
|
||||
offer_skip and all_good)
|
||||
d.exec_()
|
||||
return d.result() == d.Accepted
|
||||
|
||||
class DisplayStoryListDialog(SizePersistedDialog):
|
||||
def __init__(self, gui, header, prefs, icon, books,
|
||||
label_text='',
|
||||
save_size_name='fanfictiondownloader_plugin:display list dialog',
|
||||
offer_skip=False):
|
||||
SizePersistedDialog.__init__(self, gui, save_size_name)
|
||||
self.name = save_size_name
|
||||
self.gui = gui
|
||||
|
||||
self.setWindowTitle(header)
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
self.setLayout(layout)
|
||||
title_layout = ImageTitleLayout(self, 'images/icon.png',
|
||||
header)
|
||||
layout.addLayout(title_layout)
|
||||
|
||||
self.books_table = StoryListTableWidget(self)
|
||||
layout.addWidget(self.books_table)
|
||||
|
||||
options_layout = QHBoxLayout()
|
||||
self.label = QLabel(label_text)
|
||||
#self.label.setOpenExternalLinks(True)
|
||||
#self.label.setWordWrap(True)
|
||||
options_layout.addWidget(self.label)
|
||||
|
||||
if offer_skip:
|
||||
spacerItem1 = QtGui.QSpacerItem(2, 4, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
|
||||
options_layout.addItem(spacerItem1)
|
||||
self.again = QCheckBox('Show this again?',self)
|
||||
self.again.setChecked(True)
|
||||
self.again.stateChanged.connect(self.toggle)
|
||||
self.again.setToolTip('Uncheck to skip review and update stories immediately when no problems.')
|
||||
options_layout.addWidget(self.again)
|
||||
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
button_box.accepted.connect(self.accept)
|
||||
button_box.rejected.connect(self.reject)
|
||||
|
||||
options_layout.addWidget(button_box)
|
||||
|
||||
layout.addLayout(options_layout)
|
||||
|
||||
# Cause our dialog size to be restored from prefs or created on first usage
|
||||
self.resize_dialog()
|
||||
self.books_table.populate_table(books)
|
||||
|
||||
def get_books(self):
|
||||
return self.books_table.get_books()
|
||||
|
||||
def toggle(self, *args):
|
||||
dynamic[confirm_config_name(self.name)] = self.again.isChecked()
|
||||
|
||||
|
||||
|
||||
class StoryListTableWidget(QTableWidget):
|
||||
|
||||
def __init__(self, parent):
|
||||
@@ -642,11 +753,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):
|
||||
@@ -680,56 +789,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=[]):
|
||||
@@ -737,85 +796,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()
|
||||
@@ -838,57 +865,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=[],
|
||||
@@ -898,7 +874,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))
|
||||
@@ -918,21 +893,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)
|
||||
|
||||
@@ -979,10 +946,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()
|
||||
|
||||
+802
-475
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2013, Jim Miller'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from StringIO import StringIO
|
||||
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters, exceptions
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.configurable import Configuration
|
||||
from calibre_plugins.fanfictiondownloader_plugin.prefs import (prefs)
|
||||
|
||||
def get_ffdl_personalini():
|
||||
if prefs['includeimages']:
|
||||
# this is a cheat to make it easier for users.
|
||||
return '''[epub]
|
||||
include_images:true
|
||||
keep_summary_html:true
|
||||
make_firstimage_cover:true
|
||||
''' + prefs['personal.ini']
|
||||
else:
|
||||
return prefs['personal.ini']
|
||||
|
||||
def get_ffdl_config(url,fileform="epub",personalini=None):
|
||||
if not personalini:
|
||||
personalini = get_ffdl_personalini()
|
||||
site='unknown'
|
||||
try:
|
||||
site = adapters.getConfigSectionFor(url)
|
||||
except Exception as e:
|
||||
print("Failed trying to get ini config for url(%s): %s, using section [%s] instead"%(url,e,site))
|
||||
configuration = Configuration(site,fileform)
|
||||
configuration.readfp(StringIO(get_resources("plugin-defaults.ini")))
|
||||
configuration.readfp(StringIO(personalini))
|
||||
|
||||
return configuration
|
||||
|
||||
def get_ffdl_adapter(url,fileform="epub",personalini=None):
|
||||
return adapters.getAdapter(get_ffdl_config(url,fileform,personalini),url)
|
||||
|
||||
+31
-4
@@ -18,9 +18,9 @@ from calibre.utils.ipc.job import ParallelJob
|
||||
from calibre_plugins.fanfictiondownloader_plugin.dialogs import (NotGoingToDownload,
|
||||
OVERWRITE, OVERWRITEALWAYS, UPDATE, UPDATEALWAYS, ADDNEW, SKIP, CALIBREONLY)
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters, writers, exceptions
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.configurable import Configuration
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.epubutils import get_update_data
|
||||
|
||||
from calibre_plugins.fanfictiondownloader_plugin.ffdl_util import (get_ffdl_adapter, get_ffdl_config)
|
||||
# ------------------------------------------------------------------------------
|
||||
#
|
||||
# Functions to perform downloads using worker jobs
|
||||
@@ -114,9 +114,9 @@ def do_download_for_worker(book,options):
|
||||
try:
|
||||
book['comment'] = 'Download started...'
|
||||
|
||||
configuration = Configuration(adapters.getConfigSectionFor(book['url']),options['fileform'])
|
||||
configuration.readfp(StringIO(get_resources("plugin-defaults.ini")))
|
||||
configuration.readfp(StringIO(options['personal.ini']))
|
||||
configuration = get_ffdl_config(book['url'],
|
||||
options['fileform'],
|
||||
options['personal.ini'])
|
||||
|
||||
if not options['updateepubcover'] and 'epub_for_update' in book and options['collision'] in (UPDATE, UPDATEALWAYS):
|
||||
configuration.set("overrides","never_make_cover","true")
|
||||
@@ -153,6 +153,22 @@ def do_download_for_worker(book,options):
|
||||
elif options['collision'] in (ADDNEW, SKIP, OVERWRITE, OVERWRITEALWAYS) or \
|
||||
('epub_for_update' not in book and options['collision'] in (UPDATE, UPDATEALWAYS)):
|
||||
|
||||
# preserve logfile even on overwrite.
|
||||
if 'epub_for_update' in book:
|
||||
(urlignore,
|
||||
chaptercountignore,
|
||||
oldchaptersignore,
|
||||
oldimgsignore,
|
||||
oldcoverignore,
|
||||
calibrebookmarkignore,
|
||||
# only logfile set in adapter, so others aren't used.
|
||||
adapter.logfile) = get_update_data(book['epub_for_update'])
|
||||
|
||||
# change the existing entries id to notid so
|
||||
# write_epub writes a whole new set to indicate overwrite.
|
||||
if adapter.logfile:
|
||||
adapter.logfile = adapter.logfile.replace("span id","span notid")
|
||||
|
||||
print("write to %s"%outfile)
|
||||
writer.writeStory(outfilename=outfile, forceOverwrite=True)
|
||||
book['comment'] = 'Download %s completed, %s chapters.'%(options['fileform'],story.getMetadata("numChapters"))
|
||||
@@ -171,6 +187,17 @@ def do_download_for_worker(book,options):
|
||||
adapter.calibrebookmark,
|
||||
adapter.logfile) = get_update_data(book['epub_for_update'])
|
||||
|
||||
# dup handling from ffdl_plugin needed for anthology updates.
|
||||
if options['collision'] == UPDATE:
|
||||
if chaptercount == urlchaptercount:
|
||||
book['comment']="Already contains %d chapters. Reuse as is."%chaptercount
|
||||
book['outfile'] = book['epub_for_update'] # for anthology merge ops.
|
||||
return book
|
||||
|
||||
# dup handling from ffdl_plugin needed for anthology updates.
|
||||
if chaptercount > urlchaptercount:
|
||||
raise NotGoingToDownload("Existing epub contains %d chapters, web site only has %d. Use Overwrite to force update." % (chaptercount,urlchaptercount),'dialog_error.png')
|
||||
|
||||
print("Do update - epub(%d) vs url(%d)" % (chaptercount, urlchaptercount))
|
||||
print("write to %s"%outfile)
|
||||
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2013, Jim Miller'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import copy
|
||||
|
||||
from calibre.utils.config import JSONConfig
|
||||
from calibre.gui2.ui import get_gui
|
||||
|
||||
from calibre_plugins.fanfictiondownloader_plugin.dialogs import OVERWRITE
|
||||
from calibre_plugins.fanfictiondownloader_plugin.common_utils import get_library_uuid
|
||||
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['rejecturls'] = ''
|
||||
default_prefs['rejectreasons'] = '''Sucked
|
||||
Boring
|
||||
Dup from another site'''
|
||||
|
||||
default_prefs['updatemeta'] = True
|
||||
default_prefs['updatecover'] = False
|
||||
default_prefs['updateepubcover'] = False
|
||||
default_prefs['keeptags'] = False
|
||||
default_prefs['showmarked'] = 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['checkforseriesurlid'] = True
|
||||
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['allow_custcol_from_ini'] = True
|
||||
|
||||
default_prefs['std_cols_newonly'] = {}
|
||||
|
||||
def set_library_config(library_config,db):
|
||||
db.prefs.set_namespaced(PREFS_NAMESPACE,
|
||||
PREFS_KEY_SETTINGS,
|
||||
library_config)
|
||||
|
||||
def get_library_config(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
|
||||
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 _get_db(self):
|
||||
if self.passed_db:
|
||||
return self.passed_db
|
||||
else:
|
||||
# In the GUI plugin we want current db so we detect when
|
||||
# it's changed. CLI plugin calls need to pass db in.
|
||||
return get_gui().current_db
|
||||
|
||||
def __init__(self,passed_db=None):
|
||||
self.default_prefs = default_prefs
|
||||
self.libraryid = None
|
||||
self.current_prefs = None
|
||||
self.passed_db=passed_db
|
||||
|
||||
def _get_prefs(self):
|
||||
libraryid = get_library_uuid(self._get_db())
|
||||
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(self._get_db())
|
||||
return self.current_prefs
|
||||
|
||||
def __getitem__(self,k):
|
||||
prefs = self._get_prefs()
|
||||
if k not in prefs:
|
||||
# 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)
|
||||
|
||||
def __delitem__(self,k):
|
||||
prefs = self._get_prefs()
|
||||
if k in prefs:
|
||||
del prefs[k]
|
||||
|
||||
def save_to_db(self):
|
||||
set_library_config(self._get_prefs(),self._get_db())
|
||||
|
||||
prefs = PrefsFacade()
|
||||
|
||||
+18
-8
@@ -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
|
||||
@@ -369,7 +372,7 @@ output_css:
|
||||
## It can be either a 'file:' or 'http:' url.
|
||||
## Note that if you enable make_firstimage_cover in [epub], but want
|
||||
## to use default_cover_image for a specific site, use the site:format
|
||||
## section, for example: [www.ficwad.com:epub]
|
||||
## section, for example: [ficwad.com:epub]
|
||||
## default_cover_image is a python string Template string with
|
||||
## ${title}, ${author} etc, same as titlepage_entries. Unless
|
||||
## allow_unsafe_filename is true, invalid filename chars 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
|
||||
@@ -1031,7 +1041,7 @@ extratags:
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:reviews,favs,follows
|
||||
|
||||
[www.ficwad.com]
|
||||
[ficwad.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
@@ -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
|
||||
|
||||
+34
-16
@@ -32,10 +32,17 @@ if sys.version_info >= (2, 7):
|
||||
loghandler.setFormatter(logging.Formatter("(=====)(levelname)s:%(message)s"))
|
||||
rootlogger.addHandler(loghandler)
|
||||
|
||||
from fanficdownloader import adapters,writers,exceptions
|
||||
from fanficdownloader.configurable import Configuration
|
||||
from fanficdownloader.epubutils import get_dcsource_chaptercount, get_update_data
|
||||
from fanficdownloader.geturls import get_urls_from_page
|
||||
try:
|
||||
from fanficdownloader import adapters,writers,exceptions
|
||||
from fanficdownloader.configurable import Configuration
|
||||
from fanficdownloader.epubutils import get_dcsource_chaptercount, get_update_data
|
||||
from fanficdownloader.geturls import get_urls_from_page
|
||||
except:
|
||||
# running under calibre
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters,writers,exceptions
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.configurable import Configuration
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.epubutils import get_dcsource_chaptercount, get_update_data
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.geturls import get_urls_from_page
|
||||
|
||||
if sys.version_info < (2, 5):
|
||||
print "This program requires Python 2.5 or newer."
|
||||
@@ -48,15 +55,23 @@ def writeStory(config,adapter,writeformat,metaonly=False,outstream=None):
|
||||
del writer
|
||||
return output_filename
|
||||
|
||||
def main():
|
||||
def main(argv,
|
||||
parser=None,
|
||||
passed_defaultsini=None,
|
||||
passed_personalini=None):
|
||||
# read in args, anything starting with -- will be treated as --<varible>=<value>
|
||||
usage = "usage: %prog [options] storyurl"
|
||||
parser = OptionParser(usage)
|
||||
if not parser:
|
||||
parser = OptionParser("usage: %prog [options] storyurl")
|
||||
parser.add_option("-f", "--format", dest="format", default="epub",
|
||||
help="write story as FORMAT, epub(default), text or html", metavar="FORMAT")
|
||||
help="write story as FORMAT, epub(default), mobi, text or html", metavar="FORMAT")
|
||||
|
||||
if passed_defaultsini:
|
||||
config_help="read config from specified file(s) in addition to calibre plugin personal.ini, ~/.fanficdownloader/personal.ini, and ./personal.ini"
|
||||
else:
|
||||
config_help="read config from specified file(s) in addition to ~/.fanficdownloader/defaults.ini, ~/.fanficdownloader/personal.ini, ./defaults.ini, and ./personal.ini"
|
||||
parser.add_option("-c", "--config",
|
||||
action="append", dest="configfile", default=None,
|
||||
help="read config from specified file(s) in addition to ~/.fanficdownloader/defaults.ini, ~/.fanficdownloader/personal.ini, ./defaults.ini, ./personal.ini", metavar="CONFIG")
|
||||
help=config_help, metavar="CONFIG")
|
||||
parser.add_option("-b", "--begin", dest="begin", default=None,
|
||||
help="Begin with Chapter START", metavar="START")
|
||||
parser.add_option("-e", "--end", dest="end", default=None,
|
||||
@@ -69,13 +84,13 @@ def main():
|
||||
help="Retrieve metadata and stop. Or, if --update-epub, update metadata title page only.",)
|
||||
parser.add_option("-u", "--update-epub",
|
||||
action="store_true", dest="update",
|
||||
help="Update an existing epub with new chapter, give epub filename instead of storyurl.",)
|
||||
help="Update an existing epub with new chapters, give epub filename instead of storyurl.",)
|
||||
parser.add_option("--update-cover",
|
||||
action="store_true", dest="updatecover",
|
||||
help="Update cover in an existing epub, otherwise existing cover (if any) is used on update. Only valid with --update-epub.",)
|
||||
parser.add_option("--force",
|
||||
action="store_true", dest="force",
|
||||
help="Force overwrite or update of an existing epub, download and overwrite all chapters.",)
|
||||
help="Force overwrite of an existing epub, download and overwrite all chapters.",)
|
||||
parser.add_option("-l", "--list",
|
||||
action="store_true", dest="list",
|
||||
help="Get list of valid story URLs from page given.",)
|
||||
@@ -83,7 +98,7 @@ def main():
|
||||
action="store_true", dest="debug",
|
||||
help="Show debug output while downloading.",)
|
||||
|
||||
(options, args) = parser.parse_args()
|
||||
(options, args) = parser.parse_args(argv)
|
||||
|
||||
if not options.debug:
|
||||
logger = logging.getLogger("fanficdownloader")
|
||||
@@ -107,12 +122,18 @@ def main():
|
||||
|
||||
conflist = []
|
||||
homepath = join(expanduser("~"),".fanficdownloader")
|
||||
|
||||
if passed_defaultsini:
|
||||
configuration.readfp(passed_defaultsini)
|
||||
|
||||
if isfile(join(homepath,"defaults.ini")):
|
||||
conflist.append(join(homepath,"defaults.ini"))
|
||||
if isfile("defaults.ini"):
|
||||
conflist.append("defaults.ini")
|
||||
|
||||
if passed_personalini:
|
||||
configuration.readfp(passed_personalini)
|
||||
|
||||
if isfile(join(homepath,"personal.ini")):
|
||||
conflist.append(join(homepath,"personal.ini"))
|
||||
if isfile("personal.ini"):
|
||||
@@ -155,9 +176,7 @@ def main():
|
||||
return
|
||||
|
||||
try:
|
||||
|
||||
adapter = adapters.getAdapter(configuration,url)
|
||||
|
||||
adapter.setChaptersRange(options.begin,options.end)
|
||||
|
||||
## Check for include_images and absence of PIL, give warning.
|
||||
@@ -173,7 +192,6 @@ def main():
|
||||
print "You have include_images enabled, but Python Image Library(PIL) isn't found.\nImages will be included full size in original format.\nContinue? (y/n)?"
|
||||
if not sys.stdin.readline().strip().lower().startswith('y'):
|
||||
return
|
||||
|
||||
|
||||
## three tries, that's enough if both user/pass & is_adult needed,
|
||||
## or a couple tries of one or the other
|
||||
@@ -243,5 +261,5 @@ def main():
|
||||
if __name__ == "__main__":
|
||||
#import time
|
||||
#start = time.time()
|
||||
main()
|
||||
main(sys.argv[1:])
|
||||
#print("Total time seconds:%f"%(time.time()-start))
|
||||
|
||||
@@ -151,9 +151,9 @@ getNormalStoryURL.__dummyconfig = None
|
||||
|
||||
def getAdapter(config,url):
|
||||
|
||||
logger.debug("trying url:"+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
|
||||
@@ -187,11 +187,11 @@ def getClassFor(url):
|
||||
cls = getClassFromList(domain)
|
||||
if not cls and domain.startswith("www."):
|
||||
domain = domain.replace("www.","")
|
||||
logger.debug("trying site:without www: "+domain)
|
||||
#logger.debug("trying site:without www: "+domain)
|
||||
cls = getClassFromList(domain)
|
||||
fixedurl = fixedurl.replace("http://www.","http://")
|
||||
if not cls:
|
||||
logger.debug("trying site:www."+domain)
|
||||
#logger.debug("trying site:www."+domain)
|
||||
cls = getClassFromList("www."+domain)
|
||||
fixedurl = fixedurl.replace("http://","http://www.")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -68,7 +68,6 @@ class ArchiveSkyeHawkeComAdapter(BaseSiteAdapter):
|
||||
|
||||
@classmethod
|
||||
def getAcceptDomains(cls):
|
||||
# mobile.fimifction.com isn't actually a valid domain, but we can still get the story id from URLs anyway
|
||||
return ['archive.skyehawke.com','www.skyehawke.com']
|
||||
|
||||
def getSiteExampleURLs(self):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -43,10 +43,10 @@ class FicwadComSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return 'www.ficwad.com'
|
||||
return 'ficwad.com'
|
||||
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://www.ficwad.com/story/137169"
|
||||
return "http://ficwad.com/story/137169"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape(r"http://"+self.getSiteDomain())+"/story/\d+?$"
|
||||
|
||||
@@ -28,8 +28,6 @@ from .. import BeautifulSoup as bs
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from ..bbcodeutils.bbcodeparser import bbcodeparser
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
def getClass():
|
||||
@@ -181,10 +179,11 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
self.setCoverImage(self.url,coverurl)
|
||||
|
||||
# the fimfic API gives bbcode for desc, not html.
|
||||
# btw, bbcode honors newlines, html doesn't. change newlines to br tags.
|
||||
self.setDescription(self.url,
|
||||
bbcodeparser().parse(storyMetadata["description"]).html(doDeepCopy=False).replace('\r','').replace('\n','<br />'))
|
||||
# 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
|
||||
@@ -193,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.
|
||||
@@ -219,7 +218,7 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
def getChapterText(self, url):
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(url),selfClosingTags=('br','hr')).find('div', {'id' : 'chapter_container'})
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(url),selfClosingTags=('br','hr')).find('div', {'class' : 'chapter_content'})
|
||||
if soup == None:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
return self.utf8FromSoup(url,soup)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -115,6 +117,10 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
|
||||
|
||||
self.story.addToList('authorUrl','http://author/url')
|
||||
self.story.addToList('authorUrl','http://author/url-2')
|
||||
self.story.addToList('category','Power Rangers')
|
||||
self.story.addToList('category','SG-1')
|
||||
self.story.addToList('genre','Porn')
|
||||
self.story.addToList('genre','Drama')
|
||||
else:
|
||||
self.story.setMetadata('authorId','98765')
|
||||
self.story.setMetadata('authorUrl','http://author/url')
|
||||
@@ -122,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')
|
||||
@@ -162,9 +183,9 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
|
||||
('Chapter 3, Over Cinnabar',self.url+"&chapter=4"),
|
||||
('Chapter 4',self.url+"&chapter=5"),
|
||||
('Chapter 5',self.url+"&chapter=6"),
|
||||
#('Chapter 6',self.url+"&chapter=7"),
|
||||
#('Chapter 7',self.url+"&chapter=8"),
|
||||
#('Chapter 8',self.url+"&chapter=9"),
|
||||
('Chapter 6',self.url+"&chapter=7"),
|
||||
('Chapter 7',self.url+"&chapter=8"),
|
||||
('Chapter 8',self.url+"&chapter=9"),
|
||||
#('Chapter 9',self.url+"&chapter=0"),
|
||||
#('Chapter 0',self.url+"&chapter=a"),
|
||||
#('Chapter a',self.url+"&chapter=b"),
|
||||
@@ -187,9 +208,6 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
|
||||
|
||||
def getChapterText(self, url):
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
if self.story.getMetadata('storyId') == '667':
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s!" % url)
|
||||
|
||||
if self.story.getMetadata('storyId').startswith('670') or \
|
||||
self.story.getMetadata('storyId').startswith('672'):
|
||||
time.sleep(1.0)
|
||||
@@ -202,7 +220,7 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
|
||||
<p>http://test1.com?sid=664 - Crazy string title</p>
|
||||
<p>http://test1.com?sid=665 - raises AdultCheckRequired</p>
|
||||
<p>http://test1.com?sid=666 - raises StoryDoesNotExist</p>
|
||||
<p>http://test1.com?sid=667 - raises FailedToDownload on chapter 1</p>
|
||||
<p>http://test1.com?sid=667 - raises FailedToDownload on chapters 2+</p>
|
||||
<p>http://test1.com?sid=668 - raises FailedToLogin unless username='Me'</p>
|
||||
<p>http://test1.com?sid=669 - Succeeds with Updated Date=now</p>
|
||||
<p>http://test1.com?sid=670 - Succeeds, but sleeps 2sec on each chapter</p>
|
||||
@@ -211,7 +229,10 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
|
||||
|
||||
|
||||
<p>http://test1.com?sid=671 - Succeeds, but sleeps 2sec metadata only</p>
|
||||
<p>http://test1.com?sid=672 - Succeeds, quick meta, sleeps 2sec chapters only</p><p>http://test1.com?sid=0 - Succeeds, generates some text specifically for testing hyphenation problems with Nook STR/STRwG</p><p>Odd sid's will be In-Progress, evens complete. sid<10 will be assigned one of four languages and included in a series.</p>
|
||||
<p>http://test1.com?sid=672 - Succeeds, quick meta, sleeps 2sec chapters only</p>
|
||||
<p>http://test1.com?sid=673 - Succeeds, multiple authors, extra categories, genres</p>
|
||||
<p>http://test1.com?sid=0 - Succeeds, generates some text specifically for testing hyphenation problems with Nook STR/STRwG</p>
|
||||
<p>Odd sid's will be In-Progress, evens complete. sid<10 will be assigned one of four languages and included in a series.</p>
|
||||
</div>
|
||||
'''
|
||||
elif self.story.getMetadata('storyId') == '0':
|
||||
@@ -226,9 +247,13 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
|
||||
<br />
|
||||
'''
|
||||
else:
|
||||
if self.story.getMetadata('storyId') == '667':
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s!" % url)
|
||||
|
||||
text=u'''
|
||||
<div>
|
||||
<h3>Chapter title from site</h3>
|
||||
<p>Timestamp:'''+datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")+'''</p>
|
||||
<p>Lorem '''+self.crazystring+u''' <i>italics</i>, <b>bold</b>, <u>underline</u> consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
br breaks<br><br>
|
||||
Puella Magi Madoka Magica/魔法少女まどか★マギカ
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -100,8 +100,8 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
if "Stories Published" not in d : #Member Account
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
params['urealname']))
|
||||
raise exceptions.FailedToLogin(self.url,params['urealname'])
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
@@ -255,9 +255,7 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
|
||||
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
|
||||
|
||||
|
||||
@@ -196,6 +196,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
|
||||
|
||||
|
||||
@@ -1,325 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
#
|
||||
# Author: Pau Sanchez (contact@pausanchez.com)
|
||||
# Version: v1.0
|
||||
# Last Modified: 2010/09/15
|
||||
#
|
||||
# For the latest version check out:
|
||||
# http://www.codigomanso.com/en/projects
|
||||
#
|
||||
# My blog:
|
||||
# http://www.codigomanso.com/en/ - English Version
|
||||
# http://www.codigomanso.com/es/ - Spanish Version
|
||||
#
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import urllib
|
||||
|
||||
class bbcode2html:
|
||||
'''
|
||||
This class gets a parsed BBCode and transforms it to valid HTML
|
||||
|
||||
Useful functions of this class:
|
||||
html
|
||||
convertToHTML
|
||||
|
||||
Example:
|
||||
> parser = bbcodeparser ()
|
||||
> parser.parse ('[b]bold[/b]')
|
||||
> bbcode2html (parser).html()
|
||||
<b>bold</b>
|
||||
|
||||
# This is faster for huge strings but changes the parser object internally
|
||||
> bbcode2html (parser).html(doDeepCopy = False)
|
||||
<b>bold</b>
|
||||
'''
|
||||
def __init__ (self, parser):
|
||||
self._parser = parser
|
||||
return
|
||||
|
||||
def html (self, allowClassAttr = False, doDeepCopy = True, parser = None):
|
||||
'''
|
||||
Convert current parsed code to HTML
|
||||
|
||||
Example:
|
||||
code = bbcodeparser ('[b]bold[/b]')
|
||||
code.html() -> '<b>bold</b>'
|
||||
'''
|
||||
if parser is None:
|
||||
parser = self._parser
|
||||
|
||||
tokens = parser
|
||||
if instanceof (parser, bbcodeparser):
|
||||
tokens = parser.getTokens()
|
||||
|
||||
return bbcode2html.convertToHTML (tokens, allowClassAttr = allowClassAttr, doDeepCopy = doDeepCopy)
|
||||
|
||||
@staticmethod
|
||||
def htmlString (string):
|
||||
toReplace = {
|
||||
u'<' : '<',
|
||||
u'>' : '>',
|
||||
u'"' : """,
|
||||
u'&' : "&"
|
||||
}
|
||||
for entity in toReplace:
|
||||
string = string.replace(entity, toReplace[entity])
|
||||
return string
|
||||
|
||||
@staticmethod
|
||||
def getValidTags ():
|
||||
simpleTags = ['b', 'u', 'i', 'sup', 'sub', 'ul', 'ol', 'li', 'table', 'tr', 'th', 'td', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']
|
||||
validTags = {
|
||||
'p' : { 'color' : 'color', 'size' : 'size', 'font' : 'font' },
|
||||
'color' : { 'color' : 'color' },
|
||||
'size' : { 'size' : 'size' },
|
||||
'font' : { 'font' : 'font' },
|
||||
'img' : { 'alt' : 'alt', 'title' : 'title', 'width' : 'width' , 'height' : 'height', 'img' : 'img'},
|
||||
'url' : { 'href' : 'href', 'url' : 'href', 'link' : 'href', 'title' : 'title' },
|
||||
's' : { },
|
||||
'code' : { },
|
||||
'quote' : { },
|
||||
'list' : { 'list' : 'type' },
|
||||
'email' : { 'email': 'href'},
|
||||
'google' : { 'google': 'google'},
|
||||
'wikipedia' : { 'wikipedia' : 'wikipedia', 'language' : 'language', 'lang' : 'lang'}
|
||||
}
|
||||
|
||||
for tag in simpleTags:
|
||||
validTags[tag] = { }
|
||||
return validTags
|
||||
|
||||
@staticmethod
|
||||
def convertToHTML (tokens, allowClassAttr = False, validTags = None, doDeepCopy = True):
|
||||
'''
|
||||
Convert internally parsed BBCode to XHTML
|
||||
|
||||
@doDeepCopy
|
||||
True: it does a deep copy of tokens so this list will remain unchanged
|
||||
False: tokens will be modified internally, but the output will be produced like 5x faster
|
||||
it's a good idea to use False only when this is the last operation
|
||||
'''
|
||||
# do a deep copy
|
||||
if doDeepCopy:
|
||||
import copy
|
||||
tokens = copy.deepcopy (tokens)
|
||||
|
||||
# filter invalid tags and attributes
|
||||
if validTags is None:
|
||||
validTags = bbcode2html.getValidTags()
|
||||
|
||||
bbcode2html._filterInvalidTagsAndAttributes (tokens, validTags, allowClassAttr)
|
||||
|
||||
# Start to convert
|
||||
index = 0
|
||||
tokenLength = len (tokens)
|
||||
|
||||
# use a list for the output (an order of magnitude faster than using string concatenation)
|
||||
htmlList = []
|
||||
lastListOpener = []
|
||||
|
||||
while index < tokenLength:
|
||||
|
||||
if isinstance (tokens [index], basestring):
|
||||
htmlList.append (bbcode2html.htmlString (tokens [index]))
|
||||
index += 1
|
||||
continue
|
||||
|
||||
token = tokens[index]
|
||||
tag = token['tag'] # opening or closing simple tag. e.g: 'b', '/b', '/u', ...
|
||||
tagName = (tag[1:] if tag[0] == '/' else tag)
|
||||
tagOpener = (u'/' if tag[0] == '/' else u'')
|
||||
tokenArgs = (token['args'] if 'args' in token else {})
|
||||
|
||||
# opening or closing simple tag COLOR / SIZE
|
||||
if (tagName in ['p', 'color', 'size', 'font']):
|
||||
style = ''
|
||||
style += ((u' color: ' + tokenArgs['color'] + u';') if ('color' in tokenArgs) else '')
|
||||
style += ((u' font-size: ' + tokenArgs['size'] + u'pt;') if ('size' in tokenArgs) else '')
|
||||
style += ((u' font-family: ' + tokenArgs['font'] + u';') if ('font' in tokenArgs) else '')
|
||||
style = style.strip()
|
||||
|
||||
pArgs = {}
|
||||
if style != '':
|
||||
pArgs ['style'] = style
|
||||
|
||||
if 'class' in tokenArgs:
|
||||
pArgs ['class'] = tokenArgs['class']
|
||||
|
||||
if ('args' not in token) and (tagName != 'p'):
|
||||
if (tagOpener == '/'): # if closing tag, close it
|
||||
htmlList.append (u'</span>')
|
||||
index += 1
|
||||
continue
|
||||
|
||||
if tagName != 'p':
|
||||
tag = tagOpener + u'span'
|
||||
|
||||
htmlList.append (bbcode2html.xml (tag, pArgs))
|
||||
|
||||
# IMG tag
|
||||
elif tag == 'img' and (index+2 < tokenLength):
|
||||
if 'img' in tokenArgs:
|
||||
# has the form of <width>x<height> ?
|
||||
sizeMatch = re.match (u'^\s*(\d+)[xX](\d+)\s*$', tokenArgs['img'])
|
||||
if sizeMatch is not None:
|
||||
tokenArgs['width'] = sizeMatch.group(1)
|
||||
tokenArgs['height'] = sizeMatch.group(2)
|
||||
# then assume is the alternative text
|
||||
else:
|
||||
tokenArgs['alt'] = tokenArgs['img']
|
||||
del tokenArgs['img']
|
||||
|
||||
# add the source of the image
|
||||
tokenArgs ['src'] = tokens[index+1]
|
||||
|
||||
# [img]http://www.whatever.com/pic.jpg[/img]
|
||||
htmlList.append (
|
||||
bbcode2html.xml ('img', tokenArgs, soloTag=True)
|
||||
)
|
||||
index += 2 # skip next token and closing tag
|
||||
|
||||
# URL tag
|
||||
elif tag == 'url':
|
||||
if ('args' not in token) and (index+2 < tokenLength):
|
||||
# [url]http://www.google.com[/url]
|
||||
htmlList.append (bbcode2html.xml ('a', { 'href' : tokens[index+1] }))
|
||||
else:
|
||||
# [url=http://www.google.com]Google[/url]
|
||||
# [url link=http://www.google.com title="This is Google"]Google[/url]
|
||||
htmlList.append (bbcode2html.xml ('a', tokenArgs))
|
||||
|
||||
# URL closing tag (sometimes needed)
|
||||
elif (tag == '/url') or (tag == '/email'):
|
||||
htmlList.append (u'</a>')
|
||||
|
||||
# Email tag
|
||||
elif tag == 'email':
|
||||
if ('args' not in token) and (index+2 < tokenLength):
|
||||
# [email]asdf@asdf.com]
|
||||
htmlList.append (bbcode2html.xml ('a', { 'href' : u'mailto:' + tokens[index+1].strip() }))
|
||||
else:
|
||||
# [email=asdf@asfd.com]john smith[/email]
|
||||
if 'href' in tokenArgs:
|
||||
tokenArgs['href'] = u'mailto:' + tokenArgs['href']
|
||||
htmlList.append (bbcode2html.xml ('a', tokenArgs))
|
||||
|
||||
elif tagName == 'list':
|
||||
if tagOpener == '/':
|
||||
htmlList.append (bbcode2html.xml (u'/' + lastListOpener.pop()))
|
||||
else:
|
||||
if ('type' not in tokenArgs):
|
||||
htmlList.append (bbcode2html.xml (tagOpener + u'ul', tokenArgs))
|
||||
lastListOpener.append ('ul')
|
||||
else:
|
||||
htmlList.append (bbcode2html.xml (tagOpener + u'ol', tokenArgs))
|
||||
lastListOpener.append ('ol')
|
||||
|
||||
elif tagName == '*':
|
||||
htmlList.append (bbcode2html.xml (tagOpener + u'li', tokenArgs))
|
||||
|
||||
elif (tagName == 's'):
|
||||
tokenArgs['style'] = 'text-decoration: line-through;'
|
||||
htmlList.append (bbcode2html.xml (tagOpener + u'span', tokenArgs))
|
||||
|
||||
elif (tagName == 'code'):
|
||||
htmlList.append (bbcode2html.xml (tagOpener + u'pre', tokenArgs))
|
||||
|
||||
elif (tagName == 'quote'):
|
||||
htmlList.append (bbcode2html.xml (tagOpener + u'blockquote', tokenArgs))
|
||||
|
||||
elif (tagName == 'google'):
|
||||
htmlList.append (
|
||||
bbcode2html.xml (
|
||||
tagOpener + u'a',
|
||||
{'href' : 'http://www.google.com/search?q=' + urllib.quote_plus (tokens[index+1])},
|
||||
tokens[index+1]
|
||||
)
|
||||
)
|
||||
index += 2
|
||||
|
||||
elif (tagName == 'wikipedia'):
|
||||
subdomain = 'www'
|
||||
for arg in ['lang', 'language', 'wikipedia']:
|
||||
if arg in tokenArgs:
|
||||
subdomain = tokenArgs[arg]
|
||||
|
||||
htmlList.append (
|
||||
bbcode2html.xml (
|
||||
tagOpener + u'a',
|
||||
{'href' : 'http://' + subdomain + '.wikipedia.org/wiki/' + tokens[index+1].replace (' ', '_')},
|
||||
tokens[index+1]
|
||||
)
|
||||
)
|
||||
index += 2
|
||||
|
||||
elif (tagName in validTags):
|
||||
htmlList.append (
|
||||
bbcode2html.xml (tag, tokenArgs)
|
||||
)
|
||||
|
||||
else:
|
||||
# ignore this tag
|
||||
pass
|
||||
|
||||
index += 1
|
||||
|
||||
return ''.join (htmlList)
|
||||
|
||||
@staticmethod
|
||||
def _filterInvalidTagsAndAttributes (tokens, validTags, allowClassAttr):
|
||||
'''
|
||||
Helper function to filter out invalid attributes from the tokens list
|
||||
'''
|
||||
# add 'class' attribute as valid (mapping 'class' itself)
|
||||
if allowClassAttr:
|
||||
for attr in validTags:
|
||||
validTags[attr]['class'] = 'class'
|
||||
|
||||
# remove invalid attributes from tokens
|
||||
for tindex in range(0, len(tokens)):
|
||||
if isinstance (tokens[tindex], dict) and ('args' in tokens[tindex]) and (tokens[tindex]['tag'] in validTags):
|
||||
validList = validTags[tokens[tindex]['tag']]
|
||||
|
||||
filteredArgs = {}
|
||||
for arg in tokens[tindex]['args']:
|
||||
if arg in validList:
|
||||
# rename the argument
|
||||
filteredArgs[validList[arg]] = tokens[tindex]['args'][arg]
|
||||
else:
|
||||
pass # do not include this arg in the filteredArgs
|
||||
|
||||
tokens[tindex]['args'] = filteredArgs
|
||||
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def xml (tag, attrs = {}, text = None, soloTag = False):
|
||||
'''
|
||||
Helper function to produce valid XML output
|
||||
'''
|
||||
xml = u'<' + tag.lower()
|
||||
|
||||
# make sure we sort attributes alphabetically (for deterministic output)
|
||||
# Faster but non-deterministic:
|
||||
# for (key, value) in attrs.iteritems():
|
||||
# xml += u' ' + key + u'="' + value + u'"'
|
||||
for key in sorted (attrs.keys()):
|
||||
xml += u' ' + key + u'="' + attrs[key] + u'"'
|
||||
|
||||
# close tag
|
||||
if text is None:
|
||||
if soloTag:
|
||||
xml += u' />'
|
||||
else:
|
||||
xml += u'>'
|
||||
else:
|
||||
xml += u'>' + text + u'</' + tag.lower() + '>'
|
||||
|
||||
return xml
|
||||
|
||||
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
#
|
||||
# Author: Pau Sanchez (contact@pausanchez.com)
|
||||
# Version: v1.0
|
||||
# Last Modified: 2010/09/15
|
||||
#
|
||||
# For the latest version check out:
|
||||
# http://www.codigomanso.com/en/projects
|
||||
#
|
||||
# My blog:
|
||||
# http://www.codigomanso.com/en/ - English Version
|
||||
# http://www.codigomanso.com/es/ - Spanish Version
|
||||
#
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import hashlib
|
||||
|
||||
class bbcodebuilder:
|
||||
'''
|
||||
This class helps to build BBCode programmatically.
|
||||
|
||||
The function names are used as the tag name, then the first parameter
|
||||
is the string that goes inside the tags and any extra parameter is
|
||||
appended as a parameter to the tag
|
||||
|
||||
Examples:
|
||||
> bbcode = bbcodebuilder() # create a instance!
|
||||
|
||||
> print bbcode.b ('bold')
|
||||
[b]bold[/b]
|
||||
|
||||
> print bbcode.color ('this goes in red', 'red')
|
||||
[color=red]this goes in red[/color]
|
||||
|
||||
> print bbcode.url ('Google', 'http://www.google.com')
|
||||
[url=http://www.google.com]Google[/url]
|
||||
|
||||
> print bbcode.alist('item 1', 'item 2')
|
||||
[list=a]
|
||||
[*]item 1
|
||||
[*]item 2
|
||||
[/list]
|
||||
|
||||
|
||||
This solution is based on the recipe found on:
|
||||
http://code.activestate.com/recipes/576831-simple-bbcode-support/
|
||||
'''
|
||||
|
||||
def __getattr__(self, name):
|
||||
'''
|
||||
This is a generic getter that returns a function which gets the first parameter
|
||||
as the string that goes between the tags, and extra parameters as tag parameters.
|
||||
|
||||
The name of the attribute is used as the tag name
|
||||
'''
|
||||
class bbcodebuilder_helper:
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
|
||||
def __call__(self, string, *args):
|
||||
return u'[{0}{1}]{2}[/{0}]'.format(self._name, (u'=' + u','.join(map(str, args))) if args else u'', string)
|
||||
|
||||
return bbcodebuilder_helper (name)
|
||||
|
||||
def list(self, *items):
|
||||
return u'[list]' + u''.join(map(lambda item: u"\n [*]" + item, items)) + u"\n[/list]"
|
||||
|
||||
def nlist(self, *items):
|
||||
return u'[list=1]' + u''.join(map(lambda item: u"\n [*]" + item, items)) + u"\n[/list]"
|
||||
|
||||
def alist(self, *items):
|
||||
return u'[list=a]' + u''.join(map(lambda item: u"\n [*]" + item, items)) + u"\n[/list]"
|
||||
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Author: Pau Sanchez (contact@pausanchez.com)
|
||||
# Version: v1.0
|
||||
# Last Modified: 2010/09/15
|
||||
#
|
||||
# For the latest version check out:
|
||||
# http://www.codigomanso.com/en/projects
|
||||
#
|
||||
# My blog:
|
||||
# http://www.codigomanso.com/en/ - English Version
|
||||
# http://www.codigomanso.com/es/ - Spanish Version
|
||||
#
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import hashlib
|
||||
|
||||
class bbcodeparser:
|
||||
'''
|
||||
This class parses BBCode into a internal structure to allow later processing and
|
||||
conversion to HTML.
|
||||
|
||||
The parser tries to fix invalid code (like unclosed tags)
|
||||
|
||||
Useful URLs:
|
||||
http://en.wikipedia.org/wiki/BBCode
|
||||
http://www.bbcode.org/reference.php
|
||||
|
||||
Example:
|
||||
> bbcode = bbcodeparser ()
|
||||
> bbcode.parse ('[b]text in bold[/b]').html()
|
||||
<b>text in bold</b>
|
||||
|
||||
# dump HTML
|
||||
> bbcode.parse ('[p][color=red]text in red').html()
|
||||
<p><span style="color:red;">text in red</span></p>
|
||||
|
||||
# dump fixed BBCode
|
||||
> bbcode.parse ('[p][color=red]text in red').bbcode()
|
||||
[p][color=red]text in red[/color][/p]
|
||||
|
||||
> bbcode.parse ('This [b][i]code[/b] will be fixed[/invalid]').bbcode()
|
||||
This [b][i]code[/i][/b] will be fixed
|
||||
|
||||
# dump fixed bbcode
|
||||
> str (bbcodeparse ('This [b][i]code[/b] will be fixed[/invalid]'))
|
||||
This [b][i]code[/i][/b] will be fixed
|
||||
'''
|
||||
_bbcode = ''
|
||||
_tokens = []
|
||||
|
||||
def __init__ (self, bbcode = '', fixInvalidCode = True):
|
||||
''' Initialize and parse bbcode string (if any is given)
|
||||
'''
|
||||
self.parse (bbcode, fixInvalidCode)
|
||||
return
|
||||
|
||||
def __str__ (self):
|
||||
return self.bbcode()
|
||||
|
||||
def parse (self, bbcode = None, fixInvalidCode = True):
|
||||
'''
|
||||
It will parse and return the token list, trying to fix tags if
|
||||
fixInvalidCode is True
|
||||
|
||||
It will return the current object to allow chaining
|
||||
|
||||
Example:
|
||||
code = bbcode()
|
||||
code.parse ('<b>bold</b>', True) ->
|
||||
code.parse ('<b>bold<i>italics</b>', True) -> internally will add the missing '</i>'
|
||||
'''
|
||||
if bbcode is not None:
|
||||
self._bbcode = bbcode
|
||||
self._tokens = self.tokenize (bbcode)
|
||||
if fixInvalidCode:
|
||||
self._tokens = self.fixWrongTags (self._tokens)
|
||||
|
||||
return self
|
||||
|
||||
# return ALL tokens
|
||||
def getTokens (self):
|
||||
return self._tokens
|
||||
|
||||
def bbcode (self):
|
||||
'''
|
||||
Dump BBCode again. This is useful for dumping valid BBCode
|
||||
'''
|
||||
bbcode = []
|
||||
for token in self._tokens:
|
||||
if token is None:
|
||||
continue
|
||||
|
||||
if isinstance (token, basestring):
|
||||
bbcode.append (token.replace (u'[', u'\[').replace (u']', u'\]'))
|
||||
continue
|
||||
|
||||
tag = token['tag'] # opening or closing simple tag. e.g: 'b', '/b', '/u', ...
|
||||
tagOpener = (u'/' if tag[0] == u'/' else u'')
|
||||
|
||||
if (tagOpener == '/') or ('args' not in token):
|
||||
bbcode.append (u'[' + tag + u']')
|
||||
else:
|
||||
# process args
|
||||
argstr = ''
|
||||
|
||||
# the arg with the same name as the tag repersents the '=whatever'
|
||||
if tag in token['args']:
|
||||
if re.match ('\s|"', token['args'][tag]) is None:
|
||||
argstr = u'=' + token['args'][tag]
|
||||
else:
|
||||
argstr = u'="' + token['args'][tag].replace (u'"', u'\"') + u'"'
|
||||
|
||||
for (k,v) in token['args'].iteritems():
|
||||
if k == tag: # already processed
|
||||
continue
|
||||
argstr += ' ' + k + u'="' + v.replace (u'"', u'\"') + u'"'
|
||||
|
||||
bbcode.append (u'[' + tag + argstr + ']')
|
||||
|
||||
return u''.join (bbcode)
|
||||
|
||||
def html (self, allowClassAttr = False, doDeepCopy = True):
|
||||
'''
|
||||
Convert current parsed code to HTML
|
||||
|
||||
@allowClassAttr
|
||||
Is something like [b class="asdf"] allowed?
|
||||
|
||||
@doDeepCopy
|
||||
True: it does a deep copy of tokens so this list will remain unchanged
|
||||
False: tokens will be modified internally, but the output will be produced like 5x faster
|
||||
it's a good idea to use False when the string parsed is huge and this is the
|
||||
last operation on the string
|
||||
|
||||
Example:
|
||||
code = bbcode ('[b]bold[/b]')
|
||||
code.html() -> '<b>bold</b>'
|
||||
'''
|
||||
from bbcode2html import bbcode2html
|
||||
return bbcode2html.convertToHTML (self._tokens, allowClassAttr = allowClassAttr, doDeepCopy = doDeepCopy)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def fixWrongTags (inTokenList):
|
||||
''' Add missing tokens that have not been closed properly and try to fix some scenarios
|
||||
'''
|
||||
opened = []
|
||||
outTokenList = []
|
||||
for token in inTokenList:
|
||||
# normal string... do nothing
|
||||
if isinstance(token, basestring):
|
||||
outTokenList.append (token)
|
||||
else:
|
||||
# if starts with '/' is closing a tag
|
||||
if token['tag'][0] == '/':
|
||||
while (len (opened) > 0) and (opened[-1] != token['tag'][1:]):
|
||||
outTokenList.append ({'tag' : '/' + opened[-1] })
|
||||
del opened[-1]
|
||||
|
||||
if len(opened):
|
||||
del opened[-1]
|
||||
outTokenList.append (token)
|
||||
|
||||
# opening tag
|
||||
else:
|
||||
# if I open the same tag I opened before, close it, and open it again
|
||||
if (len(opened) > 0) and (token['tag'] == opened[-1]):
|
||||
outTokenList.append ({'tag' : '/' + opened[-1] })
|
||||
else:
|
||||
opened.append (token['tag'])
|
||||
outTokenList.append (token)
|
||||
|
||||
# close all elements that have not been closed
|
||||
while len(opened):
|
||||
outTokenList.append ({'tag' : '/' + opened[-1] })
|
||||
del opened[-1]
|
||||
|
||||
return outTokenList
|
||||
|
||||
@staticmethod
|
||||
def tokenize(code):
|
||||
'''
|
||||
Tokenize BBCode tags and parameters
|
||||
|
||||
Return the token list using a internal format. See the example:
|
||||
[
|
||||
{ 'tag' : 'p', 'args' : { 'font' : 'arial' } },
|
||||
'This is ',
|
||||
{ 'tag' : 'url', 'args' : {'url' : 'http://www.google.com'} },
|
||||
'a link to google',
|
||||
{ 'tag' : '/url' },
|
||||
{ 'tag' : '/p' }
|
||||
]
|
||||
'''
|
||||
re_tags = re.compile (r'(\[[^]]+\])', re.DOTALL | re.UNICODE)
|
||||
re_tagName = re.compile (r'\[([^]=\s]+)([^]]*)\]', re.DOTALL | re.UNICODE)
|
||||
#re_tagArgs = re.compile (r'\s*([^=]*)=(("([^"]+)")|([^\s]+))', re.DOTALL | re.UNICODE)
|
||||
re_tagArgs = re.compile (r'\s*([\w]*)=(("([^"]+)")|([^\s]+))', re.DOTALL | re.UNICODE)
|
||||
|
||||
# get a unique name and replace escaped braces encode utf8 to
|
||||
# prevent CLI/Web from barfing on unicode chars. Not sure why
|
||||
# this even needs to be 'unique' like this, but that's the way
|
||||
# they wrote it.
|
||||
unique = hashlib.md5(code.encode('utf8')).hexdigest()
|
||||
code = code.replace ('\[', unique+'_OPEN_BRACE')
|
||||
code = code.replace ('\]', unique+'_CLOSE_BRACE')
|
||||
|
||||
splitted = re_tags.split(code)
|
||||
|
||||
outTokenList = []
|
||||
for token in splitted:
|
||||
if len(token) == 0:
|
||||
continue
|
||||
|
||||
if token[0] == '[':
|
||||
match = re_tagName.match (token)
|
||||
if match:
|
||||
tagName = match.group(1)
|
||||
tagArgs = match.group(2)
|
||||
|
||||
tagToken = { 'tag' : tagName.lower() }
|
||||
|
||||
# parse arguments (if any)
|
||||
if len(tagArgs) > 0:
|
||||
allArgs = re_tagArgs.findall(tagArgs)
|
||||
|
||||
tagArgs = {}
|
||||
for arg in allArgs:
|
||||
# if the argument has no name, use the tagName itself
|
||||
argName = (arg[0] if arg[0] != '' else tagName)
|
||||
argValue = (arg[3] if (arg[1][0] == '"') else arg[4])
|
||||
|
||||
tagArgs[argName.lower()] = argValue.replace ('\"', '"')
|
||||
|
||||
tagToken['args'] = tagArgs
|
||||
|
||||
outTokenList.append (tagToken)
|
||||
|
||||
# no match, append the text as it is
|
||||
else:
|
||||
outTokenList.append (token)
|
||||
# append the text as it is
|
||||
else:
|
||||
outTokenList.append (token)
|
||||
|
||||
# restore escaped braces back (once code is parsed)
|
||||
restoredTokenList = []
|
||||
for token in outTokenList:
|
||||
if isinstance (token, basestring):
|
||||
token = token.replace (unique+'_OPEN_BRACE', '[').replace (unique+'_CLOSE_BRACE', ']')
|
||||
restoredTokenList.append (token)
|
||||
|
||||
return restoredTokenList
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
AUTHOR
|
||||
Pau Sanchez
|
||||
http://www.codigomanso.com/
|
||||
|
||||
VERSION:
|
||||
bbcodeutils v1.0
|
||||
|
||||
LICENSE
|
||||
This code is licensed under Creative Commons Attribution 3.0
|
||||
http://creativecommons.org/licenses/by/3.0/
|
||||
|
||||
You can use this python module or any part of the code you want as long as you add
|
||||
my name as a contributor to your project.
|
||||
|
||||
DESCRIPTION
|
||||
This module can be used to produce HTML from BBCode, to generate BBCode or to fix invalid BBCode.
|
||||
|
||||
The classes are:
|
||||
- bbcodeparser
|
||||
- bbcodebuilder
|
||||
- bbcode2html
|
||||
|
||||
You can use bbcodeparser to parse BBCode and to produce output in any format you want.
|
||||
|
||||
Open the python file to find more information and examples of use of each class. It can
|
||||
be a good idea to check the test.py for examples
|
||||
|
||||
To run the unit tests:
|
||||
> python test.py
|
||||
|
||||
To run the performance test:
|
||||
> python test.py BBCodeTests.performanceTest
|
||||
|
||||
|
||||
EXAMPLES OF BBCode:
|
||||
|
||||
[b] -> bold
|
||||
[u] -> underline
|
||||
[i] -> italic
|
||||
|
||||
[center] -> center the text inside
|
||||
[color=XXX] -> change color of text
|
||||
[size=XXX] -> change size of text
|
||||
|
||||
Lists:
|
||||
[ul] -> unordered list
|
||||
[ol] -> ordered list
|
||||
[li] -> list item
|
||||
|
||||
[list] -> start unordered list
|
||||
[*] -> list item
|
||||
[list=1] -> start a list of numbers
|
||||
[list=a] -> start a list of alphabetic characters
|
||||
|
||||
Advanced:
|
||||
[url] -> link to url
|
||||
[url=http://link/url/]text[/url]
|
||||
[url link=http://link/url/ title="This is the title"]text[/url]
|
||||
|
||||
[img]http://to/image[/img]
|
||||
[img=230x330]http://to/image[/img]
|
||||
[img="Alt text here"]http://to/image[/img]
|
||||
[img="Alt text here" width=320 height=240]http://to/image[/img]
|
||||
|
||||
[email]asdf@asdf.com[/email]
|
||||
[email=john@asdf.com]John Smith[/email]
|
||||
|
||||
[google]search this[/google]
|
||||
[wikipedia]Tom Hanks[/wikipedia]
|
||||
[wikipedia lang=es]Tom Hanks[/wikipedia]
|
||||
|
||||
Tables:
|
||||
[table]
|
||||
[tr]
|
||||
[th]
|
||||
[td]
|
||||
|
||||
Advanced:
|
||||
[google]
|
||||
[wikipedia]
|
||||
|
||||
@@ -1,420 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
#
|
||||
# Author: Pau Sanchez (contact@pausanchez.com)
|
||||
# Version: v1.0
|
||||
# Last Modified: 2010/09/15
|
||||
#
|
||||
# For the latest version check out:
|
||||
# http://www.codigomanso.com/en/projects
|
||||
#
|
||||
# My blog:
|
||||
# http://www.codigomanso.com/en/ - English Version
|
||||
# http://www.codigomanso.com/es/ - Spanish Version
|
||||
#
|
||||
|
||||
from bbcodeparser import bbcodeparser
|
||||
from bbcodebuilder import bbcodebuilder
|
||||
|
||||
import random
|
||||
import unittest
|
||||
|
||||
class BBCodeTests(unittest.TestCase):
|
||||
def setUp (self):
|
||||
self.bbcode = bbcodeparser()
|
||||
return
|
||||
|
||||
def testConstructor (self):
|
||||
self.assertEqual (bbcodeparser ('whatever').html(), 'whatever')
|
||||
self.assertEqual (bbcodeparser ('[b]bold[/b]').html(), '<b>bold</b>')
|
||||
self.assertEqual (str (bbcodeparser ('[b]bold[/b]')), '[b]bold[/b]')
|
||||
return
|
||||
|
||||
def testBold (self):
|
||||
self.assertEqual (self.bbcode.parse ('whatever').html(), 'whatever')
|
||||
self.assertEqual (self.bbcode.parse ('[b]bold[/b]').html(), '<b>bold</b>')
|
||||
self.assertEqual (self.bbcode.parse ('[B]bold[/b]').html(), '<b>bold</b>')
|
||||
self.assertEqual (self.bbcode.parse ('this is [B]bold[/B]').html(), 'this is <b>bold</b>')
|
||||
return
|
||||
|
||||
def testItalic (self):
|
||||
self.assertEqual (self.bbcode.parse ('[i]italic[/i]').html(), '<i>italic</i>')
|
||||
return
|
||||
|
||||
def testUnderline (self):
|
||||
self.assertEqual (self.bbcode.parse ('[u]italic[/u]').html(), '<u>italic</u>')
|
||||
return
|
||||
|
||||
def testURLs (self):
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[url]http://www.google.com[/url]').html(),
|
||||
'<a href="http://www.google.com">http://www.google.com</a>'
|
||||
)
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[url=http://www.google.com]Google[/url]').html(),
|
||||
'<a href="http://www.google.com">Google</a>'
|
||||
)
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[url="http://www.google.com"]Google[/url]').html(),
|
||||
'<a href="http://www.google.com">Google</a>'
|
||||
)
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[url="http://www.google.com" title="Search Engine"]Google[/url]').html(),
|
||||
'<a href="http://www.google.com" title="Search Engine">Google</a>'
|
||||
)
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[url link="http://www.google.com"]Google[/url]').html(),
|
||||
'<a href="http://www.google.com">Google</a>'
|
||||
)
|
||||
return
|
||||
|
||||
def testPTag (self):
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[p color=#0000ff]blue[/p]').html(),
|
||||
u'<p style="color: #0000ff;">blue</p>'
|
||||
)
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[p size=12]12pt font[/p]').html(),
|
||||
u'<p style="font-size: 12pt;">12pt font</p>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[p font=arial]arial[/p]').html(),
|
||||
u'<p style="font-family: arial;">arial</p>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[p font=arial color=blue size=14]blue 14pt arial').html(),
|
||||
u'<p style="color: blue; font-size: 14pt; font-family: arial;">blue 14pt arial</p>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[p class=whatever]text[/p]').html(),
|
||||
u'<p>text</p>'
|
||||
)
|
||||
return
|
||||
|
||||
def testColorTag (self):
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[color=#0000ff]blue[/color]').html(),
|
||||
u'<span style="color: #0000ff;">blue</span>'
|
||||
)
|
||||
return
|
||||
|
||||
def testSizeTag (self):
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[size=12]12pt font[/size]').html(),
|
||||
u'<span style="font-size: 12pt;">12pt font</span>'
|
||||
)
|
||||
return
|
||||
|
||||
def testEmail(self):
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[email]asdf@asdf.com[/email]').html(),
|
||||
u'<a href="mailto:asdf@asdf.com">asdf@asdf.com</a>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[email=john@smith.com]John Smith[/email]').html(),
|
||||
u'<a href="mailto:john@smith.com">John Smith</a>'
|
||||
)
|
||||
return
|
||||
|
||||
def testImgTag (self):
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[img]http://www.codigomanso.com/image.jpg[/img]').html(),
|
||||
u'<img src="http://www.codigomanso.com/image.jpg" />'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[img="This is the ALT of the image"]http://www.codigomanso.com/image.jpg[/img]').html(),
|
||||
u'<img alt="This is the ALT of the image" src="http://www.codigomanso.com/image.jpg" />'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[img=320x200]http://www.codigomanso.com/image.jpg[/img]').html(),
|
||||
u'<img height="200" src="http://www.codigomanso.com/image.jpg" width="320" />'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[img=320x200 title="Image Test"]http://www.codigomanso.com/image.jpg[/img]').html(),
|
||||
u'<img height="200" src="http://www.codigomanso.com/image.jpg" title="Image Test" width="320" />'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[img="whatever" width=320 height="212" title="Image Test"]http://www.codigomanso.com/image.jpg[/img]').html(),
|
||||
u'<img alt="whatever" height="212" src="http://www.codigomanso.com/image.jpg" title="Image Test" width="320" />'
|
||||
)
|
||||
return
|
||||
|
||||
def testGoogleURL (self):
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[google]asdf[/google]').html(),
|
||||
u'<a href="http://www.google.com/search?q=asdf">asdf</a>'
|
||||
)
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[google]Tom Hanks[/google]').html(),
|
||||
u'<a href="http://www.google.com/search?q=Tom+Hanks">Tom Hanks</a>'
|
||||
)
|
||||
return
|
||||
|
||||
def testWikipediaURL (self):
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[wikipedia]Tom Hanks[/wikipedia]').html(),
|
||||
u'<a href="http://www.wikipedia.org/wiki/Tom_Hanks">Tom Hanks</a>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[wikipedia language=en]Tom Hanks[/wikipedia]').html(),
|
||||
u'<a href="http://en.wikipedia.org/wiki/Tom_Hanks">Tom Hanks</a>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[wikipedia lang=es]Tom Hanks[/wikipedia]').html(),
|
||||
u'<a href="http://es.wikipedia.org/wiki/Tom_Hanks">Tom Hanks</a>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[wikipedia=es]Tom Hanks[/wikipedia]').html(),
|
||||
u'<a href="http://es.wikipedia.org/wiki/Tom_Hanks">Tom Hanks</a>'
|
||||
)
|
||||
return
|
||||
|
||||
def testListTags (self):
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[ul][li]item 1[/li][li]item 2[/li][/ul]').html(),
|
||||
u'<ul><li>item 1</li><li>item 2</li></ul>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[ol][li]item 1[/li][li]item 2[/li][/ol]').html(),
|
||||
u'<ol><li>item 1</li><li>item 2</li></ol>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[list][li]item 1[/li][li]item 2[/li][/list]').html(),
|
||||
u'<ul><li>item 1</li><li>item 2</li></ul>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[list][*]item 1[*]item 2[/list]').html(),
|
||||
u'<ul><li>item 1</li><li>item 2</li></ul>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[list=1][li]item 1[/li][li]item 2[/li][/list]').html(),
|
||||
u'<ol type="1"><li>item 1</li><li>item 2</li></ol>'
|
||||
)
|
||||
return
|
||||
|
||||
def testInvalidCode (self):
|
||||
self.assertEqual (self.bbcode.parse ('[invalid]valid text[/invalid]').html(), 'valid text')
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[b]bold and [i]italics[/b]').html(),
|
||||
'<b>bold and <i>italics</i></b>'
|
||||
)
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[/b]invalid[/b][/p]').html(),
|
||||
'invalid'
|
||||
)
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[p][b]bold').html(),
|
||||
'<p><b>bold</b></p>'
|
||||
)
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[p][b]a <b>').html(),
|
||||
'<p><b>a <b></b></p>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[ol][li]item 1[li]item 2[/li][/ol]').html(),
|
||||
u'<ol><li>item 1</li><li>item 2</li></ol>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[b]\[b\] stands for [b]bold[/b]').html(),
|
||||
u'<b>[b] stands for </b><b>bold</b>'
|
||||
)
|
||||
return
|
||||
|
||||
def testEscapedBrackets (self):
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('\[b\]not bold\[/b\]').html(),
|
||||
u'[b]not bold[/b]'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[b]\[b\] stands for bold[/b]').html(),
|
||||
u'<b>[b] stands for bold</b>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('\[b\][b]stands for bold[/b]').html(),
|
||||
u'[b]<b>stands for bold</b>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('\[b\][b]stands for bold[/b] just like <b> in HTML').html(),
|
||||
u'[b]<b>stands for bold</b> just like <b> in HTML'
|
||||
)
|
||||
|
||||
def testBigExample (self):
|
||||
inputText = """check this out
|
||||
|
||||
[h1 class=circle]heading[/h1]
|
||||
|
||||
[p size=14 color=blue font="verdana, Times New Roman"]This is [b] bold [/b] and this [i]italic[/i] and this is [color=red]red[/color] and this is [color="red"]also red[/color].
|
||||
[/p]
|
||||
|
||||
fix [b][i]bold [font=verdana][size=12]and[/size][/font] italic[/b]
|
||||
[img]http://www.codigomanso.com/b.jpg[/img]
|
||||
[url]http://www.codigomanso.com/[/url]
|
||||
[url=http://www.codigomanso.com/]Codigo Manso[/url]
|
||||
[uRl link=http://www.codigomanso.com title="Codigo Manso Blog"]Codigo Manso[/url]
|
||||
|
||||
[ul]
|
||||
[Li]item 1[/Li]
|
||||
[li]item 2[/LI]
|
||||
[/UL]
|
||||
|
||||
[list=1 ]
|
||||
[*]item 1
|
||||
[*]item 2
|
||||
[/list]
|
||||
|
||||
[table class="big"]
|
||||
[tr]
|
||||
[th]big[/th]
|
||||
[/tr]
|
||||
[/table]
|
||||
[invalid class="extra"]whatever[/invalid]"""
|
||||
|
||||
out = self.bbcode.parse (inputText).html(allowClassAttr = True)
|
||||
self.assertEquals (out, '''check this out
|
||||
|
||||
<h1 class="circle">heading</h1>
|
||||
|
||||
<p style="color: blue; font-size: 14pt; font-family: verdana, Times New Roman;">This is <b> bold </b> and this <i>italic</i> and this is <span style="color: red;">red</span> and this is <span style="color: red;">also red</span>.
|
||||
</p>
|
||||
|
||||
fix <b><i>bold <span style="font-family: verdana;"><span style="font-size: 12pt;">and</span></span> italic</i></b>
|
||||
<img src="http://www.codigomanso.com/b.jpg" />
|
||||
<a href="http://www.codigomanso.com/">http://www.codigomanso.com/</a>
|
||||
<a href="http://www.codigomanso.com/">Codigo Manso</a>
|
||||
<a href="http://www.codigomanso.com" title="Codigo Manso Blog">Codigo Manso</a>
|
||||
|
||||
<ul>
|
||||
<li>item 1</li>
|
||||
<li>item 2</li>
|
||||
</ul>
|
||||
|
||||
<ol type="1">
|
||||
<li>item 1
|
||||
</li><li>item 2
|
||||
</li></ol>
|
||||
|
||||
<table class="big">
|
||||
<tr>
|
||||
<th>big</th>
|
||||
</tr>
|
||||
</table>
|
||||
whatever''')
|
||||
|
||||
|
||||
def testBBCodeDumper (self):
|
||||
self.assertEquals (
|
||||
self.bbcode.parse ('[b]bold[/b]').bbcode(),
|
||||
'[b]bold[/b]'
|
||||
)
|
||||
|
||||
self.assertEquals (
|
||||
self.bbcode.parse ('[color=red]text in red[/color]').bbcode(),
|
||||
'[color=red]text in red[/color]'
|
||||
)
|
||||
self.assertEquals (
|
||||
self.bbcode.parse ('[p][color=red]text in red').bbcode(),
|
||||
'[p][color=red]text in red[/color][/p]'
|
||||
)
|
||||
|
||||
self.assertEquals (
|
||||
self.bbcode.parse ('This [b][i]code[/b] will be fixed[/invalid]').bbcode(),
|
||||
'This [b][i]code[/i][/b] will be fixed'
|
||||
)
|
||||
|
||||
self.assertEquals (
|
||||
self.bbcode.parse ('\[[url]http://www.codigomanso.com/en[/url]\]').bbcode(),
|
||||
"\[[url]http://www.codigomanso.com/en[/url]\]"
|
||||
)
|
||||
|
||||
def performanceTest(self):
|
||||
'''
|
||||
This test checks the performance of parse and html operations
|
||||
|
||||
To run this test type:
|
||||
> python test.py BBCodeTests.performanceTest
|
||||
'''
|
||||
inputText = """check this out
|
||||
|
||||
[h1 class=circle]heading[/h1]
|
||||
|
||||
[p size=14 color=blue font="verdana, Times New Roman"]This is [b] bold [/b] and this [i]italic[/i] and this is [color=red]red[/color] and this is [color="red"]also red[/color].
|
||||
[/p]
|
||||
|
||||
fix [b][i]bold [font=verdana][size=12]and[/size][/font] italic[/b]
|
||||
[img]http://www.codigomanso.com/b.jpg[/img]
|
||||
[url]http://www.codigomanso.com/[/url]
|
||||
[url=http://www.codigomanso.com/]Codigo Manso[/url]
|
||||
[uRl link=http://www.codigomanso.com title="Codigo Manso Blog"]Codigo Manso[/url]
|
||||
|
||||
[ul]
|
||||
[Li]item 1[/Li]
|
||||
[li]item 2[/LI]
|
||||
[/UL]
|
||||
|
||||
[list=1 ]
|
||||
[*]item 1
|
||||
[*]item 2
|
||||
[/list]
|
||||
|
||||
[table class="big"]
|
||||
[tr]
|
||||
[th]big[/th]
|
||||
[/tr]
|
||||
[/table]
|
||||
[invalid class="extra"]whatever[/invalid]"""
|
||||
|
||||
import time
|
||||
start = time.time()
|
||||
|
||||
for i in range(0, 12):
|
||||
inputText += inputText
|
||||
|
||||
print "len(inputText) = %.2f MB (took %.2f seconds)" % (len(inputText)/(1024.0*1024.0), time.time() - start)
|
||||
|
||||
bbcode = bbcodeparser()
|
||||
start = time.time()
|
||||
bbcode.parse (inputText)
|
||||
total = (time.time() - start)
|
||||
print "time (bbcode.parse()) = %f" % total
|
||||
print " >> %.2f chars/second" % (len(inputText) / total)
|
||||
|
||||
start = time.time()
|
||||
bbcode.html(doDeepCopy = False)
|
||||
total = (time.time() - start)
|
||||
print "time (bbcode.html()) = %f" % total
|
||||
print " >> %.2f chars/second" % (len(inputText) / total)
|
||||
return
|
||||
|
||||
def testCodeBuilder (self):
|
||||
bbcode = bbcodebuilder ()
|
||||
self.assertEquals (bbcode.b ('bold'), u'[b]bold[/b]')
|
||||
self.assertEquals (bbcode.color ('this goes in red', 'red'), u'[color=red]this goes in red[/color]')
|
||||
self.assertEquals (bbcode.url ('Google', 'http://www.google.com'), u'[url=http://www.google.com]Google[/url]')
|
||||
self.assertEquals (bbcode.alist('item 1', 'item 2'), u"[list=a]\n [*]item 1\n [*]item 2\n[/list]")
|
||||
return
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
|
||||
|
||||
@@ -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'
|
||||
]
|
||||
|
||||
@@ -94,7 +96,12 @@ class Configuration(ConfigParser.SafeConfigParser):
|
||||
#print("found %s in section [%s]"%(key,section))
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
self.get(section,"add_to_"+key)
|
||||
#print("found add_to_%s in section [%s]"%(key,section))
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
@@ -106,16 +113,24 @@ class Configuration(ConfigParser.SafeConfigParser):
|
||||
if val and val.lower() == "false":
|
||||
val = False
|
||||
#print "getConfig(%s)=[%s]%s" % (key,section,val)
|
||||
return val
|
||||
break
|
||||
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError), e:
|
||||
pass
|
||||
|
||||
for section in self.sectionslist[::-1]:
|
||||
# 'martian smiley' [::-1] reverses list by slicing whole list with -1 step.
|
||||
try:
|
||||
val = val + self.get(section,"add_to_"+key)
|
||||
#print "getConfig(add_to_%s)=[%s]%s" % (key,section,val)
|
||||
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError), e:
|
||||
pass
|
||||
|
||||
return val
|
||||
|
||||
# split and strip each.
|
||||
def getConfigList(self, key):
|
||||
vlist = self.getConfig(key).split(',')
|
||||
vlist = [ v.strip() for v in vlist ]
|
||||
vlist = filter( lambda x : x !='', [ v.strip() for v in vlist ])
|
||||
#print "vlist("+key+"):"+str(vlist)
|
||||
return vlist
|
||||
|
||||
|
||||
@@ -27,8 +27,6 @@ from configurable import Configuration
|
||||
|
||||
def get_urls_from_page(url,configuration=None):
|
||||
|
||||
normalized = set() # normalized url
|
||||
retlist = [] # orig urls.
|
||||
if not configuration:
|
||||
configuration = Configuration("test1.com","EPUB")
|
||||
|
||||
@@ -56,6 +54,16 @@ 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)
|
||||
|
||||
def get_urls_from_html(data,url=None,configuration=None):
|
||||
|
||||
normalized = set() # normalized url
|
||||
retlist = [] # orig urls.
|
||||
|
||||
if not configuration:
|
||||
configuration = Configuration("test1.com","EPUB")
|
||||
|
||||
soup = BeautifulSoup(data)
|
||||
|
||||
for a in soup.findAll('a'):
|
||||
@@ -81,6 +89,33 @@ def get_urls_from_page(url,configuration=None):
|
||||
|
||||
return retlist
|
||||
|
||||
def get_urls_from_text(data,configuration=None):
|
||||
|
||||
normalized = set() # normalized url
|
||||
retlist = [] # orig urls.
|
||||
|
||||
if not configuration:
|
||||
configuration = Configuration("test1.com","EPUB")
|
||||
|
||||
for href in re.findall('http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', data):
|
||||
# this (should) catch normal story links, some javascript
|
||||
# 'are you old enough' links, and 'Report This' links.
|
||||
# The 'normalized' set prevents duplicates.
|
||||
if 'story.php' in href:
|
||||
m = re.search(r"(?P<sid>(view)?story\.php\?(sid|psid|no|story|stid)=\d+)",a['href'])
|
||||
if m != None:
|
||||
href = form_url(None,m.group('sid'))
|
||||
try:
|
||||
href = href.replace('&index=1','')
|
||||
adapter = adapters.getAdapter(configuration,href)
|
||||
if adapter.story.getMetadata('storyUrl') not in normalized:
|
||||
normalized.add(adapter.story.getMetadata('storyUrl'))
|
||||
retlist.append(href)
|
||||
except:
|
||||
pass
|
||||
|
||||
return retlist
|
||||
|
||||
def form_url(parenturl,url):
|
||||
url = url.strip() # ran across an image with a space in the
|
||||
# src. Browser handled it, so we'd better, too.
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+3
-4
@@ -57,9 +57,8 @@
|
||||
<h3>Changes:</h3>
|
||||
<p>
|
||||
<ul>
|
||||
<li>New site: www.henneth-annun.net -- Thanks Ida!</li>
|
||||
<li>New site: www.psychfic.com -- Thanks Ida!</li>
|
||||
<li>Now accepting www.skyehawke.com/archive URLs for archive.skyehawke.com stories.</li>
|
||||
<li>Add seriesUrl (and generated seriesHTML) as valid metadata entries. The default series on title_page is now a link.</li>
|
||||
<li>Add user/pass for dokuga.com.</li>
|
||||
</ul>
|
||||
</p>
|
||||
<p>
|
||||
@@ -70,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-42.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
|
||||
<a href="http://4-4-47.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
|
||||
</p>
|
||||
<div id='error'>
|
||||
{{ error_message }}
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ if __name__=="__main__":
|
||||
exclude=['*.pyc','*~','*.xcf','*[0-9].png']
|
||||
# from top dir. 'w' for overwrite
|
||||
createZipFile(filename,"w",
|
||||
['plugin-defaults.ini','plugin-example.ini','epubmerge.py','fanficdownloader'],
|
||||
['plugin-defaults.ini','plugin-example.ini','fanficdownloader','downloader.py','defaults.ini'],
|
||||
exclude=exclude)
|
||||
#from calibre-plugin dir. 'a' for append
|
||||
os.chdir('calibre-plugin')
|
||||
|
||||
+21
-9
@@ -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
|
||||
@@ -157,7 +160,7 @@ extratags: FanFiction
|
||||
## metadata part(s) to look at, 2) a regular expression to match the
|
||||
## template, and 3) the name of the GC setting to use, which must
|
||||
## match exactly. Use this parameter in [defaults], or by site eg,
|
||||
## [www.ficwad.com]
|
||||
## [ficwad.com]
|
||||
## Make sure to keep at least one space at the start of each line and
|
||||
## to escape % to %%, if used.
|
||||
## template => regexp to match => GC Setting to use.
|
||||
@@ -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
|
||||
@@ -269,6 +272,8 @@ include_tocpage: false
|
||||
## dateUpdated,numChapters,numWords at a minimum) will be shown.
|
||||
## Great for tracking when chapters came out and when the description,
|
||||
## etc changed.
|
||||
## Plugin will now preserve the log page when the epub is overwritten,
|
||||
## too.
|
||||
include_logpage: false
|
||||
## If set to 'smart', logpage will only be included if the story is
|
||||
## status:In-Progress or already had a logpage. That way you don't
|
||||
@@ -280,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
|
||||
@@ -344,7 +349,7 @@ output_css:
|
||||
## It can be either a 'file:' or 'http:' url.
|
||||
## Note that if you enable make_firstimage_cover in [epub], but want
|
||||
## to use default_cover_image for a specific site, use the site:format
|
||||
## section, for example: [www.ficwad.com:epub]
|
||||
## section, for example: [ficwad.com:epub]
|
||||
## default_cover_image is a python string Template string with
|
||||
## ${title}, ${author} etc, same as titlepage_entries. Unless
|
||||
## allow_unsafe_filename is true, invalid filename chars will be
|
||||
@@ -949,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
|
||||
@@ -1008,7 +1020,7 @@ extratags:
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:reviews,favs,follows
|
||||
|
||||
[www.ficwad.com]
|
||||
[ficwad.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
@@ -1196,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
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@
|
||||
## default is false
|
||||
#collect_series: true
|
||||
|
||||
[www.ficwad.com]
|
||||
[ficwad.com]
|
||||
#username:YourUsername
|
||||
#password:YourPassword
|
||||
|
||||
|
||||
Reference in New Issue
Block a user