Compare commits

...
Author SHA1 Message Date
Jim Miller 6630e6bb9c Bump version, don't use calibre specific OptionParser for plugin CLI. 2013-03-15 16:19:10 -05:00
Jim Miller c26bca0f6d PI only - Drag and drop emails from Thunderbird on FFDL URL box to add URLs. 2013-03-14 21:31:37 -05:00
Jim Miller 8cd3663d82 Add *plugin* CLI via calibre's cli_main feature. --PI only. (list continues)
Add "marked:ffdl_success/failed" to added/updated/failed books. --PI only.
Add "Show added/updated" pref using above. --PI only.
Preserve log page in epub across overwrites as well as updates. --PI only.
Add "add_to_" feature to ini config.  Allow higher priority sections to *add* to
 any ini param rather than replace it.
2013-03-14 17:16:26 -05:00
Jim Miller fdb45183c7 Remove BBCode, not used by adapter_fimfictionnet anymore. 2013-03-12 16:34:43 -05:00
Jim Miller 284ef7578e Remove BBCode, not used by adapter_fimfictionnet anymore. 2013-03-12 16:33:38 -05:00
Jim Miller 6589dcd4b6 Added tag FanFictionDownLoader-4.4.46 for changeset de548d8c0ba6 2013-03-04 12:02:58 -06:00
Jim Miller 81a75d2097 Added tag calibre-plugin-1.7.12 for changeset de548d8c0ba6 2013-03-04 12:02:45 -06:00
Jim Miller df09eadf81 Fix login failure for tth, add anthology=true metadata, bump version. 2013-03-04 12:02:32 -06:00
Jim Miller abde9fdf8d Page scrape story descriptions again for fimfiction.net due to API issues. 2013-03-02 15:46:47 -06:00
Jim Miller b76e50719b Use existing anthology title on update, even when a series. 2013-03-01 10:57:42 -06:00
Jim Miller 053b629d4b Added tag FanFictionDownLoader-4.4.45 for changeset 41ec08c9f675 2013-02-27 13:47:11 -06:00
Jim Miller 60a2e22c93 Moved tag calibre-plugin-1.7.11 to changeset 41ec08c9f675 (from changeset 436fc385166c) 2013-02-27 13:47:02 -06:00
Jim Miller c6127b2087 Bump versions. 2013-02-27 13:46:48 -06:00
Jim Miller e53661bb06 Added tag calibre-plugin-1.7.11 for changeset 436fc385166c 2013-02-27 13:44:48 -06:00
Jim Miller bb86f55c4a www.ficwad.com -> ficwad.com. Ficwad changed their canonical domain. 2013-02-27 13:44:34 -06:00
Jim Miller e2e086f2e5 Added tag FanFictionDownLoader-4.4.44 for changeset ee7ad490f080 2013-02-19 18:39:02 -06:00
Jim Miller 101e3d9866 Added tag calibre-plugin-1.7.10 for changeset ee7ad490f080 2013-02-19 18:38:49 -06:00
Jim Miller 6c9cfa49f8 Source URL inside anthologies, tweaks, bump versions. 2013-02-19 18:38:35 -06:00
Jim Miller 2db927665a First version with Anthology feature in Plugin. 2013-02-15 17:50:12 -06:00
Jim Miller cd8f5f2769 Some refactoring of plugin code. Mostly renaming, but some optimization, too. 2013-02-13 16:54:26 -06:00
Jim Miller 020e588527 Added tag FanFictionDownLoader-4.4.43 for changeset 2d1bca833812 2013-02-08 11:06:50 -06:00
Jim Miller 5a63bdff8f Added tag calibre-plugin-1.7.09 for changeset 2d1bca833812 2013-02-08 11:06:32 -06:00
Jim Miller f742e581c9 Include author notes in chapters on fimfiction.net. 2013-02-08 11:05:59 -06:00
Jim Miller 7d7cad34ec Bump versions. 2013-02-06 11:55:57 -06:00
Jim Miller 82525af9d5 Allow other URLs for archive.skyehawke.com:
http://archive.skyehawke.com/story.php?no=1234
http://www.skyehawke.com/archive/story.php?no=1234
http://skyehawke.com/archive/story.php?no=1234
2013-02-06 11:33:34 -06:00
Jim Miller db68816020 Merge changes 2013-02-06 11:16:17 -06:00
Jim Miller 5fa9399cf2 Change PI status message when given URLs are all rejected. 2013-02-05 23:39:27 -06:00
Ida c7f26d0448 New adapters for www.henneth-annun.net and www.psychfic.com 2013-02-06 00:28:14 -05:00
Jim Miller b3777810dc Added tag FanFictionDownLoader-4.4.42 for changeset 5bc0ad4d66f1 2013-02-04 13:29:40 -06:00
Jim Miller 302eef4287 Added tag calibre-plugin-1.7.08 for changeset 5bc0ad4d66f1 2013-02-04 13:29:27 -06:00
Jim Miller e128888b3e Fix author URLs for multiple author stories, bump versions again. 2013-02-04 13:29:16 -06:00
Jim Miller e0b56d2f2d Moved tag FanFictionDownLoader-4.4.41 to changeset 0215d06a610e (from changeset 95b201446937) 2013-02-04 12:57:57 -06:00
Jim Miller 47126e1a5b Moved tag calibre-plugin-1.7.07 to changeset 0215d06a610e (from changeset 95b201446937) 2013-02-04 12:57:47 -06:00
Jim Miller 770162568e Bump versions. 2013-02-04 12:57:34 -06:00
Jim Miller c6e06e66bd Added tag FanFictionDownLoader-4.4.41 for changeset 95b201446937 2013-02-04 12:55:09 -06:00
Jim Miller b375fc5565 Added tag calibre-plugin-1.7.07 for changeset 95b201446937 2013-02-04 12:54:53 -06:00
Jim Miller b1a036d2fd Optimize adding to Reading Lists on add/update. 2013-02-03 11:45:00 -06:00
Jim Miller f6d407d5eb Fix broken lastupdate tags feature. Defaulted off for PI. 2013-02-03 11:24:34 -06:00
Jim Miller d033983e56 Remove extra newlines introduced by writer_epub on updates. 2013-02-03 10:57:19 -06:00
Jim Miller 81b74d045d Better fix for Nook hyphenation issue--Nook now honors adobe-hyphenate: none. 2013-02-03 10:57:06 -06:00
Jim Miller b818a0c3ee Make get list url from page fill add dialog, not view log. 2013-02-01 18:04:36 -06:00
Jim Miller 72ae3b2e6a Include hyphenation test case in test1 adapter. 2013-02-01 17:57:56 -06:00
Jim Miller 8154fd770c Strip leading 0s from story ids for canonical ids in AO3. 2013-02-01 17:56:51 -06:00
Jim Miller 97a3e0c6af Improve error handling, add retry for restrictedsection.org. 2013-01-30 19:44:22 -06:00
Jim Miller 28f93fcee2 Added tag calibre-plugin-1.7.06 for changeset 26ec05f3601f 2013-01-28 09:57:52 -06:00
Jim Miller dacf70553f Added tag FanFictionDownLoader-4.4.40 for changeset 26ec05f3601f 2013-01-28 09:57:38 -06:00
Jim Miller 9b21c0e4d6 Fix for thewriterscoffeeshop.com's changed date format. Bump versions. 2013-01-28 09:57:18 -06:00
Jim Miller f36833b114 Added tag FanFictionDownLoader-4.4.39 for changeset 7e016bb3f167 2013-01-23 22:20:18 -06:00
Jim Miller fe65c3a29c Added tag calibre-plugin-1.7.05 for changeset 7e016bb3f167 2013-01-23 22:20:03 -06:00
Jim Miller 99c896e979 Fix for YA fimf API issue. This time it declares passworded stories 'Invalid'. 2013-01-19 16:20:25 -06:00
36 changed files with 1843 additions and 1959 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# ffd-retief-hrd fanfictiondownloader
application: fanfictiondownloader
version: 4-4-39
version: 4-4-46
runtime: python27
api_version: 1
threadsafe: true
+27 -9
View File
@@ -27,7 +27,7 @@ class FanFictionDownLoaderBase(InterfaceActionBase):
description = 'UI plugin to download FanFiction stories from various sites.'
supported_platforms = ['windows', 'osx', 'linux']
author = 'Jim Miller'
version = (1, 7, 5)
version = (1, 7, 13)
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"]))
+1 -1
View File
@@ -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.
+12 -120
View File
@@ -15,10 +15,10 @@ 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
from calibre_plugins.fanfictiondownloader_plugin.dialogs \
import (UPDATE, UPDATEALWAYS, OVERWRITE, collision_order, RejectListDialog,
EditTextDialog)
@@ -27,126 +27,10 @@ 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
@@ -282,6 +166,7 @@ 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()
@@ -448,6 +333,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'])
@@ -911,7 +803,7 @@ titleLabels = {
'ships':'Relationships',
'datePublished':'Published',
'dateUpdated':'Updated',
'dateCreated':'Packaged',
'dateCreated':'Created',
'rating':'Rating',
'warnings':'Warnings',
'numChapters':'Chapters',
@@ -922,7 +814,7 @@ titleLabels = {
'extratags':'Extra Tags',
'title':'Title',
'storyUrl':'Story URL',
'description':'Summary',
'description':'Description',
'author':'Author',
'authorUrl':'Author URL',
'formatname':'File Format',
+164 -145
View File
@@ -10,6 +10,9 @@ __docformat__ = 'restructuredtext en'
import traceback
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,
@@ -30,13 +33,15 @@ from calibre_plugins.fanfictiondownloader_plugin.common_utils \
import (ReadOnlyTableWidgetItem, ReadOnlyTextIconWidgetItem, SizePersistedDialog,
ImageTitleLayout, get_icon)
SKIP='Skip'
ADDNEW='Add New Book'
UPDATE='Update EPUB if New Chapters'
UPDATEALWAYS='Update EPUB Always'
OVERWRITE='Overwrite if Newer'
OVERWRITEALWAYS='Overwrite Always'
CALIBREONLY='Update Calibre Metadata Only'
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.geturls import get_urls_from_html, get_urls_from_text
SKIP=u'Skip'
ADDNEW=u'Add New Book'
UPDATE=u'Update EPUB if New Chapters'
UPDATEALWAYS=u'Update EPUB Always'
OVERWRITE=u'Overwrite if Newer'
OVERWRITEALWAYS=u'Overwrite Always'
CALIBREONLY=u'Update Calibre Metadata Only'
collision_order=[SKIP,
ADDNEW,
UPDATE,
@@ -44,6 +49,10 @@ collision_order=[SKIP,
OVERWRITE,
OVERWRITEALWAYS,
CALIBREONLY,]
anthology_collision_order=[UPDATE,
UPDATEALWAYS,
OVERWRITEALWAYS]
# This is a more than slightly kludgey way to get
# EditWithComplete to *not* alpha-order the reasons, but leave
@@ -69,6 +78,46 @@ class NotGoingToDownload(Exception):
class DroppableQTextEdit(QTextEdit):
def __init__(self,parent):
QTextEdit.__init__(self,parent)
def dropEvent(self,event):
# print("event:%s"%event)
# print("event.mimeData():%s"%event.mimeData())
# print("event.mimeData().text():%s"%str(event.mimeData().text()))
# print("event.mimeData().data():%s"%str(event.mimeData().data()))
# print("event.mimeData().formats():%s"%[str(f) for f in event.mimeData().formats()])
# for f in event.mimeData().formats():
# try:
# print("event.mimeData().data('%s'):%s"%(f,event.mimeData().data(f)))
# except:
# print("failed %s"%f)
mimetype='text/uri-list'
# print("event.mimeData().data('%s'):%s"%(mimetype,event.mimeData().data(mimetype)))
urllist=[]
filelist="%s"%event.mimeData().data(mimetype)
for f in filelist.splitlines():
#print("filename:%s"%f)
if f.endswith(".eml"):
fhandle = urllib.urlopen(f)
#print("file:\n%s\n\n"%fhandle.read())
msg = email.message_from_file(fhandle)
if msg.is_multipart():
for part in msg.walk():
#print("part type:%s"%part.get_content_type())
if part.get_content_type() == "text/html":
#print("URL list:%s"%get_urls_from_data(part.get_payload(decode=True)))
urllist.extend(get_urls_from_html(part.get_payload(decode=True)))
if part.get_content_type() == "text/plain":
#print("part content:text/plain")
# print("part content:%s"%part.get_payload(decode=True))
urllist.extend(get_urls_from_text(part.get_payload(decode=True)))
else:
urllist.extend(get_urls_from_text("%s"%msg))
if urllist:
self.append("\n".join(urllist))
return QTextEdit.dropEvent(self,event)
def canInsertFromMimeData(self, source):
if source.hasUrls():
@@ -84,10 +133,23 @@ class DroppableQTextEdit(QTextEdit):
class AddNewDialog(SizePersistedDialog):
def __init__(self, gui, prefs, icon, url_list_text):
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
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 )
@@ -98,55 +160,58 @@ class AddNewDialog(SizePersistedDialog):
self.setWindowTitle('FanFictionDownLoader')
self.setWindowIcon(icon)
self.l.addWidget(QLabel('Story URL(s), one per line:'))
self.l.addWidget(QLabel(labeltext))
self.url = DroppableQTextEdit(self)
self.url.setToolTip('URLs for stories, one per line.\nWill take URLs from clipboard, but only valid URLs.\nAdd [1,5] after the URL to limit the download to chapters 1-5.')
self.url.setToolTip(tooltiptext)
self.url.setLineWrapMode(QTextEdit.NoWrap)
self.url.setText(url_list_text)
self.l.addWidget(self.url)
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)
horz = QHBoxLayout()
label = QLabel('If Story Already Exists?')
horz.addWidget(label)
self.collision = QComboBox(self)
self.collision.setToolTip("What to do if there's already an existing story with the same URL or title and author.")
# add collision options
self.set_collisions()
i = self.collision.findText(prefs['collision'])
if i > -1:
self.collision.setCurrentIndex(i)
label.setBuddy(self.collision)
horz.addWidget(self.collision)
self.l.addLayout(horz)
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)
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.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)
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)
@@ -163,20 +228,39 @@ class AddNewDialog(SizePersistedDialog):
def set_collisions(self):
prev=self.collision.currentText()
self.collision.clear()
for o in collision_order:
if self.fileform.currentText() == 'epub' or o not in [UPDATE,UPDATEALWAYS]:
if self.merge:
order = anthology_collision_order
else:
order = collision_order
for o in order:
if self.merge or self.fileform.currentText() == 'epub' or o not in [UPDATE,UPDATEALWAYS]:
self.collision.addItem(o)
i = self.collision.findText(prev)
if i > -1:
self.collision.setCurrentIndex(i)
def get_ffdl_options(self):
return {
'fileform': unicode(self.fileform.currentText()),
'collision': unicode(self.collision.currentText()),
'updatemeta': self.updatemeta.isChecked(),
'updateepubcover': self.updateepubcover.isChecked(),
}
if self.merge:
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(),
}
def get_urlstext(self):
return unicode(self.url.toPlainText())
@@ -193,10 +277,11 @@ class CollectURLDialog(SizePersistedDialog):
'''
Collect single url for get urls.
'''
def __init__(self, gui, title, url_text):
def __init__(self, gui, title, url_text, epubmerge_plugin=None):
SizePersistedDialog.__init__(self, gui, 'FanFictionDownLoader plugin:get story urls')
self.gui = gui
self.status=False
self.anthology=False
self.setMinimumWidth(300)
@@ -204,28 +289,40 @@ class CollectURLDialog(SizePersistedDialog):
self.setLayout(self.l)
self.setWindowTitle(title)
self.l.addWidget(QLabel(title),0,0,1,2)
self.l.addWidget(QLabel(title),0,0,1,3)
self.l.addWidget(QLabel("URL:"),1,0)
self.url = QLineEdit(self)
self.url.setText(url_text)
self.l.addWidget(self.url,1,1)
self.l.addWidget(self.url,1,1,1,2)
self.ok_button = QPushButton('OK', self)
self.ok_button.clicked.connect(self.ok)
self.l.addWidget(self.ok_button,2,0)
self.indiv_button = QPushButton('For Individual Books', self)
self.indiv_button.setToolTip('Get URLs and go to dialog for individual story downloads.')
self.indiv_button.clicked.connect(self.indiv)
self.l.addWidget(self.indiv_button,2,0)
self.merge_button = QPushButton('For Anthology Epub', self)
self.merge_button.setToolTip('Get URLs and go to dialog for Anthology download.\nRequires EpubMerge 1.3.1+ plugin.')
self.merge_button.clicked.connect(self.merge)
self.l.addWidget(self.merge_button,2,1)
self.merge_button.setEnabled(epubmerge_plugin!=None)
self.cancel_button = QPushButton('Cancel', self)
self.cancel_button.clicked.connect(self.cancel)
self.l.addWidget(self.cancel_button,2,1)
self.l.addWidget(self.cancel_button,2,2)
# restore saved size.
self.resize_dialog()
def ok(self):
def indiv(self):
self.status=True
self.accept()
def merge(self):
self.status=True
self.anthology=True
self.accept()
def cancel(self):
self.status=False
self.reject()
@@ -505,84 +602,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):
File diff suppressed because it is too large Load Diff
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2013, Jim Miller'
__docformat__ = 'restructuredtext en'
from StringIO import StringIO
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters, exceptions
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.configurable import Configuration
from calibre_plugins.fanfictiondownloader_plugin.prefs import (prefs)
def get_ffdl_personalini():
if prefs['includeimages']:
# this is a cheat to make it easier for users.
return '''[epub]
include_images:true
keep_summary_html:true
make_firstimage_cover:true
''' + prefs['personal.ini']
else:
return prefs['personal.ini']
def get_ffdl_config(url,fileform="epub",personalini=None):
if not personalini:
personalini = get_ffdl_personalini()
site='unknown'
try:
site = adapters.getConfigSectionFor(url)
except Exception as e:
print("Failed trying to get ini config for url(%s): %s, using section [%s] instead"%(url,e,site))
configuration = Configuration(site,fileform)
configuration.readfp(StringIO(get_resources("plugin-defaults.ini")))
configuration.readfp(StringIO(personalini))
return configuration
def get_ffdl_adapter(url,fileform="epub",personalini=None):
return adapters.getAdapter(get_ffdl_config(url,fileform,personalini),url)
+31 -4
View File
@@ -18,9 +18,9 @@ from calibre.utils.ipc.job import ParallelJob
from calibre_plugins.fanfictiondownloader_plugin.dialogs import (NotGoingToDownload,
OVERWRITE, OVERWRITEALWAYS, UPDATE, UPDATEALWAYS, ADDNEW, SKIP, CALIBREONLY)
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters, writers, exceptions
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.configurable import Configuration
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.epubutils import get_update_data
from calibre_plugins.fanfictiondownloader_plugin.ffdl_util import (get_ffdl_adapter, get_ffdl_config)
# ------------------------------------------------------------------------------
#
# Functions to perform downloads using worker jobs
@@ -114,9 +114,9 @@ def do_download_for_worker(book,options):
try:
book['comment'] = 'Download started...'
configuration = Configuration(adapters.getConfigSectionFor(book['url']),options['fileform'])
configuration.readfp(StringIO(get_resources("plugin-defaults.ini")))
configuration.readfp(StringIO(options['personal.ini']))
configuration = get_ffdl_config(book['url'],
options['fileform'],
options['personal.ini'])
if not options['updateepubcover'] and 'epub_for_update' in book and options['collision'] in (UPDATE, UPDATEALWAYS):
configuration.set("overrides","never_make_cover","true")
@@ -153,6 +153,22 @@ def do_download_for_worker(book,options):
elif options['collision'] in (ADDNEW, SKIP, OVERWRITE, OVERWRITEALWAYS) or \
('epub_for_update' not in book and options['collision'] in (UPDATE, UPDATEALWAYS)):
# preserve logfile even on overwrite.
if 'epub_for_update' in book:
(urlignore,
chaptercountignore,
oldchaptersignore,
oldimgsignore,
oldcoverignore,
calibrebookmarkignore,
# only logfile set in adapter, so others aren't used.
adapter.logfile) = get_update_data(book['epub_for_update'])
# change the existing entries id to notid so
# write_epub writes a whole new set to indicate overwrite.
if adapter.logfile:
adapter.logfile = adapter.logfile.replace("span id","span notid")
print("write to %s"%outfile)
writer.writeStory(outfilename=outfile, forceOverwrite=True)
book['comment'] = 'Download %s completed, %s chapters.'%(options['fileform'],story.getMetadata("numChapters"))
@@ -171,6 +187,17 @@ def do_download_for_worker(book,options):
adapter.calibrebookmark,
adapter.logfile) = get_update_data(book['epub_for_update'])
# dup handling from ffdl_plugin needed for anthology updates.
if options['collision'] == UPDATE:
if chaptercount == urlchaptercount:
book['comment']="Already contains %d chapters. Reuse as is."%chaptercount
book['outfile'] = book['epub_for_update'] # for anthology merge ops.
return book
# dup handling from ffdl_plugin needed for anthology updates.
if chaptercount > urlchaptercount:
raise NotGoingToDownload("Existing epub contains %d chapters, web site only has %d. Use Overwrite to force update." % (chaptercount,urlchaptercount),'dialog_error.png')
print("Do update - epub(%d) vs url(%d)" % (chaptercount, urlchaptercount))
print("write to %s"%outfile)
+141
View File
@@ -0,0 +1,141 @@
#!/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['injectseries'] = False
default_prefs['send_lists'] = ''
default_prefs['read_lists'] = ''
default_prefs['addtolists'] = False
default_prefs['addtoreadlists'] = False
default_prefs['addtolistsonread'] = False
default_prefs['gcnewonly'] = False
default_prefs['gc_site_settings'] = {}
default_prefs['allow_gc_from_ini'] = True
default_prefs['countpagesstats'] = []
default_prefs['errorcol'] = ''
default_prefs['custom_cols'] = {}
default_prefs['custom_cols_newonly'] = {}
default_prefs['allow_custcol_from_ini'] = True
default_prefs['std_cols_newonly'] = {}
def set_library_config(library_config,db):
db.prefs.set_namespaced(PREFS_NAMESPACE,
PREFS_KEY_SETTINGS,
library_config)
def get_library_config(db):
library_id = get_library_uuid(db)
library_config = None
# Check whether this is a configuration needing to be migrated
# from json into database. If so: get it, set it, rename it in json.
if library_id in old_prefs:
#print("get prefs from old_prefs")
library_config = old_prefs[library_id]
set_library_config(library_config)
old_prefs["migrated to library db %s"%library_id] = old_prefs[library_id]
del old_prefs[library_id]
if library_config is None:
#print("get prefs from db")
library_config = db.prefs.get_namespaced(PREFS_NAMESPACE, PREFS_KEY_SETTINGS,
copy.deepcopy(default_prefs))
return library_config
# This is where all preferences for this plugin *were* stored
# Remember that this name (i.e. plugins/fanfictiondownloader_plugin) is also
# in a global namespace, so make it as unique as possible.
# You should always prefix your config file name with plugins/,
# so as to ensure you dont accidentally clobber a calibre config file
old_prefs = JSONConfig('plugins/fanfictiondownloader_plugin')
# fake out so I don't have to change the prefs calls anywhere. The
# Java programmer in me is offended by op-overloading, but it's very
# tidy.
class PrefsFacade():
def _get_db(self):
if self.passed_db:
return self.passed_db
else:
# In the GUI plugin we want current db so we detect when
# it's changed. CLI plugin calls need to pass db in.
return get_gui().current_db
def __init__(self,passed_db=None):
self.default_prefs = default_prefs
self.libraryid = None
self.current_prefs = None
self.passed_db=passed_db
def _get_prefs(self):
libraryid = get_library_uuid(self._get_db())
if self.current_prefs == None or self.libraryid != libraryid:
#print("self.current_prefs == None(%s) or self.libraryid != libraryid(%s)"%(self.current_prefs == None,self.libraryid != libraryid))
self.libraryid = libraryid
self.current_prefs = get_library_config(self._get_db())
return self.current_prefs
def __getitem__(self,k):
prefs = self._get_prefs()
if k not in prefs:
# pulls from default_prefs.defaults automatically if not set
# in default_prefs
return self.default_prefs[k]
return prefs[k]
def __setitem__(self,k,v):
prefs = self._get_prefs()
prefs[k]=v
# self._save_prefs(prefs)
def __delitem__(self,k):
prefs = self._get_prefs()
if k in prefs:
del prefs[k]
def save_to_db(self):
set_library_config(self._get_prefs(),self._get_db())
prefs = PrefsFacade()
+18 -4
View File
@@ -320,12 +320,13 @@ background_color: ffffff
## Allow customization of CSS. Make sure to keep at least one space
## at the start of each line and to escape % to %%. Also need
## background_color to be in the same section, if included in CSS.
## 'adobe-text-layout: optimizeSpeed;' prevents hyphenation on newer Nooks
## 'adobe-hyphenate: none;' prevents hyphenation on newer Nooks
## STR(wG) (1.2.1+ for sure)
output_css:
body { background-color: #%(background_color)s;
text-align: justify;
margin: 2%%;
adobe-text-layout: optimizeSpeed; }
adobe-hyphenate: none; }
pre { font-size: x-small; }
sml { font-size: small; }
h1 { text-align: center; }
@@ -368,7 +369,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
@@ -1030,7 +1031,7 @@ extratags:
## for examples of how to use them.
extra_valid_entries:reviews,favs,follows
[www.ficwad.com]
[ficwad.com]
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -1070,6 +1071,10 @@ extracategories:Harry Potter
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[www.henneth-annun.net]
## Site dedicated to these categories/characters/ships
extracategories:The Hobbit
[www.hpfandom.net]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
@@ -1157,6 +1162,15 @@ extracategories:Harry Potter
## Site dedicated to these categories/characters/ships
extracategories:Prison Break
[www.psychfic.com]
## Site dedicated to these categories/characters/ships
extracategories:Psych
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[www.qaf-fic.com]
## Site dedicated to these categories/characters/ships
extracategories:Queer as Folk
+34 -16
View File
@@ -32,10 +32,17 @@ if sys.version_info >= (2, 7):
loghandler.setFormatter(logging.Formatter("(=====)(levelname)s:%(message)s"))
rootlogger.addHandler(loghandler)
from fanficdownloader import adapters,writers,exceptions
from fanficdownloader.configurable import Configuration
from fanficdownloader.epubutils import get_dcsource_chaptercount, get_update_data
from fanficdownloader.geturls import get_urls_from_page
try:
from fanficdownloader import adapters,writers,exceptions
from fanficdownloader.configurable import Configuration
from fanficdownloader.epubutils import get_dcsource_chaptercount, get_update_data
from fanficdownloader.geturls import get_urls_from_page
except:
# running under calibre
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters,writers,exceptions
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.configurable import Configuration
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.epubutils import get_dcsource_chaptercount, get_update_data
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.geturls import get_urls_from_page
if sys.version_info < (2, 5):
print "This program requires Python 2.5 or newer."
@@ -48,15 +55,23 @@ def writeStory(config,adapter,writeformat,metaonly=False,outstream=None):
del writer
return output_filename
def main():
def main(argv,
parser=None,
passed_defaultsini=None,
passed_personalini=None):
# read in args, anything starting with -- will be treated as --<varible>=<value>
usage = "usage: %prog [options] storyurl"
parser = OptionParser(usage)
if not parser:
parser = OptionParser("usage: %prog [options] storyurl")
parser.add_option("-f", "--format", dest="format", default="epub",
help="write story as FORMAT, epub(default), text or html", metavar="FORMAT")
help="write story as FORMAT, epub(default), mobi, text or html", metavar="FORMAT")
if passed_defaultsini:
config_help="read config from specified file(s) in addition to calibre plugin personal.ini, ~/.fanficdownloader/personal.ini, and ./personal.ini"
else:
config_help="read config from specified file(s) in addition to ~/.fanficdownloader/defaults.ini, ~/.fanficdownloader/personal.ini, ./defaults.ini, and ./personal.ini"
parser.add_option("-c", "--config",
action="append", dest="configfile", default=None,
help="read config from specified file(s) in addition to ~/.fanficdownloader/defaults.ini, ~/.fanficdownloader/personal.ini, ./defaults.ini, ./personal.ini", metavar="CONFIG")
help=config_help, metavar="CONFIG")
parser.add_option("-b", "--begin", dest="begin", default=None,
help="Begin with Chapter START", metavar="START")
parser.add_option("-e", "--end", dest="end", default=None,
@@ -69,13 +84,13 @@ def main():
help="Retrieve metadata and stop. Or, if --update-epub, update metadata title page only.",)
parser.add_option("-u", "--update-epub",
action="store_true", dest="update",
help="Update an existing epub with new chapter, give epub filename instead of storyurl.",)
help="Update an existing epub with new chapters, give epub filename instead of storyurl.",)
parser.add_option("--update-cover",
action="store_true", dest="updatecover",
help="Update cover in an existing epub, otherwise existing cover (if any) is used on update. Only valid with --update-epub.",)
parser.add_option("--force",
action="store_true", dest="force",
help="Force overwrite or update of an existing epub, download and overwrite all chapters.",)
help="Force overwrite of an existing epub, download and overwrite all chapters.",)
parser.add_option("-l", "--list",
action="store_true", dest="list",
help="Get list of valid story URLs from page given.",)
@@ -83,7 +98,7 @@ def main():
action="store_true", dest="debug",
help="Show debug output while downloading.",)
(options, args) = parser.parse_args()
(options, args) = parser.parse_args(argv)
if not options.debug:
logger = logging.getLogger("fanficdownloader")
@@ -107,12 +122,18 @@ def main():
conflist = []
homepath = join(expanduser("~"),".fanficdownloader")
if passed_defaultsini:
configuration.readfp(passed_defaultsini)
if isfile(join(homepath,"defaults.ini")):
conflist.append(join(homepath,"defaults.ini"))
if isfile("defaults.ini"):
conflist.append("defaults.ini")
if passed_personalini:
configuration.readfp(passed_personalini)
if isfile(join(homepath,"personal.ini")):
conflist.append(join(homepath,"personal.ini"))
if isfile("personal.ini"):
@@ -155,9 +176,7 @@ def main():
return
try:
adapter = adapters.getAdapter(configuration,url)
adapter.setChaptersRange(options.begin,options.end)
## Check for include_images and absence of PIL, give warning.
@@ -173,7 +192,6 @@ def main():
print "You have include_images enabled, but Python Image Library(PIL) isn't found.\nImages will be included full size in original format.\nContinue? (y/n)?"
if not sys.stdin.readline().strip().lower().startswith('y'):
return
## three tries, that's enough if both user/pass & is_adult needed,
## or a couple tries of one or the other
@@ -243,5 +261,5 @@ def main():
if __name__ == "__main__":
#import time
#start = time.time()
main()
main(sys.argv[1:])
#print("Total time seconds:%f"%(time.time()-start))
+5 -3
View File
@@ -113,6 +113,8 @@ import adapter_pommedesangcom
import adapter_restrictedsectionorg
import adapter_imagineeficcom
import adapter_buffynfaithnet
import adapter_psychficcom
import adapter_hennethannunnet
## This bit of complexity allows adapters to be added by just adding
## importing. It eliminates the long if/else clauses we used to need
@@ -149,7 +151,7 @@ getNormalStoryURL.__dummyconfig = None
def getAdapter(config,url):
logger.debug("trying url:"+url)
#logger.debug("trying url:"+url)
(cls,fixedurl) = getClassFor(url)
logger.debug("fixedurl:"+fixedurl)
if cls:
@@ -185,11 +187,11 @@ def getClassFor(url):
cls = getClassFromList(domain)
if not cls and domain.startswith("www."):
domain = domain.replace("www.","")
logger.debug("trying site:without www: "+domain)
#logger.debug("trying site:without www: "+domain)
cls = getClassFromList(domain)
fixedurl = fixedurl.replace("http://www.","http://")
if not cls:
logger.debug("trying site:www."+domain)
#logger.debug("trying site:www."+domain)
cls = getClassFromList("www."+domain)
fixedurl = fixedurl.replace("http://","http://www.")
@@ -82,7 +82,8 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
def getSiteURLPattern(self):
# http://archiveofourown.org/collections/Smallville_Slash_Archive/works/159770
return re.escape("http://")+re.escape(self.getSiteDomain())+r"(/collections/[^/]+)?/works/(?P<id>\d+)"
# Discard leading zeros from story ID numbers--AO3 doesn't use them in it's own chapter URLs.
return re.escape("http://")+re.escape(self.getSiteDomain())+r"(/collections/[^/]+)?/works/0*(?P<id>\d+)"
## Login
def needToLoginCheck(self, data):
@@ -66,14 +66,15 @@ class ArchiveSkyeHawkeComAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'archive.skyehawke.com'
@classmethod
def getAcceptDomains(cls):
return ['archive.skyehawke.com','www.skyehawke.com']
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/story.php?no=1234"
return "http://archive.skyehawke.com/story.php?no=1234 http://www.skyehawke.com/archive/story.php?no=1234 http://skyehawke.com/archive/story.php?no=1234"
def getSiteURLPattern(self):
return re.escape("http://"+self.getSiteDomain()+"/story.php?no=")+r"\d+$"
return re.escape("http://")+r"(archive|www)\.skyehawke\.com/(archive/)?story\.php\?no=\d+$"
## Getting the chapter list and the meta data, plus 'is adult' checking.
def extractChapterUrlsAndMetadata(self):
@@ -43,10 +43,10 @@ class FicwadComSiteAdapter(BaseSiteAdapter):
@staticmethod
def getSiteDomain():
return 'www.ficwad.com'
return 'ficwad.com'
def getSiteExampleURLs(self):
return "http://www.ficwad.com/story/137169"
return "http://ficwad.com/story/137169"
def getSiteURLPattern(self):
return re.escape(r"http://"+self.getSiteDomain())+"/story/\d+?$"
@@ -28,8 +28,6 @@ from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from ..bbcodeutils.bbcodeparser import bbcodeparser
from base_adapter import BaseSiteAdapter, makeDate
def getClass():
@@ -97,9 +95,6 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
if "/images/missing_story.png" in data:
raise exceptions.StoryDoesNotExist(self.url)
if "Invalid story id" in apiData.values():
raise exceptions.StoryDoesNotExist(self.url)
if "This story has been marked as having adult content." in data:
raise exceptions.AdultCheckRequired(self.url)
@@ -114,6 +109,9 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
else:
raise exceptions.FailedToLogin(self.url,"Story requires individual password",passwdonly=True)
if "Invalid story id" in apiData.values():
raise exceptions.StoryDoesNotExist(self.url)
storyMetadata = apiData["story"]
## Title
@@ -181,10 +179,7 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
self.setCoverImage(self.url,coverurl)
# the fimfic API gives bbcode for desc, not html.
# btw, bbcode honors newlines, html doesn't. change newlines to br tags.
self.setDescription(self.url,
bbcodeparser().parse(storyMetadata["description"]).html(doDeepCopy=False).replace('\r','').replace('\n','<br />'))
self.setDescription(self.url,soup.find("div", {"class":"description"}))
# Dates are in Unix time
# Take the publish date from the first chapter posted
@@ -219,7 +214,7 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
def getChapterText(self, url):
logger.debug('Getting chapter text from: %s' % url)
soup = bs.BeautifulSoup(self._fetchUrl(url),selfClosingTags=('br','hr')).find('div', {'id' : 'chapter_container'})
soup = bs.BeautifulSoup(self._fetchUrl(url),selfClosingTags=('br','hr')).find('div', {'class' : 'chapter_content'})
if soup == None:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
return self.utf8FromSoup(url,soup)
@@ -0,0 +1,171 @@
# -*- coding: utf-8 -*-
# Copyright 2013 Fanficdownloader team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import time
import logging
logger = logging.getLogger(__name__)
import re
import urllib2
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, makeDate
def getClass():
return HennethAnnunNetAdapter
# Class name has to be unique. Our convention is camel case the
# sitename with Adapter at the end. www is skipped.
class HennethAnnunNetAdapter(BaseSiteAdapter):
def __init__(self, config, url):
BaseSiteAdapter.__init__(self, config, url)
self.decode = ["Windows-1252",
"utf8"] # 1252 is a superset of iso-8859-1.
# Most sites that claim to be
# iso-8859-1 (and some that claim to be
# utf8) are really windows-1252.
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
self.password = ""
self.is_adult=False
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/stories/chapter.cfm?stid='+self.story.getMetadata('storyId'))
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','htan')
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%m/%d/%y"
@staticmethod # must be @staticmethod, don't remove it.
def getSiteDomain():
# The site domain. Does have www here, if it uses it.
return 'www.henneth-annun.net'
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/stories/chapter.cfm?stid=1234"
def getSiteURLPattern(self):
return "http://"+self.getSiteDomain()+"/stories/chapter(_view)?.cfm\?stid="+r"\d+$"
## Getting the chapter list and the meta data, plus 'is adult' checking.
def extractChapterUrlsAndMetadata(self):
# index=1 makes sure we see the story chapter index. Some
# sites skip that for one-chapter stories.
url = self.url
logger.debug("URL: "+url)
try:
data = self._fetchUrl(url)
except urllib2.HTTPError, e:
if e.code == 404:
raise exceptions.StoryDoesNotExist(self.url)
else:
raise e
if "We're sorry. This story is not available." in data:
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: This story is not available.")
# use BeautifulSoup HTML parser to make everything easier to find.
soup = bs.BeautifulSoup(data)
# print data
# Now go hunting for all the meta data and the chapter list.
## Title
a = soup.find('h2', {'id':'page_heading'})
self.story.setMetadata('title',a.string)
# Find the chapters: chapter_view.cfm?stid=6663&amp;spordinal=1"
for chapter in soup.findAll('a', href=re.compile(r'chapter_view.cfm\?stid='+self.story.getMetadata('storyId')+"&spordinal=\d+$")):
# just in case there's tags, like <i> in chapter titles.
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/stories/'+chapter['href']))
self.story.setMetadata('numChapters',len(self.chapterUrls))
self.story.setMetadata('numWords', soup.find('tr', {'class':'foot'}).findAll('td')[1].text)
self.setDescription(url,soup.find('div', {'id':'summary'}))
# <span class="label">Rated:</span> NC-17<br /> etc
info = soup.find('div', {'id':'storyinformation'})
labels=info.findAll('b')
for labelspan in labels:
value = labelspan.nextSibling
label = labelspan.string
if 'Completion' in label:
if 'Complete' in value.string:
self.story.setMetadata('status', 'Completed')
else:
self.story.setMetadata('status', 'In-Progress')
if 'Rating' in label:
self.story.setMetadata('rating', value.string)
if 'Era:' in label:
self.story.addToList('category',value.string)
if 'Genre' in label:
self.story.addToList('genre',value.string)
labels=info.findAll('strong')
for labelspan in labels:
value = labelspan.nextSibling
label = labelspan.string
if 'Author' in label:
value=value.nextSibling
self.story.setMetadata('authorId',value['href'].split('=')[1])
self.story.setMetadata('authorUrl','http://'+self.host+'/'+value['href'])
self.story.setMetadata('author',value.string)
if 'Post' in label:
self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
if 'Updated:' in label:
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
for char in soup.findAll('a', href=re.compile(r"/resources/bios_view.cfm\?scid=\d+")):
self.story.addToList('characters',stripHTML(char))
# grab the text for an individual chapter.
def getChapterText(self, url):
logger.debug('Getting chapter text from: %s' % url)
soup = bs.BeautifulStoneSoup(self._fetchUrl(url),
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
div = soup.find('div', {'class' : 'block chapter'})
if None == div:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
return self.utf8FromSoup(url,div)
@@ -0,0 +1,246 @@
# -*- coding: utf-8 -*-
# Copyright 2013 Fanficdownloader team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import time
import logging
logger = logging.getLogger(__name__)
import re
import urllib2
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, makeDate
def getClass():
return PsychFicComAdapter
# Class name has to be unique. Our convention is camel case the
# sitename with Adapter at the end. www is skipped.
class PsychFicComAdapter(BaseSiteAdapter):
def __init__(self, config, url):
BaseSiteAdapter.__init__(self, config, url)
self.decode = ["Windows-1252",
"utf8"] # 1252 is a superset of iso-8859-1.
# Most sites that claim to be
# iso-8859-1 (and some that claim to be
# utf8) are really windows-1252.
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
self.password = ""
self.is_adult=False
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','psyf')
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%B %d, %Y"
@staticmethod # must be @staticmethod, don't remove it.
def getSiteDomain():
# The site domain. Does have www here, if it uses it.
return 'www.psychfic.com'
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
def getSiteURLPattern(self):
return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$"
## Getting the chapter list and the meta data, plus 'is adult' checking.
def extractChapterUrlsAndMetadata(self):
if self.is_adult or self.getConfig("is_adult"):
# Weirdly, different sites use different warning numbers.
# If the title search below fails, there's a good chance
# you need a different number. print data at that point
# and see what the 'click here to continue' url says.
addurl = "&ageconsent=ok&warning=4"
else:
addurl=""
# index=1 makes sure we see the story chapter index. Some
# sites skip that for one-chapter stories.
url = self.url+'&index=1'+addurl
logger.debug("URL: "+url)
try:
data = self._fetchUrl(url)
except urllib2.HTTPError, e:
if e.code == 404:
raise exceptions.StoryDoesNotExist(self.url)
else:
raise e
m = re.search(r"'viewstory.php\?sid=\d+((?:&amp;ageconsent=ok)?&amp;warning=\d+)'",data)
if m != None:
if self.is_adult or self.getConfig("is_adult"):
# We tried the default and still got a warning, so
# let's pull the warning number from the 'continue'
# link and reload data.
addurl = m.group(1)
# correct stupid &amp; error in url.
addurl = addurl.replace("&amp;","&")
url = self.url+'&index=1'+addurl
logger.debug("URL 2nd try: "+url)
try:
data = self._fetchUrl(url)
except urllib2.HTTPError, e:
if e.code == 404:
raise exceptions.StoryDoesNotExist(self.url)
else:
raise e
else:
raise exceptions.AdultCheckRequired(self.url)
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
# use BeautifulSoup HTML parser to make everything easier to find.
soup = bs.BeautifulSoup(data)
# print data
# Now go hunting for all the meta data and the chapter list.
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
# Find authorid and URL from... author url.
a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
self.story.setMetadata('authorId',a['href'].split('=')[1])
self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href'])
self.story.setMetadata('author',a.string)
# Find the chapters:
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")):
# just in case there's tags, like <i> in chapter titles.
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href']+addurl))
self.story.setMetadata('numChapters',len(self.chapterUrls))
# eFiction sites don't help us out a lot with their meta data
# formating, so it's a little ugly.
# utility method
def defaultGetattr(d,k):
try:
return d[k]
except:
return ""
# <span class="label">Rated:</span> NC-17<br /> etc
labels = soup.findAll('span',{'class':'label'})
for labelspan in labels:
value = labelspan.nextSibling
label = labelspan.text
if 'Summary' in label:
## Everything until the next span class='label'
svalue = ""
while not defaultGetattr(value,'class') == 'label':
svalue += str(value)
value = value.nextSibling
self.setDescription(url,svalue)
#self.story.setMetadata('description',stripHTML(svalue))
if 'Rated' in label:
self.story.setMetadata('rating', value)
if 'Word count' in label:
self.story.setMetadata('numWords', value)
if 'Categories' in label:
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
for cat in cats:
self.story.addToList('category',cat.string)
if 'Characters' in label:
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
for char in chars:
self.story.addToList('characters',char.string)
if 'Genre' in label:
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1'))
for genre in genres:
self.story.addToList('genre',genre.string)
if 'Warnings' in label:
warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2'))
for warning in warnings:
self.story.addToList('warnings',warning.string)
if 'Completed' in label:
if 'Yes' in value:
self.story.setMetadata('status', 'Completed')
else:
self.story.setMetadata('status', 'In-Progress')
if 'Published' in label:
self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
if 'Updated' in label:
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
try:
# Find Series name from series URL.
a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+"))
series_name = a.string
series_url = 'http://'+self.host+'/'+a['href']
# use BeautifulSoup HTML parser to make everything easier to find.
seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url))
# can't use ^viewstory...$ in case of higher rated stories with javascript href.
storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+'))
i=1
for a in storyas:
# skip 'report this' and 'TOC' links
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
self.setSeries(series_name, i)
break
i+=1
except:
# I find it hard to care if the series parsing fails
pass
# grab the text for an individual chapter.
def getChapterText(self, url):
logger.debug('Getting chapter text from: %s' % url)
soup = bs.BeautifulStoneSoup(self._fetchUrl(url),
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
div = soup.find('div', {'id' : 'story'})
if None == div:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
return self.utf8FromSoup(url,div)
@@ -205,10 +205,26 @@ class RestrictedSectionOrgSiteAdapter(BaseSiteAdapter):
params['accept.x'] = 1
params['accept.y'] = 1
data = self._postUrl(url, params)
if "I certify that I am over the age of 18 and that accessing the following story will not violate the laws of my country or local ordinances." in data:
raise exceptions.FailedToLogin(url,params['username'])
return data
excpt=None
for sleeptime in [0.5, 1.5, 4, 9]:
time.sleep(sleeptime)
try:
data = self._postUrl(url, params)
if data == "Unable to connect to the database":
raise exceptions.FailedToDownload("Site reported 'Unable to connect to the database'")
if "I certify that I am over the age of 18 and that accessing the following story will not violate the laws of my country or local ordinances." in data:
raise exceptions.FailedToLogin(url,params['username'])
return data
except exceptions.FailedToLogin, ftl:
# no need to retry these.
raise(ftl)
except Exception, e:
excpt=e
logger.warn("Caught an exception reading URL: %s Exception %s."%(unicode(url),unicode(e)))
logger.error("Giving up on %s" %url)
logger.exception(excpt)
raise(excpt)
# grab the text for an individual chapter.
def getChapterText(self, url):
@@ -216,6 +232,13 @@ class RestrictedSectionOrgSiteAdapter(BaseSiteAdapter):
logger.debug('Getting chapter text from: %s' % url)
data = self._postUrlUP(url)
#print("data:%s"%data)
# some stories have html that confuses the parser. For story
# text we don't care about anything before '<table id="page"'
# and seems to clear the issue.
data = data[data.index('<table id="page"'):]
soup = bs.BeautifulSoup(data)
div = soup.find('td',{'id':'page_content'})
+32 -10
View File
@@ -101,9 +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)
if idnum == 0:
self.setSeries("A Nook Hyphen Test "+self.story.getMetadata('dateCreated'),idnum)
self.story.setMetadata('rating','Tweenie')
if self.story.getMetadata('storyId') == '673':
self.story.addToList('author','Author From List')
@@ -114,6 +115,10 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
self.story.addToList('authorUrl','http://author/url')
self.story.addToList('authorUrl','http://author/url-2')
self.story.addToList('category','Power Rangers')
self.story.addToList('category','SG-1')
self.story.addToList('genre','Porn')
self.story.addToList('genre','Drama')
else:
self.story.setMetadata('authorId','98765')
self.story.setMetadata('authorUrl','http://author/url')
@@ -161,9 +166,9 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
('Chapter 3, Over Cinnabar',self.url+"&chapter=4"),
('Chapter 4',self.url+"&chapter=5"),
('Chapter 5',self.url+"&chapter=6"),
#('Chapter 6',self.url+"&chapter=7"),
#('Chapter 7',self.url+"&chapter=8"),
#('Chapter 8',self.url+"&chapter=9"),
('Chapter 6',self.url+"&chapter=7"),
('Chapter 7',self.url+"&chapter=8"),
('Chapter 8',self.url+"&chapter=9"),
#('Chapter 9',self.url+"&chapter=0"),
#('Chapter 0',self.url+"&chapter=a"),
#('Chapter a',self.url+"&chapter=b"),
@@ -186,9 +191,6 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
def getChapterText(self, url):
logger.debug('Getting chapter text from: %s' % url)
if self.story.getMetadata('storyId') == '667':
raise exceptions.FailedToDownload("Error downloading Chapter: %s!" % url)
if self.story.getMetadata('storyId').startswith('670') or \
self.story.getMetadata('storyId').startswith('672'):
time.sleep(1.0)
@@ -201,20 +203,40 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
<p>http://test1.com?sid=664 - Crazy string title</p>
<p>http://test1.com?sid=665 - raises AdultCheckRequired</p>
<p>http://test1.com?sid=666 - raises StoryDoesNotExist</p>
<p>http://test1.com?sid=667 - raises FailedToDownload on chapter 1</p>
<p>http://test1.com?sid=667 - raises FailedToDownload on chapters 2+</p>
<p>http://test1.com?sid=668 - raises FailedToLogin unless username='Me'</p>
<p>http://test1.com?sid=669 - Succeeds with Updated Date=now</p>
<p>http://test1.com?sid=670 - Succeeds, but sleeps 2sec on each chapter</p>
<p>http://test1.com?sid=671 - Succeeds, but sleeps 2sec metadata only</p>
<p>http://test1.com?sid=672 - Succeeds, quick meta, sleeps 2sec chapters only</p>
<p>http://test1.com?sid=673 - Succeeds, multiple authors</p>
<p>http://test1.com?sid=673 - Succeeds, multiple authors, extra categories, genres</p>
<p>http://test1.com?sid=0 - Succeeds, generates some text specifically for testing hyphenation problems with Nook STR/STRwG</p>
<p>Odd sid's will be In-Progress, evens complete. sid&lt;10 will be assigned one of four languages and included in a series.</p>
</div>
'''
elif self.story.getMetadata('storyId') == '0':
text=u'''
<h3>45. Pronglet Returns to Hogwarts: Chapter 7</h3>
<br />
eyes… but Im not convinced we should automatically<br />
<br /><br />
<b>Thanks to the latest to recommend me: Alastor</b><br />
<br /><br />
“Sure, invite her along. Does she have children?”<br />
<br />
'''
else:
if self.story.getMetadata('storyId') == '667':
raise exceptions.FailedToDownload("Error downloading Chapter: %s!" % url)
text=u'''
<div>
<h3>Chapter title from site</h3>
<p>Timestamp:'''+datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")+'''</p>
<p>Lorem '''+self.crazystring+u''' <i>italics</i>, <b>bold</b>, <u>underline</u> consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
br breaks<br><br>
Puella Magi Madoka Magica/魔法少女まどか★マギカ
@@ -48,7 +48,7 @@ class TheWritersCoffeeShopComSiteAdapter(BaseSiteAdapter):
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/library/viewstory.php?sid='+self.story.getMetadata('storyId'))
self.dateformat = "%B %d, %Y"
self.dateformat = "%d %b %Y"
@staticmethod
@@ -100,8 +100,8 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
if "Stories Published" not in d : #Member Account
logger.info("Failed to login to URL %s as %s" % (loginUrl,
params['penname']))
raise exceptions.FailedToLogin(url,params['penname'])
params['urealname']))
raise exceptions.FailedToLogin(self.url,params['urealname'])
return False
else:
return True
-325
View File
@@ -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'<' : '&lt;',
u'>' : '&gt;',
u'"' : "&quot;",
u'&' : "&amp;"
}
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
-81
View File
@@ -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]
-420
View File
@@ -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 &lt;b&gt;</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 &lt;b&gt; 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()
+18 -4
View File
@@ -74,7 +74,8 @@ class Configuration(ConfigParser.SafeConfigParser):
# internal stuff.
'langcode',
'output_css',
'authorHTML'
'authorHTML',
'lastupdate'
]
def addConfigSection(self,section):
@@ -93,7 +94,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
@@ -105,16 +111,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
+37 -2
View File
@@ -27,8 +27,6 @@ from configurable import Configuration
def get_urls_from_page(url,configuration=None):
normalized = set() # normalized url
retlist = [] # orig urls.
if not configuration:
configuration = Configuration("test1.com","EPUB")
@@ -56,6 +54,16 @@ def get_urls_from_page(url,configuration=None):
opener = u2.build_opener(u2.HTTPCookieProcessor(),GZipProcessor())
data = opener.open(url).read()
return get_urls_from_html(data,url)
def get_urls_from_html(data,url=None,configuration=None):
normalized = set() # normalized url
retlist = [] # orig urls.
if not configuration:
configuration = Configuration("test1.com","EPUB")
soup = BeautifulSoup(data)
for a in soup.findAll('a'):
@@ -81,6 +89,33 @@ def get_urls_from_page(url,configuration=None):
return retlist
def get_urls_from_text(data,configuration=None):
normalized = set() # normalized url
retlist = [] # orig urls.
if not configuration:
configuration = Configuration("test1.com","EPUB")
for href in re.findall('http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', data):
# this (should) catch normal story links, some javascript
# 'are you old enough' links, and 'Report This' links.
# The 'normalized' set prevents duplicates.
if 'story.php' in href:
m = re.search(r"(?P<sid>(view)?story\.php\?(sid|psid|no|story|stid)=\d+)",a['href'])
if m != None:
href = form_url(None,m.group('sid'))
try:
href = href.replace('&index=1','')
adapter = adapters.getAdapter(configuration,href)
if adapter.story.getMetadata('storyUrl') not in normalized:
normalized.add(adapter.story.getMetadata('storyUrl'))
retlist.append(href)
except:
pass
return retlist
def form_url(parenturl,url):
url = url.strip() # ran across an image with a space in the
# src. Browser handled it, so we'd better, too.
+7 -2
View File
@@ -432,8 +432,13 @@ class Story(Configurable):
map(removeAllEntities,retlist) )
if retlist:
# remove dups and sort.
return sorted(list(set(retlist)))
if listname in ('author','authorUrl'):
# need to retain order for author & authorUrl so the
# two match up.
return retlist
else:
# remove dups and sort.
return sorted(list(set(retlist)))
else:
return []
+7 -1
View File
@@ -21,6 +21,7 @@ import StringIO
import zipfile
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
import urllib
import re
## XML isn't as forgiving as HTML, so rather than generate as strings,
## use DOM to generate the XML files.
@@ -657,7 +658,12 @@ div { margin: 0pt; padding: 0pt; }
# as one line. This causes problems for nook(at
# least) when the chapter size starts getting big
# (200k+)
fullhtml = fullhtml.replace('</p>','</p>\n').replace('<br />','<br />\n')
#fullhtml = fullhtml.replace('</p>','</p>\n').replace('<br />','<br />\n')
# The replaces above added tons of extra newlines
# during *each* epub update. The regexp version adds
# only one and removes any extra.
fullhtml = re.sub(r'(</p>|<br />)\n*',r'\1\n',fullhtml)
outputepub.writestr("OEBPS/file%04d.xhtml"%(index+1),fullhtml.encode('utf-8'))
del fullhtml
+14 -3
View File
@@ -57,9 +57,8 @@
<h3>Changes:</h3>
<p>
<ul>
<li>New site: buffynfaith.net (Thanks Dan!)</li>
<li>Change how fimfiction.net story descriptions are collected.</li>
</ul>
FFDL now supports over 80 different sites.
</p>
<p>
Questions? Check out our
@@ -69,7 +68,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-38.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
<a href="http://4-4-45.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
</p>
<div id='error'>
{{ error_message }}
@@ -293,6 +292,8 @@
<dd>
Use the URL of the story's summary, such as
<br /><a href="http://archive.skyehawke.com/story.php?no=17466">http://archive.skyehawke.com/story.php?no=17466</a>.
<br /><a href="http://www.skyehawke.com/archive/story.php?no=17466">http://www.skyehawke.com/archive/story.php?no=17466</a>.
<br /><a href="http://skyehawke.com/archive/story.php?no=17466">http://skyehawke.com/archive/story.php?no=17466</a>.
</dd>
<dt>www.libraryofmoria.com</dt>
<dd>
@@ -601,6 +602,16 @@
<br />Or, use the URL of any story chapter, such as
<br /><a href="http://buffynfaith.net/fanfictions/index.php?act=vie&id=1234&ch=2">http://buffynfaith.net/fanfictions/index.php?act=vie&amp;id=1234&amp;ch=2</a>
</dd>
<dt>www.henneth-annun.net</dt>
<dd>
Use the URL of the story's chapter list, such as
<br /><a href="http://www.henneth-annun.net/stories/chapter.cfm?stid=1234">http://www.henneth-annun.net/stories/chapter.cfm?stid=1234</a>
</dd>
<dt>http://www.psychfic.com</dt>
<dd>
Use the URL of the story's chapter list, such as
<br /><a href="http://www.psychfic.com/viewstory.php?sid=1234">http://www.psychfic.com/viewstory.php?sid=1234</a>
</dd>
</dl>
<p>
+1 -1
View File
@@ -27,7 +27,7 @@ if __name__=="__main__":
exclude=['*.pyc','*~','*.xcf','*[0-9].png']
# from top dir. 'w' for overwrite
createZipFile(filename,"w",
['plugin-defaults.ini','plugin-example.ini','epubmerge.py','fanficdownloader'],
['plugin-defaults.ini','plugin-example.ini','fanficdownloader','downloader.py','defaults.ini'],
exclude=exclude)
#from calibre-plugin dir. 'a' for append
os.chdir('calibre-plugin')
+21 -5
View File
@@ -157,7 +157,7 @@ extratags: FanFiction
## metadata part(s) to look at, 2) a regular expression to match the
## template, and 3) the name of the GC setting to use, which must
## match exactly. Use this parameter in [defaults], or by site eg,
## [www.ficwad.com]
## [ficwad.com]
## Make sure to keep at least one space at the start of each line and
## to escape % to %%, if used.
## template => regexp to match => GC Setting to use.
@@ -269,6 +269,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
@@ -296,12 +298,13 @@ background_color: ffffff
## Allow customization of CSS. Make sure to keep at least one space
## at the start of each line and to escape % to %%. Also need
## background_color to be in the same section, if included in CSS.
## 'adobe-text-layout: optimizeSpeed;' prevents hyphenation on newer Nooks
## 'adobe-hyphenate: none;' prevents hyphenation on newer Nooks
## STR(wG) (1.2.1+ for sure)
output_css:
body { background-color: #%(background_color)s;
text-align: justify;
margin: 2%%;
adobe-text-layout: optimizeSpeed; }
adobe-hyphenate: none; }
pre { font-size: x-small; }
sml { font-size: small; }
h1 { text-align: center; }
@@ -343,7 +346,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
@@ -1007,7 +1010,7 @@ extratags:
## for examples of how to use them.
extra_valid_entries:reviews,favs,follows
[www.ficwad.com]
[ficwad.com]
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -1047,6 +1050,10 @@ extracategories:Harry Potter
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[www.henneth-annun.net]
## Site dedicated to these categories/characters/ships
extracategories:The Hobbit
[www.hpfandom.net]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
@@ -1134,6 +1141,15 @@ extracategories:Harry Potter
## Site dedicated to these categories/characters/ships
extracategories:Prison Break
[www.psychfic.com]
## Site dedicated to these categories/characters/ships
extracategories:Psych
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[www.qaf-fic.com]
## Site dedicated to these categories/characters/ships
extracategories:Queer as Folk
+1 -1
View File
@@ -51,7 +51,7 @@
## default is false
#collect_series: true
[www.ficwad.com]
[ficwad.com]
#username:YourUsername
#password:YourPassword