mirror of
https://github.com/wassname/FanFicFare.git
synced 2026-09-15 11:36:30 +08:00
Compare commits
45
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6875d472d | ||
|
|
5758e364e4 | ||
|
|
d9a99fb7e9 | ||
|
|
0a32cdb277 | ||
|
|
00c1fe8704 | ||
|
|
bb96ecd5fc | ||
|
|
64866f7da6 | ||
|
|
73815bda43 | ||
|
|
d54acd936e | ||
|
|
e88244bebd | ||
|
|
02bdaf1086 | ||
|
|
21d5a39958 | ||
|
|
db661b1f9d | ||
|
|
0f654e86d6 | ||
|
|
063ee09e36 | ||
|
|
4bb26278ad | ||
|
|
d1f11f8ac4 | ||
|
|
ec352e728e | ||
|
|
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 |
@@ -1,6 +1,6 @@
|
||||
# ffd-retief-hrd fanfictiondownloader
|
||||
application: fanfictiondownloader
|
||||
version: 4-4-46
|
||||
version: 4-4-52
|
||||
runtime: python27
|
||||
api_version: 1
|
||||
threadsafe: true
|
||||
|
||||
@@ -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, 12)
|
||||
version = (1, 7, 19)
|
||||
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.
|
||||
|
||||
+55
-163
@@ -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_()
|
||||
|
||||
+289
-362
@@ -7,29 +7,28 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Jim Miller'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import traceback
|
||||
import traceback, re
|
||||
from functools import partial
|
||||
|
||||
import urllib
|
||||
import email
|
||||
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4.Qt import (QDialog, QTableWidget, QMessageBox, QVBoxLayout, QHBoxLayout,
|
||||
QGridLayout, QPushButton, QProgressDialog, QString, QLabel,
|
||||
QCheckBox, QIcon, QTextCursor, QTextEdit, QLineEdit, QInputDialog,
|
||||
QComboBox, QClipboard, QVariant, QProgressDialog, QTimer,
|
||||
QDialogButtonBox, QPixmap, Qt, QAbstractItemView, SIGNAL,
|
||||
QTableWidgetItem )
|
||||
from PyQt4.Qt import (QDialog, QTableWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
|
||||
QPushButton, QString, QLabel, QCheckBox, QIcon, QLineEdit,
|
||||
QComboBox, QVariant, QProgressDialog, QTimer, QDialogButtonBox,
|
||||
QPixmap, Qt, QAbstractItemView, SIGNAL, QTextEdit, pyqtSignal)
|
||||
|
||||
from calibre.gui2 import error_dialog, warning_dialog, question_dialog, info_dialog
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
from calibre.gui2.complete2 import EditWithComplete
|
||||
|
||||
from calibre import confirm_config_name
|
||||
from calibre.gui2 import dynamic
|
||||
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters,writers,exceptions
|
||||
from calibre_plugins.fanfictiondownloader_plugin.common_utils \
|
||||
import (ReadOnlyTableWidgetItem, ReadOnlyTextIconWidgetItem, SizePersistedDialog,
|
||||
ImageTitleLayout, get_icon)
|
||||
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.geturls import get_urls_from_html, get_urls_from_text
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.adapters import getNormalStoryURL
|
||||
|
||||
SKIP=u'Skip'
|
||||
ADDNEW=u'Add New Book'
|
||||
UPDATE=u'Update EPUB if New Chapters'
|
||||
@@ -48,7 +47,62 @@ collision_order=[SKIP,
|
||||
anthology_collision_order=[UPDATE,
|
||||
UPDATEALWAYS,
|
||||
OVERWRITEALWAYS]
|
||||
|
||||
class RejectUrlEntry:
|
||||
|
||||
matchpat=re.compile(r"^(?P<url>[^,]+)(,(?P<fullnote>(((?P<title>.+) by (?P<auth>.+?)( - (?P<note>.+))?)|.*)))?$")
|
||||
|
||||
def __init__(self,url_or_line,note=None,title=None,auth=None,
|
||||
addreasontext=None,fromline=False):
|
||||
|
||||
self.url=url_or_line
|
||||
self.note=note
|
||||
self.title=title
|
||||
self.auth=auth
|
||||
self.valid=False
|
||||
|
||||
if fromline:
|
||||
mc = re.match(self.matchpat,url_or_line)
|
||||
if mc:
|
||||
#print("mc:%s"%mc.groupdict())
|
||||
(url,title,auth,note) = mc.group('url','title','auth','note')
|
||||
if not mc.group('title'):
|
||||
title=''
|
||||
auth=''
|
||||
note=mc.group('fullnote')
|
||||
self.url=url
|
||||
self.note=note
|
||||
self.title=title
|
||||
self.auth=auth
|
||||
|
||||
if not self.note:
|
||||
if addreasontext:
|
||||
self.note = addreasontext
|
||||
else:
|
||||
self.note = ''
|
||||
else:
|
||||
if addreasontext:
|
||||
self.note = self.note + ' - ' + addreasontext
|
||||
|
||||
self.url = getNormalStoryURL(self.url)
|
||||
self.valid = self.url != None
|
||||
|
||||
def to_line(self):
|
||||
# always 'url,'
|
||||
return self.url+","+self.fullnote()
|
||||
|
||||
def fullnote(self):
|
||||
retval = ""
|
||||
if self.title and self.auth:
|
||||
retval = retval + "%s by %s"%(self.title,self.auth)
|
||||
if self.note:
|
||||
retval = retval + " - "
|
||||
|
||||
if self.note:
|
||||
retval = retval + self.note
|
||||
|
||||
return retval
|
||||
|
||||
# This is a more than slightly kludgey way to get
|
||||
# EditWithComplete to *not* alpha-order the reasons, but leave
|
||||
# them in the order entered. If
|
||||
@@ -73,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():
|
||||
@@ -88,25 +172,11 @@ class DroppableQTextEdit(QTextEdit):
|
||||
|
||||
class AddNewDialog(SizePersistedDialog):
|
||||
|
||||
def __init__(self, gui, prefs, icon, url_list_text, merge=False, newmerge=False):
|
||||
SizePersistedDialog.__init__(self, gui, 'FanFictionDownLoader plugin:add new dialog')
|
||||
self.gui = gui
|
||||
self.merge = merge
|
||||
self.newmerge = newmerge
|
||||
go_signal = pyqtSignal(object, object, object, object)
|
||||
|
||||
if merge:
|
||||
labeltext = 'Story URL(s) for anthology, one per line:'
|
||||
tooltiptext = 'URLs for stories to include in the anthology, one per line.\nWill take URLs from clipboard, but only valid URLs.'
|
||||
collisiontext = 'If Story Already Exists in Anthology?'
|
||||
collisiontooltip = "What to do if there's already an existing story with the same URL in the anthology."
|
||||
else:
|
||||
labeltext = 'Story URL(s), one per line:'
|
||||
tooltiptext = 'URLs for stories, one per line.\nWill take URLs from clipboard, but only valid URLs.\nAdd [1,5] after the URL to limit the download to chapters 1-5.'
|
||||
collisiontext = 'If Story Already Exists?'
|
||||
collisiontooltip = "What to do if there's already an existing story with the same URL or title and author."
|
||||
|
||||
if prefs['adddialogstaysontop']:
|
||||
QDialog.setWindowFlags ( self, Qt.Dialog|Qt.WindowStaysOnTopHint )
|
||||
def __init__(self, gui, prefs, icon):
|
||||
SizePersistedDialog.__init__(self, gui, 'FanFictionDownLoader plugin:add new dialog')
|
||||
self.prefs = prefs
|
||||
|
||||
self.setMinimumWidth(300)
|
||||
self.l = QVBoxLayout()
|
||||
@@ -115,69 +185,158 @@ class AddNewDialog(SizePersistedDialog):
|
||||
self.setWindowTitle('FanFictionDownLoader')
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
self.l.addWidget(QLabel(labeltext))
|
||||
self.toplabel=QLabel("Toplabel")
|
||||
self.l.addWidget(self.toplabel)
|
||||
self.url = DroppableQTextEdit(self)
|
||||
self.url.setToolTip(tooltiptext)
|
||||
self.url.setToolTip("UrlTooltip")
|
||||
self.url.setLineWrapMode(QTextEdit.NoWrap)
|
||||
self.url.setText(url_list_text)
|
||||
self.l.addWidget(self.url)
|
||||
|
||||
if not merge:
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel('Output &Format:')
|
||||
horz.addWidget(label)
|
||||
self.fileform = QComboBox(self)
|
||||
self.fileform.addItem('epub')
|
||||
self.fileform.addItem('mobi')
|
||||
self.fileform.addItem('html')
|
||||
self.fileform.addItem('txt')
|
||||
self.fileform.setCurrentIndex(self.fileform.findText(prefs['fileform']))
|
||||
self.fileform.setToolTip('Choose output format to create. May set default from plugin configuration.')
|
||||
self.fileform.activated.connect(self.set_collisions)
|
||||
|
||||
label.setBuddy(self.fileform)
|
||||
horz.addWidget(self.fileform)
|
||||
self.l.addLayout(horz)
|
||||
|
||||
if not newmerge:
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel(collisiontext)
|
||||
horz.addWidget(label)
|
||||
self.collision = QComboBox(self)
|
||||
self.collision.setToolTip(collisiontooltip)
|
||||
# add collision options
|
||||
self.set_collisions()
|
||||
i = self.collision.findText(prefs['collision'])
|
||||
if i > -1:
|
||||
self.collision.setCurrentIndex(i)
|
||||
label.setBuddy(self.collision)
|
||||
horz.addWidget(self.collision)
|
||||
self.l.addLayout(horz)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
self.updatemeta = QCheckBox('Update Calibre &Metadata?',self)
|
||||
self.updatemeta.setToolTip("Update metadata for existing stories in Calibre from web site?\n(Columns set to 'New Only' in the column tabs will only be set for new books.)")
|
||||
self.updatemeta.setChecked(prefs['updatemeta'])
|
||||
horz.addWidget(self.updatemeta)
|
||||
|
||||
if not merge: # hide if anthology merge.
|
||||
self.updateepubcover = QCheckBox('Update EPUB Cover?',self)
|
||||
self.updateepubcover.setToolTip('Update book cover image from site or defaults (if found) <i>inside</i> the EPUB when EPUB is updated.')
|
||||
self.updateepubcover.setChecked(prefs['updateepubcover'])
|
||||
horz.addWidget(self.updateepubcover)
|
||||
|
||||
self.l.addLayout(horz)
|
||||
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
button_box.accepted.connect(self.accept)
|
||||
button_box.rejected.connect(self.reject)
|
||||
self.l.addWidget(button_box)
|
||||
self.merge = self.newmerge = False
|
||||
|
||||
if url_list_text:
|
||||
button_box.button(QDialogButtonBox.Ok).setFocus()
|
||||
# elements to hide when doing merge.
|
||||
self.mergehide = []
|
||||
# elements to show again when doing *update* merge
|
||||
self.mergeupdateshow = []
|
||||
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel('Output &Format:')
|
||||
self.mergehide.append(label)
|
||||
|
||||
self.fileform = QComboBox(self)
|
||||
self.fileform.addItem('epub')
|
||||
self.fileform.addItem('mobi')
|
||||
self.fileform.addItem('html')
|
||||
self.fileform.addItem('txt')
|
||||
self.fileform.setToolTip('Choose output format to create. May set default from plugin configuration.')
|
||||
self.fileform.activated.connect(self.set_collisions)
|
||||
|
||||
horz.addWidget(label)
|
||||
label.setBuddy(self.fileform)
|
||||
horz.addWidget(self.fileform)
|
||||
self.l.addLayout(horz)
|
||||
self.mergehide.append(self.fileform)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
self.collisionlabel = QLabel("CollisionLabel")
|
||||
horz.addWidget(self.collisionlabel)
|
||||
self.collision = QComboBox(self)
|
||||
self.collision.setToolTip("CollisionToolTip")
|
||||
# add collision options
|
||||
self.set_collisions()
|
||||
i = self.collision.findText(prefs['collision'])
|
||||
if i > -1:
|
||||
self.collision.setCurrentIndex(i)
|
||||
self.collisionlabel.setBuddy(self.collision)
|
||||
horz.addWidget(self.collision)
|
||||
self.l.addLayout(horz)
|
||||
self.mergehide.append(self.collisionlabel)
|
||||
self.mergehide.append(self.collision)
|
||||
self.mergeupdateshow.append(self.collisionlabel)
|
||||
self.mergeupdateshow.append(self.collision)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
self.updatemeta = QCheckBox('Update Calibre &Metadata?',self)
|
||||
self.updatemeta.setToolTip("Update metadata for existing stories in Calibre from web site?\n(Columns set to 'New Only' in the column tabs will only be set for new books.)")
|
||||
self.updatemeta.setChecked(prefs['updatemeta'])
|
||||
horz.addWidget(self.updatemeta)
|
||||
self.mergehide.append(self.updatemeta)
|
||||
self.mergeupdateshow.append(self.updatemeta)
|
||||
|
||||
self.updateepubcover = QCheckBox('Update EPUB Cover?',self)
|
||||
self.updateepubcover.setToolTip('Update book cover image from site or defaults (if found) <i>inside</i> the EPUB when EPUB is updated.')
|
||||
self.updateepubcover.setChecked(prefs['updateepubcover'])
|
||||
horz.addWidget(self.updateepubcover)
|
||||
self.mergehide.append(self.updateepubcover)
|
||||
|
||||
self.l.addLayout(horz)
|
||||
|
||||
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
self.button_box.accepted.connect(self.ok_clicked)
|
||||
self.button_box.rejected.connect(self.reject)
|
||||
self.l.addWidget(self.button_box)
|
||||
|
||||
# invoke the
|
||||
def ok_clicked(self):
|
||||
self.hide()
|
||||
print("ok_clicked called")
|
||||
self.go_signal.emit( self.get_ffdl_options(),
|
||||
self.get_urlstext(),
|
||||
self.merge,
|
||||
self.extrapayload )
|
||||
|
||||
def show_dialog(self,
|
||||
url_list_text,
|
||||
callback,
|
||||
show=True,
|
||||
merge=False,
|
||||
newmerge=True,
|
||||
extraoptions={},
|
||||
extrapayload=None):
|
||||
# rather than mutex in ffdl_plugin, just bail here if it's
|
||||
# already in use.
|
||||
if self.isVisible(): return
|
||||
|
||||
try:
|
||||
self.go_signal.disconnect()
|
||||
except:
|
||||
pass # if not already connected.
|
||||
self.go_signal.connect(callback)
|
||||
|
||||
self.merge = merge
|
||||
self.newmerge = newmerge
|
||||
self.extraoptions = extraoptions
|
||||
self.extrapayload = extrapayload
|
||||
|
||||
if self.merge:
|
||||
self.toplabel.setText('Story URL(s) for anthology, one per line:')
|
||||
self.url.setToolTip('URLs for stories to include in the anthology, one per line.\nWill take URLs from clipboard, but only valid URLs.')
|
||||
self.collisionlabel.setText('If Story Already Exists in Anthology?')
|
||||
self.collision.setToolTip("What to do if there's already an existing story with the same URL in the anthology.")
|
||||
for widget in self.mergehide:
|
||||
widget.setVisible(False)
|
||||
if not self.newmerge:
|
||||
for widget in self.mergeupdateshow:
|
||||
widget.setVisible(True)
|
||||
else:
|
||||
for widget in self.mergehide:
|
||||
widget.setVisible(True)
|
||||
self.toplabel.setText('Story URL(s), one per line:')
|
||||
self.url.setToolTip('URLs for stories, one per line.\nWill take URLs from clipboard, but only valid URLs.\nAdd [1,5] after the URL to limit the download to chapters 1-5.')
|
||||
self.collisionlabel.setText('If Story Already Exists?')
|
||||
self.collision.setToolTip("What to do if there's already an existing story with the same URL or title and author.")
|
||||
|
||||
# Need to re-able after hiding/showing
|
||||
self.setAcceptDrops(True)
|
||||
self.url.setFocus()
|
||||
|
||||
if self.prefs['adddialogstaysontop']:
|
||||
QDialog.setWindowFlags ( self, Qt.Dialog | Qt.WindowStaysOnTopHint )
|
||||
else:
|
||||
QDialog.setWindowFlags ( self, Qt.Dialog )
|
||||
|
||||
if not self.merge:
|
||||
self.fileform.setCurrentIndex(self.fileform.findText(self.prefs['fileform']))
|
||||
|
||||
# add collision options
|
||||
self.set_collisions()
|
||||
|
||||
i = self.collision.findText(self.prefs['collision'])
|
||||
if i > -1:
|
||||
self.collision.setCurrentIndex(i)
|
||||
self.updatemeta.setChecked(self.prefs['updatemeta'])
|
||||
|
||||
if not self.merge:
|
||||
self.updateepubcover.setChecked(self.prefs['updateepubcover'])
|
||||
|
||||
self.url.setText(url_list_text)
|
||||
if url_list_text:
|
||||
self.button_box.button(QDialogButtonBox.Ok).setFocus()
|
||||
# restore saved size.
|
||||
self.resize_dialog()
|
||||
|
||||
if show: # so anthology update can be modal still.
|
||||
self.show()
|
||||
#self.resize(self.sizeHint())
|
||||
|
||||
def set_collisions(self):
|
||||
@@ -195,27 +354,21 @@ class AddNewDialog(SizePersistedDialog):
|
||||
self.collision.setCurrentIndex(i)
|
||||
|
||||
def get_ffdl_options(self):
|
||||
retval = {
|
||||
'fileform': unicode(self.fileform.currentText()),
|
||||
'collision': unicode(self.collision.currentText()),
|
||||
'updatemeta': self.updatemeta.isChecked(),
|
||||
'updateepubcover': self.updateepubcover.isChecked(),
|
||||
}
|
||||
|
||||
if self.merge:
|
||||
retval['fileform']=='epub'
|
||||
retval['updateepubcover']=True
|
||||
if self.newmerge:
|
||||
updatemeta=True
|
||||
collision=ADDNEW
|
||||
else:
|
||||
updatemeta=self.updatemeta.isChecked()
|
||||
collision=unicode(self.collision.currentText())
|
||||
|
||||
return {
|
||||
'fileform': 'epub',
|
||||
'collision': collision,
|
||||
'updatemeta': updatemeta,
|
||||
'updateepubcover': True,
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'fileform': unicode(self.fileform.currentText()),
|
||||
'collision': unicode(self.collision.currentText()),
|
||||
'updatemeta': self.updatemeta.isChecked(),
|
||||
'updateepubcover': self.updateepubcover.isChecked(),
|
||||
}
|
||||
retval['updatemeta']=True
|
||||
retval['collision']=ADDNEW
|
||||
|
||||
return dict(retval.items() + self.extraoptions.items() )
|
||||
|
||||
def get_urlstext(self):
|
||||
return unicode(self.url.toPlainText())
|
||||
@@ -234,7 +387,6 @@ class CollectURLDialog(SizePersistedDialog):
|
||||
'''
|
||||
def __init__(self, gui, title, url_text, epubmerge_plugin=None):
|
||||
SizePersistedDialog.__init__(self, gui, 'FanFictionDownLoader plugin:get story urls')
|
||||
self.gui = gui
|
||||
self.status=False
|
||||
self.anthology=False
|
||||
|
||||
@@ -288,7 +440,6 @@ class UserPassDialog(QDialog):
|
||||
'''
|
||||
def __init__(self, gui, site, exception=None):
|
||||
QDialog.__init__(self, gui)
|
||||
self.gui = gui
|
||||
self.status=False
|
||||
|
||||
self.l = QGridLayout()
|
||||
@@ -347,7 +498,6 @@ class LoopProgressDialog(QProgressDialog):
|
||||
QString(), 0, len(book_list), gui)
|
||||
self.setWindowTitle(win_title)
|
||||
self.setMinimumWidth(500)
|
||||
self.gui = gui
|
||||
self.book_list = book_list
|
||||
self.foreach_function = foreach_function
|
||||
self.finish_function = finish_function
|
||||
@@ -396,7 +546,6 @@ class LoopProgressDialog(QProgressDialog):
|
||||
|
||||
def do_when_finished(self):
|
||||
self.hide()
|
||||
self.gui = None
|
||||
# Queues a job to process these books in the background.
|
||||
self.finish_function(self.book_list)
|
||||
|
||||
@@ -445,7 +594,6 @@ class UpdateExistingDialog(SizePersistedDialog):
|
||||
def __init__(self, gui, header, prefs, icon, books,
|
||||
save_size_name='fanfictiondownloader_plugin:update list dialog'):
|
||||
SizePersistedDialog.__init__(self, gui, save_size_name)
|
||||
self.gui = gui
|
||||
|
||||
self.setWindowTitle(header)
|
||||
self.setWindowIcon(icon)
|
||||
@@ -463,11 +611,7 @@ class UpdateExistingDialog(SizePersistedDialog):
|
||||
|
||||
button_layout = QVBoxLayout()
|
||||
books_layout.addLayout(button_layout)
|
||||
# self.move_up_button = QtGui.QToolButton(self)
|
||||
# self.move_up_button.setToolTip('Move selected books up the list')
|
||||
# self.move_up_button.setIcon(QIcon(I('arrow-up.png')))
|
||||
# self.move_up_button.clicked.connect(self.books_table.move_rows_up)
|
||||
# button_layout.addWidget(self.move_up_button)
|
||||
|
||||
spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
button_layout.addItem(spacerItem)
|
||||
self.remove_button = QtGui.QToolButton(self)
|
||||
@@ -477,11 +621,6 @@ class UpdateExistingDialog(SizePersistedDialog):
|
||||
button_layout.addWidget(self.remove_button)
|
||||
spacerItem1 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
button_layout.addItem(spacerItem1)
|
||||
# self.move_down_button = QtGui.QToolButton(self)
|
||||
# self.move_down_button.setToolTip('Move selected books down the list')
|
||||
# self.move_down_button.setIcon(QIcon(I('arrow-down.png')))
|
||||
# self.move_down_button.clicked.connect(self.books_table.move_rows_down)
|
||||
# button_layout.addWidget(self.move_down_button)
|
||||
|
||||
options_layout = QHBoxLayout()
|
||||
|
||||
@@ -507,7 +646,6 @@ class UpdateExistingDialog(SizePersistedDialog):
|
||||
i = self.collision.findText(prefs['collision'])
|
||||
if i > -1:
|
||||
self.collision.setCurrentIndex(i)
|
||||
# self.collision.setToolTip('Overwrite will replace the existing story. Add New will create a new story with the same title and author.')
|
||||
label.setBuddy(self.collision)
|
||||
options_layout.addWidget(self.collision)
|
||||
|
||||
@@ -557,84 +695,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):
|
||||
@@ -694,11 +754,9 @@ class StoryListTableWidget(QTableWidget):
|
||||
self.setItem(row, 2, AuthorTableWidgetItem(", ".join(book['author']), ", ".join(book['author_sort'])))
|
||||
|
||||
url_cell = ReadOnlyTableWidgetItem(book['url'])
|
||||
#url_cell.setData(Qt.UserRole, QVariant(book['url']))
|
||||
self.setItem(row, 3, url_cell)
|
||||
|
||||
comment_cell = ReadOnlyTableWidgetItem(book['comment'])
|
||||
#comment_cell.setData(Qt.UserRole, QVariant(book))
|
||||
self.setItem(row, 4, comment_cell)
|
||||
|
||||
def get_books(self):
|
||||
@@ -732,56 +790,6 @@ class StoryListTableWidget(QTableWidget):
|
||||
self.selectRow(row)
|
||||
self.scrollToItem(self.currentItem())
|
||||
|
||||
def move_rows_up(self):
|
||||
self.setFocus()
|
||||
rows = self.selectionModel().selectedRows()
|
||||
if len(rows) == 0:
|
||||
return
|
||||
first_sel_row = rows[0].row()
|
||||
if first_sel_row <= 0:
|
||||
return
|
||||
# Workaround for strange selection bug in Qt which "alters" the selection
|
||||
# in certain circumstances which meant move down only worked properly "once"
|
||||
selrows = []
|
||||
for row in rows:
|
||||
selrows.append(row.row())
|
||||
selrows.sort()
|
||||
for selrow in selrows:
|
||||
self.swap_row_widgets(selrow - 1, selrow + 1)
|
||||
scroll_to_row = first_sel_row - 1
|
||||
if scroll_to_row > 0:
|
||||
scroll_to_row = scroll_to_row - 1
|
||||
self.scrollToItem(self.item(scroll_to_row, 0))
|
||||
|
||||
def move_rows_down(self):
|
||||
self.setFocus()
|
||||
rows = self.selectionModel().selectedRows()
|
||||
if len(rows) == 0:
|
||||
return
|
||||
last_sel_row = rows[-1].row()
|
||||
if last_sel_row == self.rowCount() - 1:
|
||||
return
|
||||
# Workaround for strange selection bug in Qt which "alters" the selection
|
||||
# in certain circumstances which meant move down only worked properly "once"
|
||||
selrows = []
|
||||
for row in rows:
|
||||
selrows.append(row.row())
|
||||
selrows.sort()
|
||||
for selrow in reversed(selrows):
|
||||
self.swap_row_widgets(selrow + 2, selrow)
|
||||
scroll_to_row = last_sel_row + 1
|
||||
if scroll_to_row < self.rowCount() - 1:
|
||||
scroll_to_row = scroll_to_row + 1
|
||||
self.scrollToItem(self.item(scroll_to_row, 0))
|
||||
|
||||
def swap_row_widgets(self, src_row, dest_row):
|
||||
self.blockSignals(True)
|
||||
self.insertRow(dest_row)
|
||||
for col in range(0, self.columnCount()):
|
||||
self.setItem(dest_row, col, self.takeItem(src_row, col))
|
||||
self.removeRow(src_row)
|
||||
self.blockSignals(False)
|
||||
|
||||
class RejectListTableWidget(QTableWidget):
|
||||
|
||||
def __init__(self, parent,rejectreasons=[]):
|
||||
@@ -789,85 +797,53 @@ class RejectListTableWidget(QTableWidget):
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.rejectreasons = rejectreasons
|
||||
|
||||
def on_headersection_clicked(self):
|
||||
self.setSortingEnabled(True)
|
||||
|
||||
def populate_table(self, reject_list):
|
||||
self.clear()
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setRowCount(len(reject_list))
|
||||
header_labels = ['URL', 'Note']
|
||||
header_labels = ['URL', 'Title', 'Author', 'Note']
|
||||
self.setColumnCount(len(header_labels))
|
||||
self.setHorizontalHeaderLabels(header_labels)
|
||||
self.horizontalHeader().setStretchLastSection(True)
|
||||
#self.verticalHeader().setDefaultSectionSize(24)
|
||||
self.verticalHeader().hide()
|
||||
|
||||
# need sortingEnbled to sort, but off to up & down.
|
||||
self.connect(self.horizontalHeader(),
|
||||
SIGNAL('sectionClicked(int)'),
|
||||
self.on_headersection_clicked)
|
||||
|
||||
# it's generally recommended to enable sort after pop, not
|
||||
# before. But then it needs to be sorted on a column and I'd
|
||||
# rather keep the order given.
|
||||
self.setSortingEnabled(True)
|
||||
# row is just row number.
|
||||
for row, rejectrow in enumerate(reject_list):
|
||||
#print("populating table:%s"%rejectrow.to_line())
|
||||
self.populate_table_row(row,rejectrow)
|
||||
|
||||
self.resizeColumnsToContents()
|
||||
self.setMinimumColumnWidth(1, 100)
|
||||
self.setMinimumColumnWidth(2, 100)
|
||||
self.setMinimumColumnWidth(0, 100)
|
||||
self.setMinimumColumnWidth(3, 100)
|
||||
self.setMinimumSize(300, 0)
|
||||
|
||||
def setMinimumColumnWidth(self, col, minimum):
|
||||
if self.columnWidth(col) < minimum:
|
||||
self.setColumnWidth(col, minimum)
|
||||
|
||||
def populate_table_row(self, row, rejectrow):
|
||||
|
||||
(bookid,url,titleauth,oldrejnote) = rejectrow
|
||||
if oldrejnote:
|
||||
noteprefix = note = oldrejnote
|
||||
# incase the existing note ends with one of the known reasons.
|
||||
for reason in self.rejectreasons:
|
||||
if noteprefix.endswith(' - '+reason):
|
||||
noteprefix = noteprefix[:-len(' - '+reason)]
|
||||
break
|
||||
else:
|
||||
noteprefix = note = titleauth
|
||||
|
||||
if len(noteprefix) > 0:
|
||||
noteprefix = noteprefix+' - '
|
||||
|
||||
url_cell = ReadOnlyTableWidgetItem(url)
|
||||
url_cell.setData(Qt.UserRole, QVariant(bookid))
|
||||
url_cell.setToolTip('URL to add to the Reject List.')
|
||||
self.setItem(row, 0, url_cell)
|
||||
def populate_table_row(self, row, rej):
|
||||
|
||||
self.setItem(row, 0, ReadOnlyTableWidgetItem(rej.url))
|
||||
self.setItem(row, 1, ReadOnlyTableWidgetItem(rej.title))
|
||||
self.setItem(row, 2, ReadOnlyTableWidgetItem(rej.auth))
|
||||
|
||||
note_cell = EditWithComplete(self)
|
||||
|
||||
note_cell.lineEdit().mcompleter.model().set_items = \
|
||||
partial(complete_model_set_items_kludge,
|
||||
note_cell.lineEdit().mcompleter.model())
|
||||
|
||||
items = [note]+[ noteprefix+x for x in self.rejectreasons ]
|
||||
items = [rej.note]+self.rejectreasons
|
||||
note_cell.update_items_cache(items)
|
||||
note_cell.show_initial_value(note)
|
||||
note_cell.show_initial_value(rej.note)
|
||||
note_cell.set_separator(None)
|
||||
note_cell.setToolTip('Select or Edit Reject Note.')
|
||||
self.setCellWidget(row, 1, note_cell)
|
||||
self.setCellWidget(row, 3, note_cell)
|
||||
|
||||
# note_cell = QTableWidgetItem(note)
|
||||
# note_cell.setToolTip('Double-click to edit note.')
|
||||
# self.setItem(row, 1, note_cell)
|
||||
|
||||
def get_reject_list(self):
|
||||
rejectrows = []
|
||||
for row in range(self.rowCount()):
|
||||
bookid = self.item(row, 0).data(Qt.UserRole).toPyObject()
|
||||
url = unicode(self.item(row, 0).text())
|
||||
note = unicode(self.cellWidget(row, 1).currentText()).strip()
|
||||
rejectrows.append((bookid,url,note))
|
||||
return rejectrows
|
||||
|
||||
def remove_selected_rows(self):
|
||||
self.setFocus()
|
||||
rows = self.selectionModel().selectedRows()
|
||||
@@ -890,57 +866,6 @@ class RejectListTableWidget(QTableWidget):
|
||||
self.selectRow(row)
|
||||
self.scrollToItem(self.currentItem())
|
||||
|
||||
def move_rows_up(self):
|
||||
self.setFocus()
|
||||
rows = self.selectionModel().selectedRows()
|
||||
if len(rows) == 0:
|
||||
return
|
||||
first_sel_row = rows[0].row()
|
||||
if first_sel_row <= 0:
|
||||
return
|
||||
# Workaround for strange selection bug in Qt which "alters" the selection
|
||||
# in certain circumstances which meant move down only worked properly "once"
|
||||
selrows = []
|
||||
for row in rows:
|
||||
selrows.append(row.row())
|
||||
selrows.sort()
|
||||
for selrow in selrows:
|
||||
self.swap_row_widgets(selrow - 1, selrow + 1)
|
||||
scroll_to_row = first_sel_row - 1
|
||||
if scroll_to_row > 0:
|
||||
scroll_to_row = scroll_to_row - 1
|
||||
self.scrollToItem(self.item(scroll_to_row, 0))
|
||||
|
||||
def move_rows_down(self):
|
||||
self.setFocus()
|
||||
rows = self.selectionModel().selectedRows()
|
||||
if len(rows) == 0:
|
||||
return
|
||||
last_sel_row = rows[-1].row()
|
||||
if last_sel_row == self.rowCount() - 1:
|
||||
return
|
||||
# Workaround for strange selection bug in Qt which "alters" the selection
|
||||
# in certain circumstances which meant move down only worked properly "once"
|
||||
selrows = []
|
||||
for row in rows:
|
||||
selrows.append(row.row())
|
||||
selrows.sort()
|
||||
for selrow in reversed(selrows):
|
||||
self.swap_row_widgets(selrow + 2, selrow)
|
||||
scroll_to_row = last_sel_row + 1
|
||||
if scroll_to_row < self.rowCount() - 1:
|
||||
scroll_to_row = scroll_to_row + 1
|
||||
self.scrollToItem(self.item(scroll_to_row, 0))
|
||||
|
||||
def swap_row_widgets(self, src_row, dest_row):
|
||||
self.blockSignals(True)
|
||||
self.setSortingEnabled(False)
|
||||
self.insertRow(dest_row)
|
||||
for col in range(0, self.columnCount()):
|
||||
self.setItem(dest_row, col, self.takeItem(src_row, col))
|
||||
self.removeRow(src_row)
|
||||
self.blockSignals(False)
|
||||
|
||||
class RejectListDialog(SizePersistedDialog):
|
||||
def __init__(self, gui, reject_list,
|
||||
rejectreasons=[],
|
||||
@@ -950,7 +875,6 @@ class RejectListDialog(SizePersistedDialog):
|
||||
show_all_reasons=True,
|
||||
save_size_name='ffdl:reject list dialog'):
|
||||
SizePersistedDialog.__init__(self, gui, save_size_name)
|
||||
self.gui = gui
|
||||
|
||||
self.setWindowTitle(header)
|
||||
self.setWindowIcon(get_icon(icon))
|
||||
@@ -970,21 +894,13 @@ class RejectListDialog(SizePersistedDialog):
|
||||
rejects_layout.addLayout(button_layout)
|
||||
spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
button_layout.addItem(spacerItem)
|
||||
# self.move_up_button = QtGui.QToolButton(self)
|
||||
# self.move_up_button.setToolTip('Move selected books up the list')
|
||||
# self.move_up_button.setIcon(QIcon(I('arrow-up.png')))
|
||||
# self.move_up_button.clicked.connect(self.books_table.move_rows_up)
|
||||
# button_layout.addWidget(self.move_up_button)
|
||||
|
||||
self.remove_button = QtGui.QToolButton(self)
|
||||
self.remove_button.setToolTip('Remove selected URL(s) from the list')
|
||||
self.remove_button.setIcon(get_icon('list_remove.png'))
|
||||
self.remove_button.clicked.connect(self.remove_from_list)
|
||||
button_layout.addWidget(self.remove_button)
|
||||
# self.move_down_button = QtGui.QToolButton(self)
|
||||
# self.move_down_button.setToolTip('Move selected books down the list')
|
||||
# self.move_down_button.setIcon(QIcon(I('arrow-down.png')))
|
||||
# self.move_down_button.clicked.connect(self.books_table.move_rows_down)
|
||||
# button_layout.addWidget(self.move_down_button)
|
||||
|
||||
spacerItem1 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
button_layout.addItem(spacerItem1)
|
||||
|
||||
@@ -1031,10 +947,21 @@ class RejectListDialog(SizePersistedDialog):
|
||||
self.rejects_table.remove_selected_rows()
|
||||
|
||||
def get_reject_list(self):
|
||||
return self.rejects_table.get_reject_list()
|
||||
rejectrows = []
|
||||
for row in range(self.rejects_table.rowCount()):
|
||||
url = unicode(self.rejects_table.item(row, 0).text()).strip()
|
||||
title = unicode(self.rejects_table.item(row, 1).text()).strip()
|
||||
auth = unicode(self.rejects_table.item(row, 2).text()).strip()
|
||||
note = unicode(self.rejects_table.cellWidget(row, 3).currentText()).strip()
|
||||
rejectrows.append(RejectUrlEntry(url,note,title,auth,self.get_reason_text()))
|
||||
return rejectrows
|
||||
|
||||
def get_reason_text(self):
|
||||
return unicode(self.reason_edit.currentText()).strip()
|
||||
try:
|
||||
return unicode(self.reason_edit.currentText()).strip()
|
||||
except:
|
||||
# doesn't have self.reason_edit when editing existing list.
|
||||
return None
|
||||
|
||||
def get_deletebooks(self):
|
||||
return self.deletebooks.isChecked()
|
||||
|
||||
+177
-140
@@ -41,12 +41,13 @@ from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.epubutils impo
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.geturls import get_urls_from_page
|
||||
|
||||
from calibre_plugins.fanfictiondownloader_plugin.ffdl_util import (get_ffdl_adapter, get_ffdl_config, get_ffdl_personalini)
|
||||
from calibre_plugins.fanfictiondownloader_plugin.config import (prefs, permitted_values, rejecturllist)
|
||||
from calibre_plugins.fanfictiondownloader_plugin.config import (permitted_values, rejecturllist)
|
||||
from calibre_plugins.fanfictiondownloader_plugin.prefs import prefs
|
||||
from calibre_plugins.fanfictiondownloader_plugin.dialogs import (
|
||||
AddNewDialog, UpdateExistingDialog, display_story_list, DisplayStoryListDialog,
|
||||
AddNewDialog, UpdateExistingDialog,
|
||||
LoopProgressDialog, UserPassDialog, AboutDialog, CollectURLDialog, RejectListDialog,
|
||||
OVERWRITE, OVERWRITEALWAYS, UPDATE, UPDATEALWAYS, ADDNEW, SKIP, CALIBREONLY,
|
||||
NotGoingToDownload )
|
||||
NotGoingToDownload, RejectUrlEntry )
|
||||
|
||||
# because calibre immediately transforms html into zip and don't want
|
||||
# to have an 'if html'. db.has_format is cool with the case mismatch,
|
||||
@@ -101,7 +102,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
# are not found in the zip file will result in null QIcons.
|
||||
icon = get_icon('images/icon.png')
|
||||
|
||||
#self.qaction.setText('FFDL')
|
||||
self.qaction.setText('FanFictionDL')
|
||||
|
||||
# The qaction is automatically created from the action_spec defined
|
||||
# above
|
||||
@@ -125,6 +126,10 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
# been displayed once.
|
||||
self.rebuild_menus()
|
||||
|
||||
self.add_new_dialog = AddNewDialog(self.gui,
|
||||
prefs,
|
||||
self.qaction.icon())
|
||||
|
||||
## Kludgey, yes, but with the real configuration inside the
|
||||
## library now, how else would a user be able to change this
|
||||
## setting if it's crashing calibre?
|
||||
@@ -135,7 +140,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
file_path = os.path.join(calibre_config_dir,
|
||||
*("plugins/fanfictiondownloader_macmenuhack.txt".split('/')))
|
||||
file_path = os.path.abspath(file_path)
|
||||
print("macmenuhack file_path:%s"%file_path)
|
||||
print("Plugin %s macmenuhack file_path:%s"%(self.name,file_path))
|
||||
self.macmenuhack = os.access(file_path, os.F_OK)
|
||||
return self.macmenuhack
|
||||
|
||||
@@ -398,17 +403,17 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
# want title/author, too, for rejects.
|
||||
self.populate_book_from_calibre_id(book,db)
|
||||
if book['url']:
|
||||
# get existing note, if there is one.
|
||||
book['oldrejnote']=rejecturllist.check(book['url'])
|
||||
# get existing note, if on rejected list.
|
||||
book['oldrejnote']=rejecturllist.get_note(book['url'])
|
||||
|
||||
def reject_list_urls_finish(self, book_list):
|
||||
|
||||
# construct reject list of tuples:
|
||||
# (calibre_id, url, "title, authors", old reject note).
|
||||
reject_list = [ ( x['calibre_id'],x['url'],
|
||||
"%s by %s"%(x['title'],
|
||||
', '.join(x['author'])),
|
||||
x['oldrejnote'])
|
||||
reject_list = [ RejectUrlEntry(x['url'],
|
||||
x['oldrejnote'],
|
||||
x['title'],
|
||||
', '.join(x['author']))
|
||||
for x in book_list if x['good'] ]
|
||||
if reject_list:
|
||||
d = RejectListDialog(self.gui,reject_list,
|
||||
@@ -418,19 +423,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
if d.result() != d.Accepted:
|
||||
return
|
||||
|
||||
bookids=[]
|
||||
rejectlist=[]
|
||||
addreasontext=d.get_reason_text()
|
||||
for (bookid,url,note) in d.get_reject_list():
|
||||
bookids.append(bookid)
|
||||
if addreasontext and note:
|
||||
note = note +" - "+addreasontext
|
||||
elif addreasontext:
|
||||
note = addreasontext
|
||||
rejectlist.append((url,note))
|
||||
print("Adding (%s) to Reject List: %s"%(url,note))
|
||||
|
||||
rejecturllist.add(rejectlist)
|
||||
rejecturllist.add(d.get_reject_list())
|
||||
|
||||
if d.get_deletebooks():
|
||||
self.gui.iactions['Remove Books'].delete_books()
|
||||
@@ -439,41 +432,22 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
message="<p>Rejecting FFDL URLs: None of the books selected have FanFiction URLs.</p><p>Proceed to Remove?</p>"
|
||||
if confirm(message,'fanfictiondownloader_reject_non_fanfiction', self.gui):
|
||||
self.gui.iactions['Remove Books'].delete_books()
|
||||
|
||||
def add_dialog(self,url_list_text=None,merge=False,anthology_url=None):
|
||||
|
||||
#print("add_dialog()")
|
||||
def add_dialog(self,url_list_text=None,merge=False,anthology_url=None):
|
||||
'Both new individual stories and new anthologies are created here.'
|
||||
|
||||
if not url_list_text:
|
||||
url_list = self.get_urls_clip()
|
||||
url_list_text = "\n".join(url_list)
|
||||
|
||||
# self.gui is the main calibre GUI. It acts as the gateway to access
|
||||
# all the elements of the calibre user interface, it should also be the
|
||||
# parent of the dialog
|
||||
# AddNewDialog just collects URLs, format and presents buttons.
|
||||
d = AddNewDialog(self.gui,
|
||||
prefs,
|
||||
self.qaction.icon(),
|
||||
url_list_text,
|
||||
merge=merge,
|
||||
newmerge=merge # if here, it's a new anthology.
|
||||
)
|
||||
d.exec_()
|
||||
if d.result() != d.Accepted:
|
||||
return
|
||||
|
||||
url_list = split_text_to_urls(d.get_urlstext())
|
||||
add_books = self.convert_urls_to_books(url_list)
|
||||
#print("add_books:%s"%add_books)
|
||||
#print("options:%s"%d.get_ffdl_options())
|
||||
|
||||
options = d.get_ffdl_options()
|
||||
options['version'] = self.version
|
||||
options['anthology_url']=anthology_url
|
||||
print(self.version)
|
||||
|
||||
self.prep_downloads( options, add_books, merge=merge )
|
||||
# AddNewDialog collects URLs, format and presents buttons.
|
||||
# add_new_dialog is modeless and reused, both for new stories
|
||||
# and anthologies, and for updating existing anthologies.
|
||||
self.add_new_dialog.show_dialog(url_list_text,
|
||||
self.prep_downloads,
|
||||
merge=merge,
|
||||
newmerge=True,
|
||||
extraoptions={'anthology_url':anthology_url})
|
||||
|
||||
def update_anthology(self):
|
||||
if not self.get_epubmerge_plugin():
|
||||
@@ -487,7 +461,6 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
if len(self.gui.library_view.get_selected_ids()) != 1:
|
||||
self.gui.status_bar.show_message(_('Can only update 1 anthology at a time'), 3000)
|
||||
return
|
||||
#print("update_existing()")
|
||||
|
||||
db = self.gui.current_db
|
||||
book_id = self.gui.library_view.get_selected_ids()[0]
|
||||
@@ -529,29 +502,34 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
url_list_text = "\n".join(url_list)
|
||||
|
||||
#print("urlmapfile:%s"%urlmapfile)
|
||||
|
||||
# AddNewDialog collects URLs, format and presents buttons.
|
||||
# add_new_dialog is modeless and reused, both for new stories
|
||||
# and anthologies, and for updating existing anthologies.
|
||||
self.add_new_dialog.show_dialog(url_list_text,
|
||||
self.prep_anthology_downloads,
|
||||
show=False,
|
||||
merge=True,
|
||||
newmerge=False,
|
||||
extrapayload=urlmapfile,
|
||||
extraoptions={'tdir':tdir,
|
||||
'mergebook':mergebook})
|
||||
# Need to use AddNewDialog modal here because it's an update
|
||||
# of an existing book. Don't want the user deleting it or
|
||||
# switching libraries on us.
|
||||
self.add_new_dialog.exec_()
|
||||
|
||||
# self.gui is the main calibre GUI. It acts as the gateway to access
|
||||
# all the elements of the calibre user interface, it should also be the
|
||||
# parent of the dialog
|
||||
# AddNewDialog just collects URLs, format and presents buttons.
|
||||
d = AddNewDialog(self.gui,
|
||||
prefs,
|
||||
self.qaction.icon(),
|
||||
url_list_text,
|
||||
merge=True,
|
||||
newmerge=False
|
||||
)
|
||||
d.exec_()
|
||||
if d.result() != d.Accepted:
|
||||
return
|
||||
|
||||
url_list = split_text_to_urls(d.get_urlstext())
|
||||
|
||||
update_books = self.convert_urls_to_books(url_list)
|
||||
def prep_anthology_downloads(self, options, update_books,
|
||||
merge=False, urlmapfile=None):
|
||||
|
||||
if isinstance(update_books,basestring):
|
||||
url_list = split_text_to_urls(update_books)
|
||||
update_books = self.convert_urls_to_books(url_list)
|
||||
|
||||
for j, book in enumerate(update_books):
|
||||
url = book['url']
|
||||
book['mergeorder'] = j
|
||||
book['listorder'] = j
|
||||
if url in urlmapfile:
|
||||
#print("found epub for %s"%url)
|
||||
book['epub_for_update']=urlmapfile[url]
|
||||
@@ -573,13 +551,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
print("Canceling anthology update due to removed stories.")
|
||||
return
|
||||
|
||||
options = d.get_ffdl_options()
|
||||
options['version'] = self.version
|
||||
options['tdir'] = tdir
|
||||
#options['collision'] = UPDATEALWAYS
|
||||
print(self.version)
|
||||
|
||||
options['mergebook'] = mergebook
|
||||
# Now that we've
|
||||
self.prep_downloads( options, update_books, merge=True )
|
||||
|
||||
def update_dialog(self):
|
||||
@@ -596,6 +568,9 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
book_list = map( partial(self.make_book_id_only), self.gui.library_view.get_selected_ids() )
|
||||
#book_ids = self.gui.library_view.get_selected_ids()
|
||||
|
||||
for j, book in enumerate(book_list):
|
||||
book['listorder'] = j
|
||||
|
||||
LoopProgressDialog(self.gui,
|
||||
book_list,
|
||||
partial(self.populate_book_from_calibre_id, db=self.gui.current_db),
|
||||
@@ -627,8 +602,6 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
# only if there's some good ones.
|
||||
if 0 < len(filter(lambda x : x['good'], update_books)):
|
||||
options = d.get_ffdl_options()
|
||||
options['version'] = self.version
|
||||
print(self.version)
|
||||
self.prep_downloads( options, update_books )
|
||||
|
||||
def get_urls_clip(self,storyurls=True):
|
||||
@@ -644,9 +617,16 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
# No need to do anything with perfs here, but we could.
|
||||
prefs
|
||||
|
||||
def prep_downloads(self, options, books, merge=False):
|
||||
def prep_downloads(self, options, books, merge=False, extrapayload=None):
|
||||
'''Fetch metadata for stories from servers, launch BG job when done.'''
|
||||
|
||||
if isinstance(books,basestring):
|
||||
url_list = split_text_to_urls(books)
|
||||
books = self.convert_urls_to_books(url_list)
|
||||
|
||||
options['version'] = self.version
|
||||
print(self.version)
|
||||
|
||||
#print("prep_downloads:%s"%books)
|
||||
|
||||
if 'tdir' not in options: # if merging an anthology, there's alread a tdir.
|
||||
@@ -683,12 +663,13 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
print("url:%s"%url)
|
||||
|
||||
if not merge: # skip reject list when merging.
|
||||
rejnote = rejecturllist.check(url)
|
||||
if rejnote:
|
||||
if rejecturllist.check(url):
|
||||
rejnote = rejecturllist.get_full_note(url)
|
||||
if question_dialog(self.gui, 'Reject URL?',
|
||||
'<p>Reject URL?</p>'+
|
||||
'<p>%s is on the Reject URL list:<br />"%s"</p>'%(url,rejnote)+
|
||||
"<p>Click 'No' to download anyway.</p>",
|
||||
'<h3>Reject URL?</h3>'+
|
||||
'<p><b>%s</b> is on your Reject URL list:</p><p>"<b>%s</b>"</p>'%(url,rejnote)+
|
||||
"<p>Click '<b>Yes</b>' to Reject.</p>"+
|
||||
"<p>Click '<b>No</b>' to download anyway.</p>",
|
||||
show_copy_button=False):
|
||||
book['comment'] = "Story on Reject URLs list (%s)."%rejnote
|
||||
book['good']=False
|
||||
@@ -697,9 +678,10 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
return
|
||||
else:
|
||||
if question_dialog(self.gui, 'Remove Reject URL?',
|
||||
"<p>Remove URL from Reject List?</p>"+
|
||||
'<p>%s is on the Reject URL list:<br />"%s"</p>'%(url,rejnote)+
|
||||
"<p>Click 'Yes' to remove it from the list and download,<br /> 'No' to download, but leave it on the Reject list.</p>",
|
||||
"<h3>Remove URL from Reject List?</h3>"+
|
||||
'<p><b>%s</b> is on your Reject URL list:</p><p>"<b>%s</b>"</p>'%(url,rejnote)+
|
||||
"<p>Click '<b>Yes</b>' to remove it from the list,</p>"+
|
||||
"<p>Click '<b>No</b>' to leave it on the list.</p>",
|
||||
show_copy_button=False):
|
||||
rejecturllist.remove(url)
|
||||
|
||||
@@ -749,6 +731,31 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
# let other exceptions percolate up.
|
||||
story = adapter.getStoryMetadataOnly()
|
||||
|
||||
series = story.getMetadata('series')
|
||||
if not merge and series and prefs['checkforseriesurlid']:
|
||||
# try to find *series anthology* by *seriesUrl* identifier url or uri first.
|
||||
searchstr = 'identifiers:"~ur(i|l):=%s"'%story.getMetadata('seriesUrl').replace(":","|")
|
||||
identicalbooks = db.search_getting_ids(searchstr, None)
|
||||
# print("searchstr:%s"%searchstr)
|
||||
# print("identicalbooks:%s"%identicalbooks)
|
||||
if len(identicalbooks) > 0 and question_dialog(self.gui, 'Skip Story?',
|
||||
'<h3>Skip Anthology Story?</h3>'+
|
||||
'<p>"<b>%s</b>" is in series "<b><a href="%s">%s</a></b>" that you have an anthology book for.</p>'%
|
||||
(story.getMetadata('title'),story.getMetadata('seriesUrl'),series[:series.index(' [')])+
|
||||
"<p>Click '<b>Yes</b>' to Skip.</p>"+
|
||||
"<p>Click '<b>No</b>' to download anyway.</p>",
|
||||
show_copy_button=False):
|
||||
book['comment'] = "Story in Series Anthology(%s)."%series
|
||||
book['title'] = story.getMetadata('title')
|
||||
book['author'] = [story.getMetadata('author')]
|
||||
book['good']=False
|
||||
book['icon']='rotate-right.png'
|
||||
book['status'] = 'Skipped'
|
||||
return
|
||||
|
||||
|
||||
################################################################################################################################################33
|
||||
|
||||
# set PI version instead of default.
|
||||
if 'version' in options:
|
||||
story.setMetadata('version',options['version'])
|
||||
@@ -872,8 +879,9 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
raise NotGoingToDownload("Not Overwriting, web site is not newer.",'edit-undo.png')
|
||||
|
||||
# For update, provide a tmp file copy of the existing epub so
|
||||
# it can't change underneath us.
|
||||
if collision in (UPDATE,UPDATEALWAYS) and \
|
||||
# it can't change underneath us. Now also overwrite for logpage preserve.
|
||||
if collision in (UPDATE,UPDATEALWAYS,OVERWRITE,OVERWRITEALWAYS) and \
|
||||
fileform == 'epub' and \
|
||||
db.has_format(book['calibre_id'],'EPUB',index_is_id=True):
|
||||
tmp = PersistentTemporaryFile(prefix='old-%s-'%book['calibre_id'],
|
||||
suffix='.epub',
|
||||
@@ -930,18 +938,28 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
break
|
||||
else:
|
||||
## No good stories to try to download, go straight to
|
||||
## list.
|
||||
d = DisplayStoryListDialog(self.gui,
|
||||
'Nothing to Download',
|
||||
prefs,
|
||||
self.qaction.icon(),
|
||||
book_list,
|
||||
label_text='None of the URLs/stories given can be/need to be downloaded.'
|
||||
)
|
||||
d.exec_()
|
||||
|
||||
self.update_error_column(book_list,options)
|
||||
## updating error col.
|
||||
msg = '''
|
||||
<p>None of the <b>%d</b> URLs/stories given can be/need to be downloaded.</p>
|
||||
<p>See log for details.</p>
|
||||
<p>Proceed with updating your library(Error Column, if configured)?</p>
|
||||
'''%len(book_list)
|
||||
|
||||
htmllog='<html><body><table border="1"><tr><th>Status</th><th>Title</th><th>Author</th><th>Comment</th><th>URL</th></tr>'
|
||||
for book in book_list:
|
||||
if 'status' in book:
|
||||
status = book['status']
|
||||
else:
|
||||
status = 'Bad'
|
||||
htmllog = htmllog + '<tr><td>' + '</td><td>'.join([escapehtml(status),escapehtml(book['title']),escapehtml(", ".join(book['author'])),escapehtml(book['comment']),book['url']]) + '</td></tr>'
|
||||
|
||||
htmllog = htmllog + '</table></body></html>'
|
||||
|
||||
payload = ([], book_list, options)
|
||||
self.gui.proceed_question(self.update_error_column,
|
||||
payload, htmllog,
|
||||
'FFDL log', 'FFDL download ended', msg,
|
||||
show_copy_button=False)
|
||||
return
|
||||
|
||||
func = 'arbitrary_n'
|
||||
@@ -961,6 +979,18 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
'collision':ADDNEW,
|
||||
'updatemeta':True,
|
||||
'updateepubcover':True}):
|
||||
custom_columns = self.gui.library_view.model().custom_columns
|
||||
if book['calibre_id'] and prefs['errorcol'] != '' and prefs['errorcol'] in custom_columns:
|
||||
label = custom_columns[prefs['errorcol']]['label']
|
||||
if not book['good']:
|
||||
print("record/update error message column %s %s"%(book['title'],book['url']))
|
||||
db.set_custom(book['calibre_id'], book['comment'], label=label, commit=True) # book['comment']
|
||||
else:
|
||||
db.set_custom(book['calibre_id'], '', label=label, commit=True) # book['comment']
|
||||
|
||||
if not book['good']:
|
||||
return # only update errorcol on error.
|
||||
|
||||
print("add/update %s %s"%(book['title'],book['url']))
|
||||
mi = self.make_mi_from_book(book)
|
||||
|
||||
@@ -979,8 +1009,10 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
add_ids = [ x['calibre_id'] for x in add_list ]
|
||||
update_list = filter(lambda x : x['good'] and not x['added'], book_list)
|
||||
update_ids = [ x['calibre_id'] for x in update_list ]
|
||||
all_ids = add_ids
|
||||
all_ids.extend(update_ids)
|
||||
all_ids = add_ids + update_ids
|
||||
|
||||
failed_list = filter(lambda x : not x['good'] , book_list)
|
||||
failed_ids = [ x['calibre_id'] for x in failed_list ]
|
||||
|
||||
if options['collision'] != CALIBREONLY and \
|
||||
(prefs['addtolists'] or prefs['addtoreadlists']):
|
||||
@@ -999,19 +1031,26 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
|
||||
if self.gui.cover_flow:
|
||||
self.gui.cover_flow.dataChanged()
|
||||
|
||||
|
||||
if showlist: # don't use with anthology
|
||||
db = self.gui.current_db
|
||||
marked_ids = dict()
|
||||
marked_text = "ffdl_success"
|
||||
for index, book_id in enumerate(all_ids):
|
||||
marked_ids[book_id] = '%s_%04d' % (marked_text, index)
|
||||
for index, book_id in enumerate(failed_ids):
|
||||
marked_ids[book_id] = 'ffdl_failed_%04d' % index
|
||||
# Mark the results in our database
|
||||
db.set_marked_ids(marked_ids)
|
||||
|
||||
if prefs['showmarked']: # show add/update
|
||||
# Search to display the list contents
|
||||
self.gui.search.set_search_string('marked:' + marked_text)
|
||||
# Sort by our marked column to display the books in order
|
||||
self.gui.library_view.sort_by_named_field('marked', True)
|
||||
|
||||
self.gui.status_bar.show_message(_('Finished Adding/Updating %d books.'%(len(update_list) + len(add_list))), 3000)
|
||||
|
||||
if showlist and (len(update_list) + len(add_list) != len(book_list)):
|
||||
d = DisplayStoryListDialog(self.gui,
|
||||
'Updates completed, final status',
|
||||
prefs,
|
||||
self.qaction.icon(),
|
||||
book_list,
|
||||
label_text='Stories have be added or updated in Calibre, some had additional problems.'
|
||||
)
|
||||
d.exec_()
|
||||
|
||||
print("all done, remove temp dir.")
|
||||
remove_dir(options['tdir'])
|
||||
|
||||
@@ -1030,6 +1069,8 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
book_list = job.result
|
||||
good_list = filter(lambda x : x['good'], book_list)
|
||||
bad_list = filter(lambda x : not x['good'], book_list)
|
||||
good_list = sorted(good_list,key=lambda x : x['listorder'])
|
||||
bad_list = sorted(bad_list,key=lambda x : x['listorder'])
|
||||
#print("book_list:%s"%book_list)
|
||||
payload = (good_list, bad_list, options)
|
||||
|
||||
@@ -1051,7 +1092,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
msg = msg + '<p>Proceed with updating this anthology and your library?</p>'
|
||||
|
||||
htmllog='<html><body><table border="1"><tr><th>Status</th><th>Title</th><th>Author</th><th>Comment</th><th>URL</th></tr>'
|
||||
for book in sorted(good_list+bad_list,key=lambda x : x['mergeorder']):
|
||||
for book in sorted(good_list+bad_list,key=lambda x : x['listorder']):
|
||||
if 'status' in book:
|
||||
status = book['status']
|
||||
else:
|
||||
@@ -1106,10 +1147,10 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
(good_list,bad_list,options) = payload
|
||||
total_good = len(good_list)
|
||||
|
||||
print("merge titles:\n%s"%"\n".join([ "%s %s"%(x['title'],x['mergeorder']) for x in good_list ]))
|
||||
print("merge titles:\n%s"%"\n".join([ "%s %s"%(x['title'],x['listorder']) for x in good_list ]))
|
||||
|
||||
good_list = sorted(good_list,key=lambda x : x['mergeorder'])
|
||||
bad_list = sorted(bad_list,key=lambda x : x['mergeorder'])
|
||||
good_list = sorted(good_list,key=lambda x : x['listorder'])
|
||||
bad_list = sorted(bad_list,key=lambda x : x['listorder'])
|
||||
|
||||
self.gui.status_bar.show_message(_('Merging %s books.'%total_good))
|
||||
|
||||
@@ -1149,26 +1190,23 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
def do_download_list_update(self, payload):
|
||||
|
||||
(good_list,bad_list,options) = payload
|
||||
total_good = len(good_list)
|
||||
good_list = sorted(good_list,key=lambda x : x['listorder'])
|
||||
bad_list = sorted(bad_list,key=lambda x : x['listorder'])
|
||||
|
||||
self.gui.status_bar.show_message(_('Adding/Updating %s books.'%total_good))
|
||||
self.gui.status_bar.show_message(_('FFDL Adding/Updating books.'))
|
||||
|
||||
if total_good > 0:
|
||||
if good_list or (bad_list and prefs['errorcol'] != '' and prefs['errorcol'] in self.gui.library_view.model().custom_columns):
|
||||
LoopProgressDialog(self.gui,
|
||||
good_list,
|
||||
good_list+bad_list,
|
||||
partial(self.update_books_loop, options=options, db=self.gui.current_db),
|
||||
partial(self.update_books_finish, options=options),
|
||||
init_label="Updating calibre for FanFiction stories...",
|
||||
win_title="Update calibre for FanFiction stories",
|
||||
status_prefix="Updated")
|
||||
|
||||
total_bad = len(bad_list)
|
||||
|
||||
if total_bad > 0:
|
||||
self.update_error_column(bad_list,options)
|
||||
|
||||
def update_error_column(self,book_list,options):
|
||||
def update_error_column(self,payload):
|
||||
'''Update custom error column if configured.'''
|
||||
(empty_list,book_list,options)=payload
|
||||
custom_columns = self.gui.library_view.model().custom_columns
|
||||
if prefs['errorcol'] != '' and prefs['errorcol'] in custom_columns:
|
||||
self.previous = self.gui.library_view.currentIndex() # used by update_books_finish.
|
||||
@@ -1328,9 +1366,13 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
else:
|
||||
coldef = custom_columns[custcol]
|
||||
label = coldef['label']
|
||||
|
||||
|
||||
if flag == 'r' or book['added']: # flag 'n' isn't actually needed--*always* set if configured and new book.
|
||||
db.set_custom(book_id, book['all_metadata'][meta], label=label, commit=False)
|
||||
if coldef['datatype'] in ('int','float'): # for favs, etc--site specific metadata.
|
||||
val = unicode(book['all_metadata'][meta]).replace(",","")
|
||||
else:
|
||||
val = book['all_metadata'][meta]
|
||||
db.set_custom(book_id, val, label=label, commit=False)
|
||||
|
||||
if flag == 'a':
|
||||
vallist = []
|
||||
@@ -1391,11 +1433,6 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
realmi = db.get_metadata(book_id, index_is_id=True)
|
||||
gc_plugin.generate_cover_for_book(realmi,saved_setting_name=setting_name)
|
||||
|
||||
## if error column set.
|
||||
if prefs['errorcol'] != '' and prefs['errorcol'] in custom_columns:
|
||||
label = custom_columns[prefs['errorcol']]['label']
|
||||
db.set_custom(book['calibre_id'], '', label=label, commit=True) # book['comment']
|
||||
|
||||
def get_clean_reading_lists(self,lists):
|
||||
if lists == None or lists.strip() == "" :
|
||||
return []
|
||||
@@ -1488,15 +1525,15 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
book['good'] = False
|
||||
book['comment'] = "Same story already included."
|
||||
uniqueurls.add(book['url'])
|
||||
book['mergeorder']=i # BG d/l jobs don't come back in order.
|
||||
# Didn't matter until anthologies.
|
||||
book['listorder']=i # BG d/l jobs don't come back in order.
|
||||
# Didn't matter until anthologies & 'marked' successes
|
||||
books.append(book)
|
||||
return books
|
||||
|
||||
def convert_url_to_book(self, url):
|
||||
book = self.make_book()
|
||||
# look here for [\d,\d] at end of url, and remove?
|
||||
mc = re.match(r"^(?P<url>.*?)(?:\[(?P<begin>\d+)?(?P<comma>,)?(?P<end>\d+)?\])?$",url)
|
||||
mc = re.match(r"^(?P<url>.*?)(?:\[(?P<begin>\d+)?(?P<comma>[,-])?(?P<end>\d+)?\])?$",url)
|
||||
#print("url:(%s) begin:(%s) end:(%s)"%(mc.group('url'),mc.group('begin'),mc.group('end')))
|
||||
url = mc.group('url')
|
||||
book['begin'] = mc.group('begin')
|
||||
|
||||
@@ -11,7 +11,7 @@ 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.config import (prefs)
|
||||
from calibre_plugins.fanfictiondownloader_plugin.prefs import (prefs)
|
||||
|
||||
def get_ffdl_personalini():
|
||||
if prefs['includeimages']:
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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'] = {}
|
||||
|
||||
# This is where all preferences for this plugin *were* stored
|
||||
# Remember that this name (i.e. plugins/fanfictiondownloader_plugin) is also
|
||||
# in a global namespace, so make it as unique as possible.
|
||||
# You should always prefix your config file name with plugins/,
|
||||
# so as to ensure you dont accidentally clobber a calibre config file
|
||||
old_prefs = JSONConfig('plugins/fanfictiondownloader_plugin')
|
||||
|
||||
def set_library_config(library_config,db):
|
||||
db.prefs.set_namespaced(PREFS_NAMESPACE,
|
||||
PREFS_KEY_SETTINGS,
|
||||
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,db)
|
||||
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
|
||||
|
||||
# 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()
|
||||
|
||||
+17
-7
@@ -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
|
||||
@@ -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
|
||||
|
||||
+37
-13
@@ -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,
|
||||
@@ -79,11 +94,14 @@ def main():
|
||||
parser.add_option("-l", "--list",
|
||||
action="store_true", dest="list",
|
||||
help="Get list of valid story URLs from page given.",)
|
||||
parser.add_option("-n", "--normalize-list",
|
||||
action="store_true", dest="normalize",default=False,
|
||||
help="Get list of valid story URLs from page given, but normalized to standard forms.",)
|
||||
parser.add_option("-d", "--debug",
|
||||
action="store_true", dest="debug",
|
||||
help="Show debug output while downloading.",)
|
||||
|
||||
(options, args) = parser.parse_args()
|
||||
(options, args) = parser.parse_args(argv)
|
||||
|
||||
if not options.debug:
|
||||
logger = logging.getLogger("fanficdownloader")
|
||||
@@ -107,12 +125,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"):
|
||||
@@ -148,8 +172,8 @@ def main():
|
||||
(var,val) = opt.split('=')
|
||||
configuration.set("overrides",var,val)
|
||||
|
||||
if options.list:
|
||||
retlist = get_urls_from_page(args[0], configuration)
|
||||
if options.list or options.normalize:
|
||||
retlist = get_urls_from_page(args[0], configuration, normalize=options.normalize)
|
||||
print "\n".join(retlist)
|
||||
|
||||
return
|
||||
@@ -240,5 +264,5 @@ def main():
|
||||
if __name__ == "__main__":
|
||||
#import time
|
||||
#start = time.time()
|
||||
main()
|
||||
main(sys.argv[1:])
|
||||
#print("Total time seconds:%f"%(time.time()-start))
|
||||
|
||||
@@ -153,7 +153,7 @@ def getAdapter(config,url):
|
||||
|
||||
#logger.debug("trying url:"+url)
|
||||
(cls,fixedurl) = getClassFor(url)
|
||||
logger.debug("fixedurl:"+fixedurl)
|
||||
#logger.debug("fixedurl:"+fixedurl)
|
||||
if cls:
|
||||
adapter = cls(config,fixedurl) # raises InvalidStoryURL
|
||||
return adapter
|
||||
|
||||
@@ -195,6 +195,7 @@ class AdAstraFanficComSiteAdapter(BaseSiteAdapter):
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -295,6 +295,7 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
series_url = 'http://'+self.host+'/fanfic/'+b['href']
|
||||
series_index = int(a.text.split(' ')[1])
|
||||
self.setSeries(series_name, series_index)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
|
||||
except:
|
||||
# I find it hard to care if the series parsing fails
|
||||
|
||||
@@ -311,6 +311,7 @@ class BloodTiesFansComAdapter(BaseSiteAdapter): # XXX
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -284,6 +284,7 @@ class CastleFansOrgAdapter(BaseSiteAdapter): # XXX
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -212,6 +212,7 @@ class ChaosSycophantHexComAdapter(BaseSiteAdapter):
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -252,6 +252,7 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if ('viewstory.php?sid='+self.story.getMetadata('storyId')) in a['href']:
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -218,6 +218,7 @@ class DestinysGatewayComAdapter(BaseSiteAdapter):
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -81,6 +81,44 @@ class DokugaComAdapter(BaseSiteAdapter):
|
||||
def getSiteURLPattern(self):
|
||||
return r"http://"+self.getSiteDomain()+"/(fanfiction|spark)?/story/\d+/?\d+?$"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
if 'The author has disabled anonymous viewing for this story.' in data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def performLogin(self, url,soup):
|
||||
params = {}
|
||||
|
||||
if self.password:
|
||||
params['username'] = self.username
|
||||
params['passwd'] = self.password
|
||||
else:
|
||||
params['username'] = self.getConfig("username")
|
||||
params['passwd'] = self.getConfig("password")
|
||||
params['Submit'] = 'Submit'
|
||||
|
||||
# copy all hidden input tags to pick up appropriate tokens.
|
||||
for tag in soup.findAll('input',{'type':'hidden'}):
|
||||
params[tag['name']] = tag['value']
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/fanfiction'
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['username']))
|
||||
|
||||
d = self._postUrl(loginUrl, params)
|
||||
|
||||
if "Your session has expired. Please log in again." in d:
|
||||
d = self._postUrl(loginUrl, params)
|
||||
|
||||
if "Logout" not in d : #Member Account
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['username']))
|
||||
raise exceptions.FailedToLogin(url,params['username'])
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
@@ -97,12 +135,18 @@ class DokugaComAdapter(BaseSiteAdapter):
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = bs.BeautifulSoup(data)
|
||||
|
||||
if self.needToLoginCheck(data):
|
||||
# need to log in for this one.
|
||||
self.performLogin(url,soup)
|
||||
data = self._fetchUrl(url)
|
||||
soup = bs.BeautifulSoup(data)
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = bs.BeautifulSoup(data)
|
||||
# print data
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
@@ -218,10 +262,6 @@ class DokugaComAdapter(BaseSiteAdapter):
|
||||
a=div.text.split('Words ')
|
||||
if len(a)==2: self.story.setMetadata('numWords', a[1])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
|
||||
@@ -273,6 +273,7 @@ class DracoAndGinnyComAdapter(BaseSiteAdapter):
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -274,6 +274,7 @@ class DramioneOrgAdapter(BaseSiteAdapter):
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -195,6 +195,7 @@ class EfictionEstelielDeAdapter(BaseSiteAdapter):
|
||||
name=seriessoup.find('div', {'id' : 'pagetitle'})
|
||||
name.find('a').extract()
|
||||
self.setSeries(name.text.split(' by[')[0], i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
i=0
|
||||
break
|
||||
i+=1
|
||||
|
||||
@@ -280,6 +280,7 @@ class EFPFanFicNet(BaseSiteAdapter):
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId'))+'&i=1':
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -230,6 +230,7 @@ class ErosnSapphoSycophantHexComAdapter(BaseSiteAdapter):
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -132,9 +132,10 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
## For 1, use the second link.
|
||||
## For 2, fetch the crossover page and pull the two categories from there.
|
||||
|
||||
categories = soup.findAll('a',{'class':'xcontrast_txt'})
|
||||
categories = soup.find('div',{'id':'pre_story_links'}).findAll('a',{'class':'xcontrast_txt'})
|
||||
#print("xcontrast_txt a:%s"%categories)
|
||||
if len(categories) > 1:
|
||||
self.story.addToList('category',stripHTML(categories[-1]))
|
||||
self.story.addToList('category',stripHTML(categories[1]))
|
||||
elif 'Crossover' in categories[0]['href']:
|
||||
caturl = "http://%s%s"%(self.getSiteDomain(),categories[0]['href'])
|
||||
catsoup = bs.BeautifulSoup(self._fetchUrl(caturl))
|
||||
@@ -158,12 +159,17 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
if summarydiv:
|
||||
self.setDescription(url,stripHTML(summarydiv))
|
||||
|
||||
|
||||
metatext = stripHTML(gui_table1i.find('div', {'style':'color:gray;'})).replace('Hurt/Comfort','Hurt-Comfort')
|
||||
|
||||
grayspan = gui_table1i.find('span', {'class':'xgray xcontrast_txt'})
|
||||
# for b in grayspan.findAll('button'):
|
||||
# b.extract()
|
||||
metatext = stripHTML(grayspan).replace('Hurt/Comfort','Hurt-Comfort')
|
||||
#logger.debug("metatext:(%s)"%metatext)
|
||||
metalist = metatext.split(" - ")
|
||||
#logger.debug("metatext:(%s)"%metalist)
|
||||
#logger.debug("metalist:(%s)"%metalist)
|
||||
|
||||
# Rated: Fiction K - English - Words: 158,078 - Published: 02-04-11
|
||||
# Rated: Fiction T - English - Adventure/Sci-Fi - Naruto U. - Chapters: 22 - Words: 114,414 - Reviews: 395 - Favs: 779 - Follows: 835 - Updated: 03-21-13 - Published: 04-28-12 - id: 8067258
|
||||
|
||||
# rating is obtained above more robustly.
|
||||
if metalist[0].startswith('Rated:'):
|
||||
|
||||
@@ -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,15 +179,11 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
self.setCoverImage(self.url,coverurl)
|
||||
|
||||
self.setDescription(self.url,soup.find("div", {"class":"description"}))
|
||||
# if "description" in storyMetadata and storyMetadata["description"]:
|
||||
# # 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 />'))
|
||||
# elif "short_description" in storyMetadata and storyMetadata["short_description"]:
|
||||
# self.setDescription(self.url,
|
||||
# bbcodeparser().parse(storyMetadata["short_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
|
||||
@@ -198,7 +192,7 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
rawDateUpdated = storyMetadata["date_modified"]
|
||||
self.story.setMetadata("dateUpdated", datetime.fromtimestamp(rawDateUpdated))
|
||||
|
||||
chars = soup.find("div", {"class":"story"})
|
||||
chars = soup.find("div", {"class":"inner_data"})
|
||||
# fimfic stopped putting the char name on or around the char
|
||||
# icon now for some reason. Pull it from the image name with
|
||||
# some heuristics.
|
||||
|
||||
@@ -187,6 +187,7 @@ class FineStoriesComAdapter(BaseSiteAdapter):
|
||||
a = lc4.find('a', href=re.compile(r"/library/show_series.php\?id=\d+"))
|
||||
i = a.parent.text.split('(')[1].split(')')[0]
|
||||
self.setSeries(a.text, i)
|
||||
self.story.setMetadata('seriesUrl','http://'+self.host+a['href'])
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
|
||||
@@ -275,6 +275,7 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -206,6 +206,7 @@ class HLFictionNetAdapter(BaseSiteAdapter):
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if ('viewstory.php?sid='+self.story.getMetadata('storyId')) in a['href']:
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -193,6 +193,7 @@ class HPFanficArchiveComAdapter(BaseSiteAdapter):
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -264,6 +264,7 @@ class ImagineEFicComAdapter(BaseSiteAdapter):
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -234,6 +234,7 @@ class JLAUnlimitedComAdapter(BaseSiteAdapter):
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -285,6 +285,7 @@ class KSArchiveComAdapter(BaseSiteAdapter): # XXX
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -225,6 +225,7 @@ class LibraryOfMoriaComAdapter(BaseSiteAdapter):
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -212,6 +212,7 @@ class LumosSycophantHexComAdapter(BaseSiteAdapter):
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -268,6 +268,7 @@ class MerlinFicDtwinsCoUk(BaseSiteAdapter):
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -260,6 +260,7 @@ class MidnightwhispersCaAdapter(BaseSiteAdapter): # XXX
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -306,6 +306,7 @@ class MuggleNetComAdapter(BaseSiteAdapter): # XXX
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -163,6 +163,7 @@ class NationalLibraryNetAdapter(BaseSiteAdapter):
|
||||
|
||||
if 'Series' in label:
|
||||
self.setSeries(stripHTML(value.nextSibling), value.nextSibling.nextSibling.string[2:])
|
||||
self.story.setMetadata('seriesUrl','http://'+self.host+'/'+value.nextSibling['href'])
|
||||
|
||||
asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')))
|
||||
story=asoup.find('a', href=re.compile(r'viewstory.php\?storyid='+self.story.getMetadata('storyId')))
|
||||
|
||||
@@ -164,6 +164,7 @@ class NCISFicComAdapter(BaseSiteAdapter):
|
||||
if 'Series' in label:
|
||||
if "No Series" not in value.nextSibling.string:
|
||||
self.setSeries(stripHTML(value.nextSibling), value.nextSibling.nextSibling.string[2:])
|
||||
self.story.setMetadata('seriesUrl','http://'+self.host+'/'+value.nextSibling['href'])
|
||||
|
||||
asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')))
|
||||
story=asoup.find('a', href=re.compile(r'viewstory.php\?storyid='+self.story.getMetadata('storyId')))
|
||||
|
||||
@@ -193,7 +193,7 @@ class NCISFictionNetAdapter(BaseSiteAdapter):
|
||||
series_name = a.find('a').string
|
||||
i = a.text.split('#')[1]
|
||||
self.setSeries(series_name, i)
|
||||
|
||||
self.story.setMetadata('seriesUrl','http://'+self.host+'/'+a.find('a')['href'])
|
||||
except:
|
||||
# I find it hard to care if the series parsing fails
|
||||
pass
|
||||
|
||||
@@ -264,6 +264,7 @@ class NfaCommunityComAdapter(BaseSiteAdapter): # XXX
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -244,6 +244,7 @@ class OneDirectionFanfictionComAdapter(BaseSiteAdapter):
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -275,6 +275,7 @@ class PommeDeSangComAdapter(BaseSiteAdapter):
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if ('viewstory.php?sid='+self.story.getMetadata('storyId')) in a['href']:
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -225,6 +225,7 @@ class PonyFictionArchiveNetAdapter(BaseSiteAdapter):
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -182,6 +182,7 @@ class PotionsAndSnitchesNetSiteAdapter(BaseSiteAdapter):
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -228,6 +228,7 @@ class PretenderCenterComAdapter(BaseSiteAdapter):
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -193,6 +193,7 @@ class PrisonBreakFicNetAdapter(BaseSiteAdapter):
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -223,6 +223,7 @@ class PsychFicComAdapter(BaseSiteAdapter):
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -236,6 +236,7 @@ class QafFicComAdapter(BaseSiteAdapter):
|
||||
name=seriessoup.find('div', {'id' : 'pagetitle'})
|
||||
name.find('a').extract()
|
||||
self.setSeries(name.text.split(' by[')[0], i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
i=0
|
||||
break
|
||||
i+=1
|
||||
|
||||
@@ -207,6 +207,7 @@ class SamDeanArchiveNuAdapter(BaseSiteAdapter):
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -222,6 +222,7 @@ class ScarvesAndCoffeeNetAdapter(BaseSiteAdapter):
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -233,6 +233,7 @@ class SG1HeliopolisComAdapter(BaseSiteAdapter):
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -226,6 +226,7 @@ class SinfulDesireOrgAdapter(BaseSiteAdapter):
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -215,6 +215,7 @@ class SiyeCoUkAdapter(BaseSiteAdapter): # XXX
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -212,6 +212,7 @@ class SquidgeOrgPejaAdapter(BaseSiteAdapter):
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -204,6 +204,7 @@ class StargateAtlantisOrgAdapter(BaseSiteAdapter):
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -249,6 +249,7 @@ class SVUFictionComAdapter(BaseSiteAdapter):
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -220,6 +220,7 @@ class TenhawkPresentsComSiteAdapter(BaseSiteAdapter):
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -101,8 +101,10 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
|
||||
}
|
||||
self.story.setMetadata('language',langs[idnum%len(langs)])
|
||||
self.setSeries('The Great Test',idnum)
|
||||
self.story.setMetadata('seriesUrl','http://test1.com?seriesid=1')
|
||||
if idnum == 0:
|
||||
self.setSeries("A Nook Hyphen Test "+self.story.getMetadata('dateCreated'),idnum)
|
||||
self.story.setMetadata('seriesUrl','http://test1.com?seriesid=0')
|
||||
|
||||
self.story.setMetadata('rating','Tweenie')
|
||||
|
||||
@@ -126,21 +128,36 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
|
||||
self.story.addToList('warnings','Swearing')
|
||||
self.story.addToList('warnings','Violence')
|
||||
|
||||
self.story.addToList('category','Harry Potter')
|
||||
self.story.addToList('category','Furbie')
|
||||
self.story.addToList('category','Crossover')
|
||||
self.story.addToList('category',u'Puella Magi Madoka Magica/魔法少女まどか★マギカ')
|
||||
self.story.addToList('category',u'Magical Girl Lyrical Nanoha')
|
||||
|
||||
if self.story.getMetadata('storyId') == '80':
|
||||
self.story.addToList('category',u'Rizzoli & Isles')
|
||||
self.story.addToList('characters','J. Rizzoli')
|
||||
elif self.story.getMetadata('storyId') == '81':
|
||||
self.story.addToList('category',u'Pitch Perfect')
|
||||
self.story.addToList('characters','Chloe B.')
|
||||
elif self.story.getMetadata('storyId') == '83':
|
||||
self.story.addToList('category',u'Rizzoli & Isles')
|
||||
self.story.addToList('characters','J. Rizzoli')
|
||||
self.story.addToList('category',u'Pitch Perfect')
|
||||
self.story.addToList('characters','Chloe B.')
|
||||
elif self.story.getMetadata('storyId') == '82':
|
||||
self.story.addToList('characters','Henry (Once Upon a Time)')
|
||||
self.story.addToList('category',u'Once Upon a Time (TV)')
|
||||
else:
|
||||
self.story.addToList('category','Harry Potter')
|
||||
self.story.addToList('category','Furbie')
|
||||
self.story.addToList('category','Crossover')
|
||||
self.story.addToList('category',u'Puella Magi Madoka Magica/魔法少女まどか★マギカ')
|
||||
self.story.addToList('category',u'Magical Girl Lyrical Nanoha')
|
||||
self.story.addToList('category',u'Once Upon a Time (TV)')
|
||||
self.story.addToList('characters','Bob Smith')
|
||||
self.story.addToList('characters','George Johnson')
|
||||
self.story.addToList('characters','Fred Smythe')
|
||||
|
||||
self.story.addToList('genre','Fantasy')
|
||||
self.story.addToList('genre','Comedy')
|
||||
self.story.addToList('genre','Sci-Fi')
|
||||
self.story.addToList('genre','Noir')
|
||||
|
||||
self.story.addToList('characters','Bob Smith')
|
||||
self.story.addToList('characters','George Johnson')
|
||||
self.story.addToList('characters','Fred Smythe')
|
||||
|
||||
|
||||
self.story.addToList('listX','xVal1')
|
||||
self.story.addToList('listX','xVal2')
|
||||
self.story.addToList('listX','xVal3')
|
||||
|
||||
@@ -189,6 +189,7 @@ class TheAlphaGateComAdapter(BaseSiteAdapter):
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -283,6 +283,7 @@ class TheHookupZoneNetAdapter(BaseSiteAdapter): # XXX
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -263,6 +263,7 @@ class TheQuidditchPitchOrgAdapter(BaseSiteAdapter): # XXX
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -227,6 +227,7 @@ class TheWritersCoffeeShopComSiteAdapter(BaseSiteAdapter):
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -163,17 +163,44 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
|
||||
infodata = self._fetchUrl(infourl)
|
||||
infosoup = bs.BeautifulSoup(infodata)
|
||||
|
||||
for a in infosoup.findAll('a',href=re.compile(r"^/Author-\d+")):
|
||||
self.story.addToList('authorId',a['href'].split('/')[1].split('-')[1])
|
||||
self.story.addToList('authorUrl','http://'+self.host+a['href'].replace("/Author-","/AuthorStories-"))
|
||||
self.story.addToList('author',stripHTML(a))
|
||||
# for a in infosoup.findAll('a',href=re.compile(r"^/Author-\d+")):
|
||||
# self.story.addToList('authorId',a['href'].split('/')[1].split('-')[1])
|
||||
# self.story.addToList('authorUrl','http://'+self.host+a['href'].replace("/Author-","/AuthorStories-"))
|
||||
# self.story.addToList('author',stripHTML(a))
|
||||
|
||||
# second verticaltable is the chapter list.
|
||||
table = infosoup.findAll('table',{'class':'verticaltable'})[1]
|
||||
for a in table.findAll('a',href=re.compile(r"^/Story-"+self.story.getMetadata('storyId'))):
|
||||
autha = a.findNext('a',href=re.compile(r"^/Author-\d+"))
|
||||
self.story.addToList('authorId',autha['href'].split('/')[1].split('-')[1])
|
||||
self.story.addToList('authorUrl','http://'+self.host+autha['href'].replace("/Author-","/AuthorStories-"))
|
||||
self.story.addToList('author',stripHTML(autha))
|
||||
# include leading number to match 1. ... 2. ...
|
||||
self.chapterUrls.append(("%d. %s by %s"%(len(self.chapterUrls)+1,
|
||||
stripHTML(a),
|
||||
stripHTML(autha)),'http://'+self.host+a['href']))
|
||||
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(url)
|
||||
else:
|
||||
raise e
|
||||
else: # single author:
|
||||
# Find the chapter selector
|
||||
select = soup.find('select', { 'name' : 'chapnav' } )
|
||||
|
||||
if select is None:
|
||||
# no selector found, so it's a one-chapter story.
|
||||
self.chapterUrls.append((self.story.getMetadata('title'),url))
|
||||
else:
|
||||
allOptions = select.findAll('option')
|
||||
for o in allOptions:
|
||||
url = "http://"+self.host+o['value']
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
self.chapterUrls.append((stripHTML(o),url))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
try:
|
||||
# going to pull part of the meta data from *primary* author list page.
|
||||
logger.debug("**AUTHOR** URL: "+authorurl)
|
||||
@@ -235,29 +262,12 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
|
||||
if BtVS:
|
||||
self.story.addToList('category','Buffy: The Vampire Slayer')
|
||||
|
||||
# Find the chapter selector
|
||||
select = soup.find('select', { 'name' : 'chapnav' } )
|
||||
|
||||
if select is None:
|
||||
# no selector found, so it's a one-chapter story.
|
||||
self.chapterUrls.append((self.story.getMetadata('title'),url))
|
||||
else:
|
||||
allOptions = select.findAll('option')
|
||||
for o in allOptions:
|
||||
url = "http://"+self.host+o['value']
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
self.chapterUrls.append((stripHTML(o),url))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
pseries = soup.find('p', {'style':'margin-top:0px'})
|
||||
m = re.match('This story is No\. (?P<num>\d+) in the series "(?P<series>.+)"\.',
|
||||
pseries.text)
|
||||
if m:
|
||||
self.setSeries(m.group('series'),m.group('num'))
|
||||
|
||||
return
|
||||
|
||||
self.story.setMetadata('seriesUrl',"http://"+self.host+pseries.find('a')['href'])
|
||||
|
||||
def getChapterText(self, url):
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
@@ -163,6 +163,7 @@ class TwilightArchivesComAdapter(BaseSiteAdapter):
|
||||
for a in storyas:
|
||||
if a['href'] == ('/read/'+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -217,6 +217,7 @@ class TwilightedNetSiteAdapter(BaseSiteAdapter):
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -246,6 +246,7 @@ class TwiwriteNetSiteAdapter(BaseSiteAdapter):
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -206,6 +206,7 @@ class WalkingThePlankOrgAdapter(BaseSiteAdapter):
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -204,6 +204,7 @@ class WhoficComSiteAdapter(BaseSiteAdapter):
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -277,6 +277,7 @@ class WizardTalesNetAdapter(BaseSiteAdapter):
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -194,6 +194,7 @@ class WolverineAndRogueComAdapter(BaseSiteAdapter):
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -126,8 +126,13 @@ class WraithBaitComAdapter(BaseSiteAdapter):
|
||||
|
||||
# Find the chapters:
|
||||
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")):
|
||||
# include author on chapters if multiple authors.
|
||||
if len(alist) > 1:
|
||||
add = " by %s"%stripHTML(chapter.findNext('a', href=re.compile(r"viewuser.php\?uid=\d+")))
|
||||
else:
|
||||
add = ""
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href']+addurl))
|
||||
self.chapterUrls.append((stripHTML(chapter)+add,'http://'+self.host+'/'+chapter['href']+addurl))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
@@ -196,6 +201,7 @@ class WraithBaitComAdapter(BaseSiteAdapter):
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -25,10 +25,8 @@ from gziphttp import GZipProcessor
|
||||
import adapters
|
||||
from configurable import Configuration
|
||||
|
||||
def get_urls_from_page(url,configuration=None):
|
||||
def get_urls_from_page(url,configuration=None,normalize=False):
|
||||
|
||||
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,configuration,normalize)
|
||||
|
||||
def get_urls_from_html(data,url=None,configuration=None,normalize=False):
|
||||
|
||||
normalized = [] # normalized url
|
||||
retlist = [] # orig urls.
|
||||
|
||||
if not configuration:
|
||||
configuration = Configuration("test1.com","EPUB")
|
||||
|
||||
soup = BeautifulSoup(data)
|
||||
|
||||
for a in soup.findAll('a'):
|
||||
@@ -74,12 +82,45 @@ def get_urls_from_page(url,configuration=None):
|
||||
href = href.replace('&index=1','')
|
||||
adapter = adapters.getAdapter(configuration,href)
|
||||
if adapter.story.getMetadata('storyUrl') not in normalized:
|
||||
normalized.add(adapter.story.getMetadata('storyUrl'))
|
||||
normalized.append(adapter.story.getMetadata('storyUrl'))
|
||||
retlist.append(href)
|
||||
except:
|
||||
pass
|
||||
|
||||
return retlist
|
||||
if normalize:
|
||||
return normalized
|
||||
else:
|
||||
return retlist
|
||||
|
||||
def get_urls_from_text(data,configuration=None,normalize=False):
|
||||
|
||||
normalized = [] # 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.append(adapter.story.getMetadata('storyUrl'))
|
||||
retlist.append(href)
|
||||
except:
|
||||
pass
|
||||
|
||||
if normalize:
|
||||
return normalized
|
||||
else:
|
||||
return retlist
|
||||
|
||||
def form_url(parenturl,url):
|
||||
url = url.strip() # ran across an image with a space in the
|
||||
|
||||
@@ -352,7 +352,7 @@ class Story(Configurable):
|
||||
allmetadata = {}
|
||||
|
||||
# special handling for authors/authorUrls
|
||||
authlinkhtml="<a class='authorlink' href='%s'>%s</a>"
|
||||
linkhtml="<a class='%slink' href='%s'>%s</a>"
|
||||
if self.isList('author'): # more than one author, assume multiple authorUrl too.
|
||||
htmllist=[]
|
||||
for i, v in enumerate(self.getList('author')):
|
||||
@@ -366,12 +366,18 @@ class Story(Configurable):
|
||||
aurl=removeAllEntities(aurl)
|
||||
auth=removeAllEntities(auth)
|
||||
|
||||
htmllist.append(authlinkhtml%(aurl,auth))
|
||||
htmllist.append(linkhtml%('author',aurl,auth))
|
||||
self.setMetadata('authorHTML',', '.join(htmllist))
|
||||
else:
|
||||
self.setMetadata('authorHTML',authlinkhtml%(self.getMetadata('authorUrl', removeallentities, doreplacements),
|
||||
self.getMetadata('author', removeallentities, doreplacements)))
|
||||
self.setMetadata('authorHTML',linkhtml%('author',self.getMetadata('authorUrl', removeallentities, doreplacements),
|
||||
self.getMetadata('author', removeallentities, doreplacements)))
|
||||
|
||||
if self.getMetadataRaw('seriesUrl') != None:
|
||||
self.setMetadata('seriesHTML',linkhtml%('series',self.getMetadata('seriesUrl', removeallentities, doreplacements),
|
||||
self.getMetadata('series', removeallentities, doreplacements)))
|
||||
elif self.getMetadataRaw('series') != None:
|
||||
self.setMetadata('seriesHTML',self.getMetadataRaw('series'))
|
||||
|
||||
for k in self.getValidMetaList():
|
||||
if self.isList(k) and keeplists:
|
||||
allmetadata[k] = self.getList(k, removeallentities, doreplacements)
|
||||
@@ -415,11 +421,13 @@ class Story(Configurable):
|
||||
# includelist prevents infinite recursion of include_in_'s
|
||||
if self.hasConfig("include_in_"+listname) and listname not in includelist:
|
||||
for k in self.getConfigList("include_in_"+listname):
|
||||
retlist.extend(self.getList(k,removeallentities,doreplacements,includelist=includelist+[listname]))
|
||||
retlist.extend(self.getList(k,removeallentities=False,
|
||||
doreplacements=doreplacements,includelist=includelist+[listname]))
|
||||
else:
|
||||
|
||||
if not self.isList(listname):
|
||||
retlist = [self.getMetadata(listname,removeallentities, doreplacements)]
|
||||
retlist = [self.getMetadata(listname,removeallentities=False,
|
||||
doreplacements=doreplacements)]
|
||||
else:
|
||||
retlist = self.getMetadataRaw(listname)
|
||||
|
||||
|
||||
+4
-2
@@ -57,7 +57,9 @@
|
||||
<h3>Changes:</h3>
|
||||
<p>
|
||||
<ul>
|
||||
<li>Change how fimfiction.net story descriptions are collected.</li>
|
||||
<li>Another fix for fanfiction.net changes.</li>
|
||||
<li>Add author to chapter TOC for multi-author stories on TtH and WraithBait.<br />
|
||||
(AO3 doesn't reliably report chapter author.)</li>
|
||||
</ul>
|
||||
</p>
|
||||
<p>
|
||||
@@ -68,7 +70,7 @@
|
||||
If you have any problems with this application, please
|
||||
report them in
|
||||
the <a href="http://groups.google.com/group/fanfic-downloader">FanFictionDownLoader Google Group</a>. The
|
||||
<a href="http://4-4-45.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
|
||||
<a href="http://4-4-51.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
|
||||
</p>
|
||||
<div id='error'>
|
||||
{{ error_message }}
|
||||
|
||||
+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','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')
|
||||
|
||||
+18
-6
@@ -44,6 +44,9 @@ language_label:Language
|
||||
characters_label:Characters
|
||||
ships_label:Relationships
|
||||
series_label:Series
|
||||
seriesUrl_label:Series URL
|
||||
## seriesHTML is series as a link to seriesUrl.
|
||||
seriesHTML_label:Series
|
||||
## Completed/In-Progress
|
||||
status_label:Status
|
||||
## Dates story first published, last updated, and downloaded(last with time).
|
||||
@@ -81,7 +84,7 @@ dateUpdated_format:%%Y-%%m-%%d
|
||||
## You can include extra text or HTML that will be included as-is in
|
||||
## the title page. Eg: titlepage_entries: ...,<br />,summary,<br />,...
|
||||
## All current formats already include title and author.
|
||||
titlepage_entries: series,category,genre,language,characters,ships,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,numWords,site,description
|
||||
titlepage_entries: seriesHTML,category,genre,language,characters,ships,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,numWords,site,description
|
||||
|
||||
## Try to collect series name and number of this story in series.
|
||||
## Some sites (ab)use 'series' for reading lists and personal
|
||||
@@ -249,7 +252,7 @@ output_css:
|
||||
|
||||
[txt]
|
||||
## Add URLs since there aren't links.
|
||||
titlepage_entries: series,category,genre,language,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,numWords,site,storyUrl, authorUrl, description
|
||||
titlepage_entries: series,seriesUrl,category,genre,language,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,numWords,site,storyUrl, authorUrl, description
|
||||
|
||||
## Width to word wrap text output. 0 indicates no wrapping.
|
||||
wrap_width: 78
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user