mirror of
https://github.com/wassname/FanFicFare.git
synced 2026-09-15 11:36:30 +08:00
Compare commits
141
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad95548dff | ||
|
|
0d184ef0d6 | ||
|
|
4da9e459d1 | ||
|
|
46e3b50ead | ||
|
|
6fb5701197 | ||
|
|
85d40e0399 | ||
|
|
2e331c8d78 | ||
|
|
08afa5f38a | ||
|
|
48d0a32b8d | ||
|
|
3a872c6bcf | ||
|
|
c1e4e2c8e4 | ||
|
|
400afe96c9 | ||
|
|
595cb0029c | ||
|
|
91dba79bff | ||
|
|
875c894f91 | ||
|
|
4a24275d4e | ||
|
|
0a71e95460 | ||
|
|
ddb18be7c2 | ||
|
|
bcaefbd720 | ||
|
|
36b377fcbf | ||
|
|
f2f8f0af37 | ||
|
|
bc4876d251 | ||
|
|
e03db70185 | ||
|
|
f8cbc755de | ||
|
|
b813c7966f | ||
|
|
8c914ecf4f | ||
|
|
cf00fc53f0 | ||
|
|
580ce2cf76 | ||
|
|
9212357fed | ||
|
|
2046019bae | ||
|
|
f910946ed0 | ||
|
|
d97fe121af | ||
|
|
5825ca61e3 | ||
|
|
91ba99dee5 | ||
|
|
5ca8ef00f0 | ||
|
|
10423af8ff | ||
|
|
5faf6cba00 | ||
|
|
f7ba0caeeb | ||
|
|
4390a731dc | ||
|
|
b5a2fa1395 | ||
|
|
d53b7411d9 | ||
|
|
88bd4f395f | ||
|
|
14f073dd20 | ||
|
|
ce4b03707d | ||
|
|
8863e1be63 | ||
|
|
a33172afc2 | ||
|
|
51cfe6500e | ||
|
|
d1ab33c3a4 | ||
|
|
1349244093 | ||
|
|
e222bc1ac6 | ||
|
|
55fac6a7e3 | ||
|
|
21120b7621 | ||
|
|
3ab313b97e | ||
|
|
cfbf491bd4 | ||
|
|
a576d9ef54 | ||
|
|
7d3b453ce8 | ||
|
|
6dfa478a2a | ||
|
|
7c90a62c04 | ||
|
|
00ded7ddcc | ||
|
|
fadd39bdc9 | ||
|
|
44c4cdd704 | ||
|
|
07c9bfd346 | ||
|
|
668fc7a211 | ||
|
|
a0f16425d2 | ||
|
|
4d96632b67 | ||
|
|
60b7eae72e | ||
|
|
0877c82744 | ||
|
|
69d04d0581 | ||
|
|
88376dba3b | ||
|
|
f2dd34c64e | ||
|
|
60cb243975 | ||
|
|
3d07ddbf4d | ||
|
|
2ea820936e | ||
|
|
da425ed96d | ||
|
|
86f037543c | ||
|
|
4a5e7c9fa1 | ||
|
|
9597dd4abe | ||
|
|
2fcdbca766 | ||
|
|
ca31872a9f | ||
|
|
77728005b1 | ||
|
|
647f674f05 | ||
|
|
9f289f8456 | ||
|
|
9197221e9c | ||
|
|
9e658112d3 | ||
|
|
e481faaf4e | ||
|
|
4e0ea4bad1 | ||
|
|
12c134911c | ||
|
|
4a80d5de46 | ||
|
|
c62f5f09c1 | ||
|
|
c270d3e689 | ||
|
|
6b7797e0f0 | ||
|
|
8eba002d36 | ||
|
|
7c895f520f | ||
|
|
4bb91cd0c5 | ||
|
|
e5b0974d0e | ||
|
|
0f77c2428b | ||
|
|
bd57353601 | ||
|
|
693863a28a | ||
|
|
db4ca65bb8 | ||
|
|
686223aa6e | ||
|
|
c06a4ee9de | ||
|
|
b66af0ef73 | ||
|
|
0ca71a6455 | ||
|
|
ab521ac093 | ||
|
|
a85266e908 | ||
|
|
2b3826d69b | ||
|
|
079a0f5906 | ||
|
|
f31fcc3341 | ||
|
|
7eb1685be1 | ||
|
|
30997053ef | ||
|
|
347eb69044 | ||
|
|
86329a1fe3 | ||
|
|
3bcc31a404 | ||
|
|
5e7c28d2dc | ||
|
|
574981931f | ||
|
|
a54765cf86 | ||
|
|
7cd77c64d1 | ||
|
|
fd68b32111 | ||
|
|
eb969cfe2f | ||
|
|
337da79dbc | ||
|
|
3176b4944b | ||
|
|
a18c52e5a3 | ||
|
|
1cfaff9d42 | ||
|
|
793c65495f | ||
|
|
efb908d438 | ||
|
|
6ea015b111 | ||
|
|
eab986a07a | ||
|
|
89b86545e0 | ||
|
|
3543eecfcc | ||
|
|
dc2370fed9 | ||
|
|
f83d9467fc | ||
|
|
a82a40a221 | ||
|
|
cbc2605804 | ||
|
|
6c6a84f533 | ||
|
|
1be2e50beb | ||
|
|
5c37bddca9 | ||
|
|
42473d4f1d | ||
|
|
850567afde | ||
|
|
30aa321c74 | ||
|
|
e7972b3d8e | ||
|
|
f538753b69 |
@@ -1,6 +1,6 @@
|
||||
# ffd-retief-hrd fanfictiondownloader
|
||||
application: fanfictiondownloader
|
||||
version: 4-4-21
|
||||
version: 4-4-35
|
||||
runtime: python27
|
||||
api_version: 1
|
||||
threadsafe: true
|
||||
|
||||
@@ -27,8 +27,8 @@ class FanFictionDownLoaderBase(InterfaceActionBase):
|
||||
description = 'UI plugin to download FanFiction stories from various sites.'
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
author = 'Jim Miller'
|
||||
version = (1, 5, 46)
|
||||
minimum_calibre_version = (0, 8, 30)
|
||||
version = (1, 7, 0)
|
||||
minimum_calibre_version = (0, 8, 57)
|
||||
|
||||
#: This field defines the GUI plugin class that contains all the code
|
||||
#: that actually does something. Its format is module_path:class_name
|
||||
|
||||
@@ -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.</p>
|
||||
plugins. bbcodeutils code contributed by Pau Sanchez.</p>
|
||||
|
||||
<p>
|
||||
Calibre officially distributes plugins from the mobileread.com forum site.
|
||||
|
||||
@@ -11,9 +11,11 @@ import os
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4.Qt import (Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout,
|
||||
QTableWidgetItem, QFont, QLineEdit, QComboBox,
|
||||
QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime)
|
||||
QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime,
|
||||
QTextEdit,
|
||||
QListWidget, QAbstractItemView)
|
||||
from calibre.constants import iswindows
|
||||
from calibre.gui2 import gprefs, error_dialog, UNDEFINED_QDATETIME
|
||||
from calibre.gui2 import gprefs, error_dialog, UNDEFINED_QDATETIME, info_dialog
|
||||
from calibre.gui2.actions import menu_action_unique_name
|
||||
from calibre.gui2.keyboard import ShortcutConfig
|
||||
from calibre.utils.config import config_dir
|
||||
@@ -194,7 +196,7 @@ class ImageTitleLayout(QHBoxLayout):
|
||||
'''
|
||||
A reusable layout widget displaying an image followed by a title
|
||||
'''
|
||||
def __init__(self, parent, icon_name, title):
|
||||
def __init__(self, parent, icon_name, title, tooltip=None):
|
||||
QHBoxLayout.__init__(self)
|
||||
title_image_label = QLabel(parent)
|
||||
pixmap = get_pixmap(icon_name)
|
||||
@@ -215,6 +217,9 @@ class ImageTitleLayout(QHBoxLayout):
|
||||
self.addWidget(shelf_label)
|
||||
self.insertStretch(-1)
|
||||
|
||||
if tooltip:
|
||||
title_image_label.setToolTip(tooltip)
|
||||
shelf_label.setToolTip(tooltip)
|
||||
|
||||
class SizePersistedDialog(QDialog):
|
||||
'''
|
||||
@@ -445,3 +450,97 @@ class DateDelegate(QStyledItemDelegate):
|
||||
model.setData(index, UNDEFINED_QDATETIME, Qt.EditRole)
|
||||
else:
|
||||
model.setData(index, QDateTime(val), Qt.EditRole)
|
||||
|
||||
class PrefsViewerDialog(SizePersistedDialog):
|
||||
|
||||
def __init__(self, gui, namespace):
|
||||
SizePersistedDialog.__init__(self, gui, 'Prefs Viewer dialog')
|
||||
self.setWindowTitle('Preferences for: '+namespace)
|
||||
|
||||
self.gui = gui
|
||||
self.db = gui.current_db
|
||||
self.namespace = namespace
|
||||
self._init_controls()
|
||||
self.resize_dialog()
|
||||
|
||||
self._populate_settings()
|
||||
|
||||
if self.keys_list.count():
|
||||
self.keys_list.setCurrentRow(0)
|
||||
|
||||
def _init_controls(self):
|
||||
layout = QVBoxLayout(self)
|
||||
self.setLayout(layout)
|
||||
|
||||
ml = QHBoxLayout()
|
||||
layout.addLayout(ml, 1)
|
||||
|
||||
self.keys_list = QListWidget(self)
|
||||
self.keys_list.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
self.keys_list.setFixedWidth(150)
|
||||
self.keys_list.setAlternatingRowColors(True)
|
||||
ml.addWidget(self.keys_list)
|
||||
self.value_text = QTextEdit(self)
|
||||
self.value_text.setTabStopWidth(24)
|
||||
self.value_text.setReadOnly(True)
|
||||
ml.addWidget(self.value_text, 1)
|
||||
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.Ok)
|
||||
button_box.accepted.connect(self.accept)
|
||||
self.clear_button = button_box.addButton('Clear', QDialogButtonBox.ResetRole)
|
||||
self.clear_button.setIcon(get_icon('trash.png'))
|
||||
self.clear_button.setToolTip('Clear all settings for this plugin')
|
||||
self.clear_button.clicked.connect(self._clear_settings)
|
||||
layout.addWidget(button_box)
|
||||
|
||||
def _populate_settings(self):
|
||||
self.keys_list.clear()
|
||||
ns_prefix = self._get_ns_prefix()
|
||||
keys = sorted([k[len(ns_prefix):] for k in self.db.prefs.iterkeys()
|
||||
if k.startswith(ns_prefix)])
|
||||
for key in keys:
|
||||
self.keys_list.addItem(key)
|
||||
self.keys_list.setMinimumWidth(self.keys_list.sizeHintForColumn(0))
|
||||
self.keys_list.currentRowChanged[int].connect(self._current_row_changed)
|
||||
|
||||
def _current_row_changed(self, new_row):
|
||||
if new_row < 0:
|
||||
self.value_text.clear()
|
||||
return
|
||||
key = unicode(self.keys_list.currentItem().text())
|
||||
val = self.db.prefs.get_namespaced(self.namespace, key, '')
|
||||
self.value_text.setPlainText(self.db.prefs.to_raw(val))
|
||||
|
||||
def _get_ns_prefix(self):
|
||||
return 'namespaced:%s:'% self.namespace
|
||||
|
||||
def _clear_settings(self):
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
message = '<p>Are you sure you want to clear your settings in this library for this plugin?</p>' \
|
||||
'<p>Any settings in other libraries or stored in a JSON file in your calibre plugins ' \
|
||||
'folder will not be touched.</p>' \
|
||||
'<p>You must restart calibre afterwards.</p>'
|
||||
if not confirm(message, self.namespace+'_clear_settings', self):
|
||||
return
|
||||
ns_prefix = self._get_ns_prefix()
|
||||
keys = [k for k in self.db.prefs.iterkeys() if k.startswith(ns_prefix)]
|
||||
for k in keys:
|
||||
del self.db.prefs[k]
|
||||
self._populate_settings()
|
||||
d = info_dialog(self, 'Settings deleted',
|
||||
'<p>All settings for this plugin in this library have been cleared.</p>'
|
||||
'<p>Please restart calibre now.</p>',
|
||||
show_copy_button=False)
|
||||
b = d.bb.addButton(_('Restart calibre now'), d.bb.AcceptRole)
|
||||
b.setIcon(QIcon(I('lt.png')))
|
||||
d.do_restart = False
|
||||
def rf():
|
||||
d.do_restart = True
|
||||
b.clicked.connect(rf)
|
||||
d.set_details('')
|
||||
d.exec_()
|
||||
b.clicked.disconnect()
|
||||
self.close()
|
||||
if d.do_restart:
|
||||
self.gui.quit(restart=True)
|
||||
|
||||
|
||||
+382
-125
@@ -4,136 +4,220 @@ from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Jim Miller'
|
||||
__copyright__ = '2012, Jim Miller'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import traceback, copy
|
||||
import traceback, copy, threading
|
||||
from collections import OrderedDict
|
||||
|
||||
from PyQt4.Qt import (QDialog, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QFont, QWidget,
|
||||
QTextEdit, QComboBox, QCheckBox, QPushButton, QTabWidget, QVariant, QScrollArea)
|
||||
from PyQt4.Qt import (QDialog, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QLineEdit, QFont, QWidget, QTextEdit, QComboBox,
|
||||
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.dialogs \
|
||||
import (UPDATE, UPDATEALWAYS, OVERWRITE, collision_order)
|
||||
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.adapters import getConfigSections
|
||||
import (UPDATE, UPDATEALWAYS, OVERWRITE, collision_order, RejectListDialog,
|
||||
EditTextDialog)
|
||||
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.adapters \
|
||||
import (getConfigSections, getNormalStoryURL)
|
||||
|
||||
from calibre_plugins.fanfictiondownloader_plugin.common_utils \
|
||||
import ( get_library_uuid, KeyboardConfigDialog )
|
||||
import ( get_library_uuid, KeyboardConfigDialog, PrefsViewerDialog )
|
||||
|
||||
from calibre.gui2.complete import MultiCompleteLineEdit
|
||||
|
||||
# This is where all preferences for this plugin will be stored
|
||||
PREFS_NAMESPACE = 'FanFictionDownLoaderPlugin'
|
||||
PREFS_KEY_SETTINGS = 'settings'
|
||||
|
||||
# Set defaults used by all. Library specific settings continue to
|
||||
# take from here.
|
||||
default_prefs = {}
|
||||
default_prefs['personal.ini'] = get_resources('plugin-example.ini')
|
||||
default_prefs['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
|
||||
all_prefs = JSONConfig('plugins/fanfictiondownloader_plugin')
|
||||
|
||||
# Set defaults used by all. Library specific settings continue to
|
||||
# take from here.
|
||||
all_prefs.defaults['personal.ini'] = get_resources('plugin-example.ini')
|
||||
|
||||
all_prefs.defaults['updatemeta'] = True
|
||||
all_prefs.defaults['updatecover'] = False
|
||||
all_prefs.defaults['updateepubcover'] = False
|
||||
all_prefs.defaults['keeptags'] = False
|
||||
all_prefs.defaults['urlsfromclip'] = True
|
||||
all_prefs.defaults['updatedefault'] = True
|
||||
all_prefs.defaults['fileform'] = 'epub'
|
||||
all_prefs.defaults['collision'] = OVERWRITE
|
||||
all_prefs.defaults['deleteotherforms'] = False
|
||||
all_prefs.defaults['adddialogstaysontop'] = False
|
||||
all_prefs.defaults['includeimages'] = False
|
||||
all_prefs.defaults['lookforurlinhtml'] = False
|
||||
all_prefs.defaults['injectseries'] = False
|
||||
|
||||
all_prefs.defaults['send_lists'] = ''
|
||||
all_prefs.defaults['read_lists'] = ''
|
||||
all_prefs.defaults['addtolists'] = False
|
||||
all_prefs.defaults['addtoreadlists'] = False
|
||||
all_prefs.defaults['addtolistsonread'] = False
|
||||
|
||||
all_prefs.defaults['gcnewonly'] = False
|
||||
all_prefs.defaults['gc_site_settings'] = {}
|
||||
all_prefs.defaults['allow_gc_from_ini'] = True
|
||||
|
||||
all_prefs.defaults['countpagesstats'] = []
|
||||
|
||||
all_prefs.defaults['errorcol'] = ''
|
||||
all_prefs.defaults['custom_cols'] = {}
|
||||
|
||||
# The list of settings to copy from all_prefs or the previous library
|
||||
# when config is called for the first time on a library.
|
||||
copylist = ['personal.ini',
|
||||
'updatemeta',
|
||||
'updatecover',
|
||||
'updateepubcover',
|
||||
'keeptags',
|
||||
'urlsfromclip',
|
||||
'updatedefault',
|
||||
'fileform',
|
||||
'collision',
|
||||
'deleteotherforms',
|
||||
'adddialogstaysontop',
|
||||
'includeimages',
|
||||
'lookforurlinhtml',
|
||||
'injectseries',
|
||||
'gcnewonly',
|
||||
'gc_site_settings',
|
||||
'allow_gc_from_ini']
|
||||
old_prefs = JSONConfig('plugins/fanfictiondownloader_plugin')
|
||||
|
||||
# fake out so I don't have to change the prefs calls anywhere. The
|
||||
# Java programmer in me is offended by op-overloading, but it's very
|
||||
# tidy.
|
||||
class PrefsFacade():
|
||||
def __init__(self,all_prefs):
|
||||
self.all_prefs = all_prefs
|
||||
self.lastlibid = None
|
||||
|
||||
def _get_copylist_prefs(self,frompref):
|
||||
return filter( lambda x : x[0] in copylist, frompref.items() )
|
||||
def __init__(self,default_prefs):
|
||||
self.default_prefs = default_prefs
|
||||
self.libraryid = None
|
||||
self.current_prefs = None
|
||||
|
||||
def _get_prefs(self):
|
||||
libraryid = get_library_uuid(get_gui().current_db)
|
||||
if libraryid not in self.all_prefs:
|
||||
if self.lastlibid == None:
|
||||
self.all_prefs[libraryid] = dict(self._get_copylist_prefs(self.all_prefs))
|
||||
else:
|
||||
self.all_prefs[libraryid] = dict(self._get_copylist_prefs(self.all_prefs[self.lastlibid]))
|
||||
|
||||
self.lastlibid = libraryid
|
||||
|
||||
return self.all_prefs[libraryid]
|
||||
|
||||
def _save_prefs(self,prefs):
|
||||
libraryid = get_library_uuid(get_gui().current_db)
|
||||
self.all_prefs[libraryid] = prefs
|
||||
if self.current_prefs == None or self.libraryid != libraryid:
|
||||
#print("self.current_prefs == None(%s) or self.libraryid != libraryid(%s)"%(self.current_prefs == None,self.libraryid != libraryid))
|
||||
self.libraryid = libraryid
|
||||
self.current_prefs = get_library_config()
|
||||
return self.current_prefs
|
||||
|
||||
def __getitem__(self,k):
|
||||
prefs = self._get_prefs()
|
||||
if k not in prefs:
|
||||
# pulls from all_prefs.defaults automatically if not set
|
||||
# in all_prefs
|
||||
return self.all_prefs[k]
|
||||
# pulls from default_prefs.defaults automatically if not set
|
||||
# in default_prefs
|
||||
return self.default_prefs[k]
|
||||
return prefs[k]
|
||||
|
||||
def __setitem__(self,k,v):
|
||||
prefs = self._get_prefs()
|
||||
prefs[k]=v
|
||||
self._save_prefs(prefs)
|
||||
# self._save_prefs(prefs)
|
||||
|
||||
# to be avoided--can cause unexpected results as possibly ancient
|
||||
# all_pref settings may be pulled.
|
||||
def __delitem__(self,k):
|
||||
prefs = self._get_prefs()
|
||||
del prefs[k]
|
||||
self._save_prefs(prefs)
|
||||
if k in prefs:
|
||||
del prefs[k]
|
||||
|
||||
prefs = PrefsFacade(all_prefs)
|
||||
def save_to_db(self):
|
||||
set_library_config(self._get_prefs())
|
||||
|
||||
prefs = PrefsFacade(default_prefs)
|
||||
|
||||
class RejectURLList:
|
||||
def __init__(self,prefs):
|
||||
self.prefs = prefs
|
||||
self.sync_lock = threading.RLock()
|
||||
self.listcache = None
|
||||
|
||||
def _read_list_from_text(self,text):
|
||||
cache = {}
|
||||
for line in text.splitlines():
|
||||
if ',' in line:
|
||||
(rejurl,note) = line.split(',',1)
|
||||
else:
|
||||
(rejurl,note) = (line,'')
|
||||
rejurl = getNormalStoryURL(rejurl)
|
||||
if rejurl:
|
||||
cache[rejurl] = note
|
||||
return cache
|
||||
|
||||
|
||||
def _get_listcache(self):
|
||||
if self.listcache == None:
|
||||
self.listcache = self._read_list_from_text(prefs['rejecturls'])
|
||||
return self.listcache
|
||||
|
||||
def _save_list(self,listcache):
|
||||
rejectlist = []
|
||||
for url in listcache:
|
||||
rejectlist.append("%s,%s"%(url,listcache[url]))
|
||||
|
||||
self.prefs['rejecturls'] = '\n'.join(rejectlist)
|
||||
self.prefs.save_to_db()
|
||||
self.listcache = None
|
||||
|
||||
def check(self,url):
|
||||
with self.sync_lock:
|
||||
listcache = self._get_listcache()
|
||||
if url in listcache:
|
||||
note = listcache[url]
|
||||
return note
|
||||
|
||||
# not found
|
||||
return None
|
||||
|
||||
def remove(self,url):
|
||||
with self.sync_lock:
|
||||
listcache = self._get_listcache()
|
||||
if url in listcache:
|
||||
del listcache[url]
|
||||
self._save_list(listcache)
|
||||
|
||||
def add_text(self,rejecttext):
|
||||
self.add(self._read_list_from_text(rejecttext).items())
|
||||
|
||||
def add(self,rejectlist,clear=False):
|
||||
# rejectlist=list of (url,note) tuples.
|
||||
with self.sync_lock:
|
||||
if clear:
|
||||
listcache={}
|
||||
else:
|
||||
listcache = self._get_listcache()
|
||||
for (url,note) in rejectlist:
|
||||
listcache[url]=note
|
||||
self._save_list(listcache)
|
||||
|
||||
def get_list(self):
|
||||
return copy.deepcopy(self._get_listcache())
|
||||
|
||||
def get_reject_reasons(self):
|
||||
return self.prefs['rejectreasons'].splitlines()
|
||||
|
||||
rejecturllist = RejectURLList(prefs)
|
||||
|
||||
class ConfigWidget(QWidget):
|
||||
|
||||
@@ -157,6 +241,9 @@ class ConfigWidget(QWidget):
|
||||
self.personalini_tab = PersonalIniTab(self, plugin_action)
|
||||
tab_widget.addTab(self.personalini_tab, 'personal.ini')
|
||||
|
||||
# self.rejecturls_tab = RejectUrlsTab(self, plugin_action)
|
||||
# tab_widget.addTab(self.rejecturls_tab, 'Reject URLs')
|
||||
|
||||
self.readinglist_tab = ReadingListTab(self, plugin_action)
|
||||
tab_widget.addTab(self.readinglist_tab, 'Reading Lists')
|
||||
if 'Reading List' not in plugin_action.gui.iactions:
|
||||
@@ -172,8 +259,11 @@ class ConfigWidget(QWidget):
|
||||
if 'Count Pages' not in plugin_action.gui.iactions:
|
||||
self.countpages_tab.setEnabled(False)
|
||||
|
||||
self.columns_tab = CustomColumnsTab(self, plugin_action)
|
||||
tab_widget.addTab(self.columns_tab, 'Custom Columns')
|
||||
self.std_columns_tab = StandardColumnsTab(self, plugin_action)
|
||||
tab_widget.addTab(self.std_columns_tab, 'Standard Columns')
|
||||
|
||||
self.cust_columns_tab = CustomColumnsTab(self, plugin_action)
|
||||
tab_widget.addTab(self.cust_columns_tab, 'Custom Columns')
|
||||
|
||||
self.other_tab = OtherTab(self, plugin_action)
|
||||
tab_widget.addTab(self.other_tab, 'Other')
|
||||
@@ -241,19 +331,34 @@ class ConfigWidget(QWidget):
|
||||
|
||||
prefs['countpagesstats'] = countpagesstats
|
||||
|
||||
# Standard Columns tab
|
||||
colsnewonly = {}
|
||||
for (col,checkbox) in self.std_columns_tab.stdcol_newonlycheck.iteritems():
|
||||
colsnewonly[col] = checkbox.isChecked()
|
||||
prefs['std_cols_newonly'] = colsnewonly
|
||||
|
||||
# Custom Columns tab
|
||||
# error column
|
||||
prefs['errorcol'] = unicode(self.columns_tab.errorcol.itemData(self.columns_tab.errorcol.currentIndex()).toString())
|
||||
prefs['errorcol'] = unicode(self.cust_columns_tab.errorcol.itemData(self.cust_columns_tab.errorcol.currentIndex()).toString())
|
||||
|
||||
# cust cols
|
||||
# cust cols tab
|
||||
colsmap = {}
|
||||
for (col,combo) in self.columns_tab.custcol_dropdowns.iteritems():
|
||||
for (col,combo) in self.cust_columns_tab.custcol_dropdowns.iteritems():
|
||||
val = unicode(combo.itemData(combo.currentIndex()).toString())
|
||||
if val != 'none':
|
||||
colsmap[col] = val
|
||||
#print("colsmap[%s]:%s"%(col,colsmap[col]))
|
||||
prefs['custom_cols'] = colsmap
|
||||
|
||||
colsnewonly = {}
|
||||
for (col,checkbox) in self.cust_columns_tab.custcol_newonlycheck.iteritems():
|
||||
colsnewonly[col] = checkbox.isChecked()
|
||||
prefs['custom_cols_newonly'] = colsnewonly
|
||||
|
||||
prefs['allow_custcol_from_ini'] = self.cust_columns_tab.allow_custcol_from_ini.isChecked()
|
||||
|
||||
prefs.save_to_db()
|
||||
|
||||
def edit_shortcuts(self):
|
||||
self.save_settings()
|
||||
# Force the menus to be rebuilt immediately, so we have all our actions registered
|
||||
@@ -311,7 +416,7 @@ class BasicTab(QWidget):
|
||||
self.l.addLayout(horz)
|
||||
|
||||
self.updatemeta = QCheckBox('Default Update Calibre &Metadata?',self)
|
||||
self.updatemeta.setToolTip("On each download, FFDL offers an option to update Calibre's metadata (title, author, URL, tags, custom columns, etc) from the web site. <br />This sets whether that will default to on or off.")
|
||||
self.updatemeta.setToolTip("On each download, FFDL offers an option to update Calibre's metadata (title, author, URL, tags, custom columns, etc) from the web site. <br />This sets whether that will default to on or off. <br />Columns set to 'New Only' in the column tabs will only be set for new books.")
|
||||
self.updatemeta.setChecked(prefs['updatemeta'])
|
||||
self.l.addWidget(self.updatemeta)
|
||||
|
||||
@@ -320,16 +425,25 @@ class BasicTab(QWidget):
|
||||
self.updateepubcover.setChecked(prefs['updateepubcover'])
|
||||
self.l.addWidget(self.updateepubcover)
|
||||
|
||||
self.l.addSpacing(10)
|
||||
|
||||
self.deleteotherforms = QCheckBox('Delete other existing formats?',self)
|
||||
self.deleteotherforms.setToolTip('Check this to automatically delete all other ebook formats when updating an existing book.\nHandy if you have both a Nook(epub) and Kindle(mobi), for example.')
|
||||
self.deleteotherforms.setChecked(prefs['deleteotherforms'])
|
||||
self.l.addWidget(self.deleteotherforms)
|
||||
|
||||
self.updatecover = QCheckBox('Update Calibre Cover when Updating Metadata?',self)
|
||||
self.updatecover.setToolTip("Update calibre book cover image from EPUB when metadata is updated. (EPUB only.)\nDoesn't go looking for new images on 'Update Calibre Metadata Only'.")
|
||||
self.updatecover.setChecked(prefs['updatecover'])
|
||||
self.l.addWidget(self.updatecover)
|
||||
|
||||
self.keeptags = QCheckBox('Keep Existing Tags when Updating Metadata?',self)
|
||||
self.keeptags.setToolTip('Existing tags will be kept and any new tags added.\nCompleted and In-Progress tags will be still be updated, if known.\nLast Updated tags will be updated if lastupdate in include_subject_tags.')
|
||||
self.keeptags.setToolTip("Existing tags will be kept and any new tags added.\nCompleted and In-Progress tags will be still be updated, if known.\nLast Updated tags will be updated if lastupdate in include_subject_tags.\n(If Tags is set to 'New Only' in the Standard Columns tab, this has no effect.)")
|
||||
self.keeptags.setChecked(prefs['keeptags'])
|
||||
self.l.addWidget(self.keeptags)
|
||||
|
||||
self.l.addSpacing(10)
|
||||
|
||||
self.urlsfromclip = QCheckBox('Take URLs from Clipboard?',self)
|
||||
self.urlsfromclip.setToolTip('Prefill URLs from valid URLs in Clipboard when Adding New.')
|
||||
self.urlsfromclip.setChecked(prefs['urlsfromclip'])
|
||||
@@ -341,16 +455,13 @@ class BasicTab(QWidget):
|
||||
self.updatedefault.setChecked(prefs['updatedefault'])
|
||||
self.l.addWidget(self.updatedefault)
|
||||
|
||||
self.deleteotherforms = QCheckBox('Delete other existing formats?',self)
|
||||
self.deleteotherforms.setToolTip('Check this to automatically delete all other ebook formats when updating an existing book.\nHandy if you have both a Nook(epub) and Kindle(mobi), for example.')
|
||||
self.deleteotherforms.setChecked(prefs['deleteotherforms'])
|
||||
self.l.addWidget(self.deleteotherforms)
|
||||
|
||||
self.adddialogstaysontop = QCheckBox("Keep 'Add New from URL(s)' dialog on top?",self)
|
||||
self.adddialogstaysontop.setToolTip("Instructs the OS and Window Manager to keep the 'Add New from URL(s)'\ndialog on top of all other windows. Useful for dragging URLs onto it.")
|
||||
self.adddialogstaysontop.setChecked(prefs['adddialogstaysontop'])
|
||||
self.l.addWidget(self.adddialogstaysontop)
|
||||
|
||||
self.l.addSpacing(10)
|
||||
|
||||
# this is a cheat to make it easier for users to realize there's a new include_images features.
|
||||
self.includeimages = QCheckBox("Include images in EPUBs?",self)
|
||||
self.includeimages.setToolTip("Download and include images in EPUB stories. This is equivalent to adding:\n\n[epub]\ninclude_images:true\nkeep_summary_html:true\nmake_firstimage_cover:true\n\n ...to the top of personal.ini. Your settings in personal.ini will override this.")
|
||||
@@ -367,6 +478,27 @@ class BasicTab(QWidget):
|
||||
self.injectseries.setChecked(prefs['injectseries'])
|
||||
self.l.addWidget(self.injectseries)
|
||||
|
||||
self.l.addSpacing(10)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
|
||||
self.rejectlist = QPushButton('Edit Reject URL List', self)
|
||||
self.rejectlist.setToolTip("Edit list of URLs FFDL will automatically Reject.")
|
||||
self.rejectlist.clicked.connect(self.show_rejectlist)
|
||||
horz.addWidget(self.rejectlist)
|
||||
|
||||
self.reject_urls = QPushButton('Add Reject URLs', self)
|
||||
self.reject_urls.setToolTip("Add additional URLs to Reject as text.")
|
||||
self.reject_urls.clicked.connect(self.add_reject_urls)
|
||||
horz.addWidget(self.reject_urls)
|
||||
|
||||
self.reject_reasons = QPushButton('Edit Reject Reasons List', self)
|
||||
self.reject_reasons.setToolTip("Customize the Reasons presented when Rejecting URLs")
|
||||
self.reject_reasons.clicked.connect(self.show_reject_reasons)
|
||||
horz.addWidget(self.reject_reasons)
|
||||
|
||||
self.l.addLayout(horz)
|
||||
|
||||
self.l.insertStretch(-1)
|
||||
|
||||
def set_collisions(self):
|
||||
@@ -382,7 +514,50 @@ class BasicTab(QWidget):
|
||||
def show_defaults(self):
|
||||
text = get_resources('plugin-defaults.ini')
|
||||
ShowDefaultsIniDialog(self.windowIcon(),text,self).exec_()
|
||||
|
||||
def show_rejectlist(self):
|
||||
rejectlist = []
|
||||
for (url,note) in rejecturllist.get_list().items():
|
||||
rejectlist.append((None,url,note,note))
|
||||
|
||||
d = RejectListDialog(self,
|
||||
rejectlist,
|
||||
rejectreasons=rejecturllist.get_reject_reasons(),
|
||||
header="Edit Reject URLs List",
|
||||
show_delete=False)
|
||||
d.exec_()
|
||||
|
||||
if d.result() != d.Accepted:
|
||||
return
|
||||
|
||||
rejectlist=[]
|
||||
for (bookid,url,note) in d.get_reject_list():
|
||||
rejectlist.append((url,note))
|
||||
|
||||
rejecturllist.add(rejectlist,clear=True)
|
||||
|
||||
def show_reject_reasons(self):
|
||||
d = EditTextDialog(self,
|
||||
prefs['rejectreasons'],
|
||||
icon=self.windowIcon(),
|
||||
title="Reject Reasons",
|
||||
label="Customize Reject List Reasons",
|
||||
tooltip="Customize the Reasons presented when Rejecting URLs")
|
||||
d.exec_()
|
||||
if d.result() == d.Accepted:
|
||||
prefs['rejectreasons'] = d.get_plain_text()
|
||||
|
||||
def add_reject_urls(self):
|
||||
d = EditTextDialog(self,
|
||||
"http://example.com?story.php?sid=5,Reason why I rejected it",
|
||||
icon=self.windowIcon(),
|
||||
title="Add Reject URLs",
|
||||
label="Add Reject URLs. Use: <b>http://...,note</b>",
|
||||
tooltip="One URL per line, everything after <b>,</b> will be put in the note.")
|
||||
d.exec_()
|
||||
if d.result() == d.Accepted:
|
||||
rejecturllist.add_text(d.get_plain_text())
|
||||
|
||||
class PersonalIniTab(QWidget):
|
||||
|
||||
def __init__(self, parent_dialog, plugin_action):
|
||||
@@ -411,7 +586,7 @@ class PersonalIniTab(QWidget):
|
||||
self.ini.setText(prefs['personal.ini'])
|
||||
self.l.addWidget(self.ini)
|
||||
|
||||
self.defaults = QPushButton('View Defaults', self)
|
||||
self.defaults = QPushButton('View Defaults (plugin-defaults.ini)', self)
|
||||
self.defaults.setToolTip("View all of the plugin's configurable settings\nand their default settings.")
|
||||
self.defaults.clicked.connect(self.show_defaults)
|
||||
self.l.addWidget(self.defaults)
|
||||
@@ -422,7 +597,7 @@ class PersonalIniTab(QWidget):
|
||||
def show_defaults(self):
|
||||
text = get_resources('plugin-defaults.ini')
|
||||
ShowDefaultsIniDialog(self.windowIcon(),text,self).exec_()
|
||||
|
||||
|
||||
class ShowDefaultsIniDialog(QDialog):
|
||||
|
||||
def __init__(self, icon, text, parent=None):
|
||||
@@ -430,7 +605,7 @@ class ShowDefaultsIniDialog(QDialog):
|
||||
self.resize(600, 500)
|
||||
self.l = QVBoxLayout()
|
||||
self.setLayout(self.l)
|
||||
self.label = QLabel("Plugin Defaults (Read-Only)")
|
||||
self.label = QLabel("Plugin Defaults (plugin-defaults.ini) (Read-Only)")
|
||||
self.label.setToolTip("These are all of the plugin's configurable options\nand their default settings.")
|
||||
self.setWindowTitle(_('Plugin Defaults'))
|
||||
self.setWindowIcon(icon)
|
||||
@@ -569,6 +744,8 @@ class GenerateCoverTab(QWidget):
|
||||
horz.addWidget(dropdown)
|
||||
self.sl.addLayout(horz)
|
||||
|
||||
self.sl.insertStretch(-1)
|
||||
|
||||
self.gcnewonly = QCheckBox("Run Generate Cover Only on New Books",self)
|
||||
self.gcnewonly.setToolTip("Default is to run GC any time the calibre metadata is updated.")
|
||||
self.gcnewonly.setChecked(prefs['gcnewonly'])
|
||||
@@ -579,8 +756,6 @@ class GenerateCoverTab(QWidget):
|
||||
self.allow_gc_from_ini.setChecked(prefs['allow_gc_from_ini'])
|
||||
self.l.addWidget(self.allow_gc_from_ini)
|
||||
|
||||
self.l.insertStretch(-1)
|
||||
|
||||
class CountPagesTab(QWidget):
|
||||
|
||||
def __init__(self, parent_dialog, plugin_action):
|
||||
@@ -602,27 +777,27 @@ class CountPagesTab(QWidget):
|
||||
self.l.addSpacing(5)
|
||||
|
||||
# 'PageCount', 'WordCount', 'FleschReading', 'FleschGrade', 'GunningFog'
|
||||
self.pagecount = QCheckBox('PageCount',self)
|
||||
self.pagecount = QCheckBox('Page Count',self)
|
||||
self.pagecount.setToolTip('Which column and algorithm to use are configured in Count Pages.')
|
||||
self.pagecount.setChecked('PageCount' in prefs['countpagesstats'])
|
||||
self.l.addWidget(self.pagecount)
|
||||
|
||||
self.wordcount = QCheckBox('WordCount',self)
|
||||
self.wordcount = QCheckBox('Word Count',self)
|
||||
self.wordcount.setToolTip('Which column and algorithm to use are configured in Count Words.\nWill overwrite word count from FFDL metadata if set to update the same custom column.')
|
||||
self.wordcount.setChecked('WordCount' in prefs['countpagesstats'])
|
||||
self.l.addWidget(self.wordcount)
|
||||
|
||||
self.fleschreading = QCheckBox('FleschReading',self)
|
||||
self.fleschreading = QCheckBox('Flesch Reading Ease',self)
|
||||
self.fleschreading.setToolTip('Which column and algorithm to use are configured in Count Pages.')
|
||||
self.fleschreading.setChecked('FleschReading' in prefs['countpagesstats'])
|
||||
self.l.addWidget(self.fleschreading)
|
||||
|
||||
self.fleschgrade = QCheckBox('Fleschgrade',self)
|
||||
self.fleschgrade = QCheckBox('Flesch-Kincaid Grade Level',self)
|
||||
self.fleschgrade.setToolTip('Which column and algorithm to use are configured in Count Pages.')
|
||||
self.fleschgrade.setChecked('Fleschgrade' in prefs['countpagesstats'])
|
||||
self.fleschgrade.setChecked('FleschGrade' in prefs['countpagesstats'])
|
||||
self.l.addWidget(self.fleschgrade)
|
||||
|
||||
self.gunningfog = QCheckBox('GunningFog',self)
|
||||
self.gunningfog = QCheckBox('Gunning Fog Index',self)
|
||||
self.gunningfog.setToolTip('Which column and algorithm to use are configured in Count Pages.')
|
||||
self.gunningfog.setChecked('GunningFog' in prefs['countpagesstats'])
|
||||
self.l.addWidget(self.gunningfog)
|
||||
@@ -656,6 +831,12 @@ class OtherTab(QWidget):
|
||||
reset_confirmation_button.clicked.connect(self.reset_dialogs)
|
||||
self.l.addWidget(reset_confirmation_button)
|
||||
|
||||
view_prefs_button = QPushButton('&View library preferences...', self)
|
||||
view_prefs_button.setToolTip(_(
|
||||
'View data stored in the library database for this plugin'))
|
||||
view_prefs_button.clicked.connect(self.view_prefs)
|
||||
self.l.addWidget(view_prefs_button)
|
||||
|
||||
self.l.insertStretch(-1)
|
||||
|
||||
def reset_dialogs(self):
|
||||
@@ -667,6 +848,10 @@ class OtherTab(QWidget):
|
||||
_('Confirmation dialogs have all been reset'),
|
||||
show=True,
|
||||
show_copy_button=False)
|
||||
|
||||
def view_prefs(self):
|
||||
d = PrefsViewerDialog(self.plugin_action.gui, PREFS_NAMESPACE)
|
||||
d.exec_()
|
||||
|
||||
permitted_values = {
|
||||
'int' : ['numWords','numChapters'],
|
||||
@@ -679,6 +864,7 @@ permitted_values = {
|
||||
'language',
|
||||
'series',
|
||||
'characters',
|
||||
'ships',
|
||||
'status',
|
||||
'datePublished',
|
||||
'dateUpdated',
|
||||
@@ -696,10 +882,10 @@ permitted_values = {
|
||||
'description',
|
||||
'author',
|
||||
'authorUrl',
|
||||
'formatname'
|
||||
'formatname',
|
||||
'version'
|
||||
#,'formatext' # not useful information.
|
||||
#,'siteabbrev'
|
||||
#,'version'
|
||||
]
|
||||
}
|
||||
# no point copying the whole list.
|
||||
@@ -715,6 +901,7 @@ titleLabels = {
|
||||
'status-I':'Status:In-Progress',
|
||||
'series':'Series',
|
||||
'characters':'Characters',
|
||||
'ships':'Relationships',
|
||||
'datePublished':'Published',
|
||||
'dateUpdated':'Updated',
|
||||
'dateCreated':'Packaged',
|
||||
@@ -755,7 +942,17 @@ class CustomColumnsTab(QWidget):
|
||||
self.l.addSpacing(5)
|
||||
|
||||
self.custcol_dropdowns = {}
|
||||
self.custcol_newonlycheck = {}
|
||||
|
||||
scrollable = QScrollArea()
|
||||
scrollcontent = QWidget()
|
||||
scrollable.setWidget(scrollcontent)
|
||||
scrollable.setWidgetResizable(True)
|
||||
self.l.addWidget(scrollable)
|
||||
|
||||
self.sl = QVBoxLayout()
|
||||
scrollcontent.setLayout(self.sl)
|
||||
|
||||
for key, column in custom_columns.iteritems():
|
||||
|
||||
if column['datatype'] in permitted_values:
|
||||
@@ -763,8 +960,8 @@ class CustomColumnsTab(QWidget):
|
||||
# for (k,v) in column.iteritems():
|
||||
# print("column['%s'] => %s"%(k,v))
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel('%s(%s)'%(column['name'],key))
|
||||
label.setToolTip("Update this %s column with..."%column['datatype'])
|
||||
label = QLabel(column['name'])
|
||||
label.setToolTip("Update this %s column(%s) with..."%(key,column['datatype']))
|
||||
horz.addWidget(label)
|
||||
dropdown = QComboBox(self)
|
||||
dropdown.addItem('',QVariant('none'))
|
||||
@@ -777,17 +974,29 @@ class CustomColumnsTab(QWidget):
|
||||
dropdown.setToolTip("Metadata values valid for this type of column.\nValues that aren't valid for this enumeration column will be ignored.")
|
||||
else:
|
||||
dropdown.setToolTip("Metadata values valid for this type of column.")
|
||||
|
||||
horz.addWidget(dropdown)
|
||||
self.l.addLayout(horz)
|
||||
|
||||
self.l.insertStretch(-1)
|
||||
|
||||
newonlycheck = QCheckBox("New Only",self)
|
||||
newonlycheck.setToolTip("Write to %s(%s) only for new\nbooks, not updates to existing books."%(column['name'],key))
|
||||
self.custcol_newonlycheck[key] = newonlycheck
|
||||
if key in prefs['custom_cols_newonly']:
|
||||
newonlycheck.setChecked(prefs['custom_cols_newonly'][key])
|
||||
horz.addWidget(newonlycheck)
|
||||
|
||||
self.sl.addLayout(horz)
|
||||
|
||||
self.sl.insertStretch(-1)
|
||||
|
||||
self.l.addSpacing(5)
|
||||
self.allow_custcol_from_ini = QCheckBox('Allow custom_columns_settings from personal.ini to override',self)
|
||||
self.allow_custcol_from_ini.setToolTip("The personal.ini parameter custom_columns_settings allows you to set custom columns to site specific values that aren't common to all sites.<br \>custom_columns_settings is ignored when this is off.")
|
||||
self.allow_custcol_from_ini.setChecked(prefs['allow_custcol_from_ini'])
|
||||
self.l.addWidget(self.allow_custcol_from_ini)
|
||||
|
||||
self.l.addSpacing(5)
|
||||
label = QLabel("Special column:")
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
self.l.addSpacing(5)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel("Update/Overwrite Error Column:")
|
||||
@@ -803,6 +1012,54 @@ class CustomColumnsTab(QWidget):
|
||||
self.errorcol.setCurrentIndex(self.errorcol.findData(QVariant(prefs['errorcol'])))
|
||||
horz.addWidget(self.errorcol)
|
||||
self.l.addLayout(horz)
|
||||
|
||||
|
||||
|
||||
#print("prefs['custom_cols'] %s"%prefs['custom_cols'])
|
||||
|
||||
|
||||
class StandardColumnsTab(QWidget):
|
||||
|
||||
def __init__(self, parent_dialog, plugin_action):
|
||||
self.parent_dialog = parent_dialog
|
||||
self.plugin_action = plugin_action
|
||||
QWidget.__init__(self)
|
||||
|
||||
columns=OrderedDict()
|
||||
|
||||
columns["title"]="Title"
|
||||
columns["authors"]="Author(s)"
|
||||
columns["publisher"]="Publisher"
|
||||
columns["tags"]="Tags"
|
||||
columns["languages"]="Languages"
|
||||
columns["pubdate"]="Published Date"
|
||||
columns["timestamp"]="Date"
|
||||
columns["comments"]="Comments"
|
||||
columns["series"]="Series"
|
||||
columns["identifiers"]="Ids(url id only)"
|
||||
|
||||
self.l = QVBoxLayout()
|
||||
self.setLayout(self.l)
|
||||
|
||||
label = QLabel("The standard calibre metadata columns are listed below. You may choose whether FFDL will fill each column automatically on updates or only for new books.")
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
self.l.addSpacing(5)
|
||||
|
||||
self.stdcol_newonlycheck = {}
|
||||
|
||||
for key, column in columns.iteritems():
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel(column)
|
||||
#label.setToolTip("Update this %s column(%s) with..."%(key,column['datatype']))
|
||||
horz.addWidget(label)
|
||||
|
||||
newonlycheck = QCheckBox("New Only",self)
|
||||
newonlycheck.setToolTip("Write to %s only for new\nbooks, not updates to existing books."%column)
|
||||
self.stdcol_newonlycheck[key] = newonlycheck
|
||||
if key in prefs['std_cols_newonly']:
|
||||
newonlycheck.setChecked(prefs['std_cols_newonly'][key])
|
||||
horz.addWidget(newonlycheck)
|
||||
|
||||
self.l.addLayout(horz)
|
||||
|
||||
self.l.insertStretch(-1)
|
||||
|
||||
|
||||
+300
-19
@@ -8,15 +8,19 @@ __copyright__ = '2011, Jim Miller'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import traceback
|
||||
from functools import partial
|
||||
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4.Qt import (QDialog, QTableWidget, QMessageBox, QVBoxLayout, QHBoxLayout, QGridLayout,
|
||||
QPushButton, QProgressDialog, QString, QLabel, QCheckBox, QIcon, QTextCursor,
|
||||
QTextEdit, QLineEdit, QInputDialog, QComboBox, QClipboard, QVariant,
|
||||
QProgressDialog, QTimer, QDialogButtonBox, QPixmap, Qt, QAbstractItemView )
|
||||
from PyQt4.Qt import (QDialog, QTableWidget, QMessageBox, QVBoxLayout, QHBoxLayout,
|
||||
QGridLayout, QPushButton, QProgressDialog, QString, QLabel,
|
||||
QCheckBox, QIcon, QTextCursor, QTextEdit, QLineEdit, QInputDialog,
|
||||
QComboBox, QClipboard, QVariant, QProgressDialog, QTimer,
|
||||
QDialogButtonBox, QPixmap, Qt, QAbstractItemView, SIGNAL,
|
||||
QTableWidgetItem )
|
||||
|
||||
from calibre.gui2 import error_dialog, warning_dialog, question_dialog, info_dialog
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
from calibre.gui2.complete2 import EditWithComplete
|
||||
|
||||
from calibre import confirm_config_name
|
||||
from calibre.gui2 import dynamic
|
||||
@@ -106,26 +110,21 @@ class AddNewDialog(SizePersistedDialog):
|
||||
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel('If Story Already Exists?')
|
||||
label.setToolTip("What to do if there's already an existing story with the same title and author.")
|
||||
horz.addWidget(label)
|
||||
self.collision = QComboBox(self)
|
||||
self.collision.setToolTip("What to do if there's already an existing story with the same URL or title and author.")
|
||||
# add collision options
|
||||
self.set_collisions()
|
||||
i = self.collision.findText(prefs['collision'])
|
||||
if i > -1:
|
||||
self.collision.setCurrentIndex(i)
|
||||
# self.collision.setToolTip(OVERWRITE+' will replace the existing story.\n'+
|
||||
# UPDATE+' will download new chapters only and add to existing EPUB.\n'+
|
||||
# ADDNEW+' will create a new story with the same title and author.\n'+
|
||||
# SKIP+' will not download existing stories.\n'+
|
||||
# CALIBREONLY+' will not download stories, but will update Calibre metadata.')
|
||||
label.setBuddy(self.collision)
|
||||
horz.addWidget(self.collision)
|
||||
self.l.addLayout(horz)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
self.updatemeta = QCheckBox('Update Calibre &Metadata?',self)
|
||||
self.updatemeta.setToolTip('Update metadata for story in Calibre from web site?')
|
||||
self.updatemeta.setToolTip("Update metadata for existing stories in Calibre from web site?\n(Columns set to 'New Only' in the column tabs will only be set for new books.)")
|
||||
self.updatemeta.setChecked(prefs['updatemeta'])
|
||||
horz.addWidget(self.updatemeta)
|
||||
|
||||
@@ -177,15 +176,17 @@ class FakeLineEdit():
|
||||
def text(self):
|
||||
pass
|
||||
|
||||
class CollectURLDialog(QDialog):
|
||||
class CollectURLDialog(SizePersistedDialog):
|
||||
'''
|
||||
Collect single url for get urls.
|
||||
'''
|
||||
def __init__(self, gui, title):
|
||||
QDialog.__init__(self, gui)
|
||||
def __init__(self, gui, title, url_text):
|
||||
SizePersistedDialog.__init__(self, gui, 'FanFictionDownLoader plugin:get story urls')
|
||||
self.gui = gui
|
||||
self.status=False
|
||||
|
||||
self.setMinimumWidth(300)
|
||||
|
||||
self.l = QGridLayout()
|
||||
self.setLayout(self.l)
|
||||
|
||||
@@ -194,6 +195,7 @@ class CollectURLDialog(QDialog):
|
||||
|
||||
self.l.addWidget(QLabel("URL:"),1,0)
|
||||
self.url = QLineEdit(self)
|
||||
self.url.setText(url_text)
|
||||
self.l.addWidget(self.url,1,1)
|
||||
|
||||
self.ok_button = QPushButton('OK', self)
|
||||
@@ -204,15 +206,16 @@ class CollectURLDialog(QDialog):
|
||||
self.cancel_button.clicked.connect(self.cancel)
|
||||
self.l.addWidget(self.cancel_button,2,1)
|
||||
|
||||
self.resize(self.sizeHint())
|
||||
# restore saved size.
|
||||
self.resize_dialog()
|
||||
|
||||
def ok(self):
|
||||
self.status=True
|
||||
self.hide()
|
||||
self.accept()
|
||||
|
||||
def cancel(self):
|
||||
self.status=False
|
||||
self.hide()
|
||||
self.reject()
|
||||
|
||||
class UserPassDialog(QDialog):
|
||||
'''
|
||||
@@ -431,9 +434,9 @@ class UpdateExistingDialog(SizePersistedDialog):
|
||||
options_layout.addWidget(self.fileform)
|
||||
|
||||
label = QLabel('Update Mode:')
|
||||
label.setToolTip("What sort of update to perform. May set default from plugin configuration.")
|
||||
options_layout.addWidget(label)
|
||||
self.collision = QComboBox(self)
|
||||
self.collision.setToolTip("What sort of update to perform. May set default from plugin configuration.")
|
||||
# add collision options
|
||||
self.set_collisions()
|
||||
i = self.collision.findText(prefs['collision'])
|
||||
@@ -444,7 +447,7 @@ class UpdateExistingDialog(SizePersistedDialog):
|
||||
options_layout.addWidget(self.collision)
|
||||
|
||||
self.updatemeta = QCheckBox('Update Calibre &Metadata?',self)
|
||||
self.updatemeta.setToolTip('Update metadata for story in Calibre from web site? May set default from plugin configuration.')
|
||||
self.updatemeta.setToolTip("Update metadata for existing stories in Calibre from web site?\n(Columns set to 'New Only' in the column tabs will only be set for new books.)")
|
||||
self.updatemeta.setChecked(prefs['updatemeta'])
|
||||
options_layout.addWidget(self.updatemeta)
|
||||
|
||||
@@ -713,3 +716,281 @@ class StoryListTableWidget(QTableWidget):
|
||||
self.setItem(dest_row, col, self.takeItem(src_row, col))
|
||||
self.removeRow(src_row)
|
||||
self.blockSignals(False)
|
||||
|
||||
class RejectListTableWidget(QTableWidget):
|
||||
|
||||
def __init__(self, parent,rejectreasons=[]):
|
||||
QTableWidget.__init__(self, parent)
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.rejectreasons = rejectreasons
|
||||
|
||||
def on_headersection_clicked(self):
|
||||
self.setSortingEnabled(True)
|
||||
|
||||
def populate_table(self, reject_list):
|
||||
self.clear()
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setRowCount(len(reject_list))
|
||||
header_labels = ['URL', 'Note']
|
||||
self.setColumnCount(len(header_labels))
|
||||
self.setHorizontalHeaderLabels(header_labels)
|
||||
self.horizontalHeader().setStretchLastSection(True)
|
||||
#self.verticalHeader().setDefaultSectionSize(24)
|
||||
self.verticalHeader().hide()
|
||||
|
||||
# need sortingEnbled to sort, but off to up & down.
|
||||
self.connect(self.horizontalHeader(),
|
||||
SIGNAL('sectionClicked(int)'),
|
||||
self.on_headersection_clicked)
|
||||
|
||||
# row is just row number.
|
||||
for row, rejectrow in enumerate(reject_list):
|
||||
self.populate_table_row(row,rejectrow)
|
||||
|
||||
self.resizeColumnsToContents()
|
||||
self.setMinimumColumnWidth(1, 100)
|
||||
self.setMinimumColumnWidth(2, 100)
|
||||
self.setMinimumSize(300, 0)
|
||||
|
||||
def setMinimumColumnWidth(self, col, minimum):
|
||||
if self.columnWidth(col) < minimum:
|
||||
self.setColumnWidth(col, minimum)
|
||||
|
||||
def populate_table_row(self, row, rejectrow):
|
||||
|
||||
(bookid,url,titleauth,oldrejnote) = rejectrow
|
||||
if oldrejnote:
|
||||
noteprefix = note = oldrejnote
|
||||
# incase the existing note ends with one of the known reasons.
|
||||
for reason in self.rejectreasons:
|
||||
if noteprefix.endswith(' - '+reason):
|
||||
noteprefix = noteprefix[:-len(' - '+reason)]
|
||||
break
|
||||
else:
|
||||
noteprefix = note = titleauth
|
||||
|
||||
if len(noteprefix) > 0:
|
||||
noteprefix = noteprefix+' - '
|
||||
|
||||
url_cell = ReadOnlyTableWidgetItem(url)
|
||||
url_cell.setData(Qt.UserRole, QVariant(bookid))
|
||||
url_cell.setToolTip('URL to add to the Reject List.')
|
||||
self.setItem(row, 0, url_cell)
|
||||
|
||||
note_cell = EditWithComplete(self)
|
||||
|
||||
# This is a more than slightly kludgey way to get
|
||||
# EditWithComplete to *not* alpha-order the reasons, but leave
|
||||
# them in the order entered. If
|
||||
# calibre.gui2.complete2.CompleteModel.set_items ever changes,
|
||||
# this function will need to also.
|
||||
def complete_model_set_items_kludge(self, items):
|
||||
items = [unicode(x.strip()) for x in items]
|
||||
items = [x for x in items if x]
|
||||
items = tuple(items)
|
||||
self.all_items = self.current_items = items
|
||||
self.current_prefix = ''
|
||||
self.reset()
|
||||
|
||||
note_cell.lineEdit().mcompleter.model().set_items = \
|
||||
partial(complete_model_set_items_kludge,
|
||||
note_cell.lineEdit().mcompleter.model())
|
||||
|
||||
items = [note]+[ noteprefix+x for x in self.rejectreasons ]
|
||||
note_cell.update_items_cache(items)
|
||||
note_cell.show_initial_value(note)
|
||||
note_cell.set_separator(None)
|
||||
note_cell.setToolTip('Select or Edit Reject Note.')
|
||||
self.setCellWidget(row, 1, note_cell)
|
||||
|
||||
# note_cell = QTableWidgetItem(note)
|
||||
# note_cell.setToolTip('Double-click to edit note.')
|
||||
# self.setItem(row, 1, note_cell)
|
||||
|
||||
def get_reject_list(self):
|
||||
rejectrows = []
|
||||
for row in range(self.rowCount()):
|
||||
bookid = self.item(row, 0).data(Qt.UserRole).toPyObject()
|
||||
url = unicode(self.item(row, 0).text())
|
||||
note = unicode(self.cellWidget(row, 1).currentText()).strip()
|
||||
rejectrows.append((bookid,url,note))
|
||||
return rejectrows
|
||||
|
||||
def remove_selected_rows(self):
|
||||
self.setFocus()
|
||||
rows = self.selectionModel().selectedRows()
|
||||
if len(rows) == 0:
|
||||
return
|
||||
message = '<p>Are you sure you want to remove this URL from the list?'
|
||||
if len(rows) > 1:
|
||||
message = '<p>Are you sure you want to remove the %d selected URLs from the list?'%len(rows)
|
||||
if not confirm(message,'ffdl_rejectlist_delete_item_again', self):
|
||||
return
|
||||
first_sel_row = self.currentRow()
|
||||
for selrow in reversed(rows):
|
||||
self.removeRow(selrow.row())
|
||||
if first_sel_row < self.rowCount():
|
||||
self.select_and_scroll_to_row(first_sel_row)
|
||||
elif self.rowCount() > 0:
|
||||
self.select_and_scroll_to_row(first_sel_row - 1)
|
||||
|
||||
def select_and_scroll_to_row(self, row):
|
||||
self.selectRow(row)
|
||||
self.scrollToItem(self.currentItem())
|
||||
|
||||
def move_rows_up(self):
|
||||
self.setFocus()
|
||||
rows = self.selectionModel().selectedRows()
|
||||
if len(rows) == 0:
|
||||
return
|
||||
first_sel_row = rows[0].row()
|
||||
if first_sel_row <= 0:
|
||||
return
|
||||
# Workaround for strange selection bug in Qt which "alters" the selection
|
||||
# in certain circumstances which meant move down only worked properly "once"
|
||||
selrows = []
|
||||
for row in rows:
|
||||
selrows.append(row.row())
|
||||
selrows.sort()
|
||||
for selrow in selrows:
|
||||
self.swap_row_widgets(selrow - 1, selrow + 1)
|
||||
scroll_to_row = first_sel_row - 1
|
||||
if scroll_to_row > 0:
|
||||
scroll_to_row = scroll_to_row - 1
|
||||
self.scrollToItem(self.item(scroll_to_row, 0))
|
||||
|
||||
def move_rows_down(self):
|
||||
self.setFocus()
|
||||
rows = self.selectionModel().selectedRows()
|
||||
if len(rows) == 0:
|
||||
return
|
||||
last_sel_row = rows[-1].row()
|
||||
if last_sel_row == self.rowCount() - 1:
|
||||
return
|
||||
# Workaround for strange selection bug in Qt which "alters" the selection
|
||||
# in certain circumstances which meant move down only worked properly "once"
|
||||
selrows = []
|
||||
for row in rows:
|
||||
selrows.append(row.row())
|
||||
selrows.sort()
|
||||
for selrow in reversed(selrows):
|
||||
self.swap_row_widgets(selrow + 2, selrow)
|
||||
scroll_to_row = last_sel_row + 1
|
||||
if scroll_to_row < self.rowCount() - 1:
|
||||
scroll_to_row = scroll_to_row + 1
|
||||
self.scrollToItem(self.item(scroll_to_row, 0))
|
||||
|
||||
def swap_row_widgets(self, src_row, dest_row):
|
||||
self.blockSignals(True)
|
||||
self.setSortingEnabled(False)
|
||||
self.insertRow(dest_row)
|
||||
for col in range(0, self.columnCount()):
|
||||
self.setItem(dest_row, col, self.takeItem(src_row, col))
|
||||
self.removeRow(src_row)
|
||||
self.blockSignals(False)
|
||||
|
||||
class RejectListDialog(SizePersistedDialog):
|
||||
def __init__(self, gui, reject_list,
|
||||
rejectreasons=[],
|
||||
header="List of Books to Reject",
|
||||
icon='rotate-right.png',
|
||||
show_delete=True,
|
||||
save_size_name='ffdl:reject list dialog'):
|
||||
SizePersistedDialog.__init__(self, gui, save_size_name)
|
||||
self.gui = gui
|
||||
|
||||
self.setWindowTitle(header)
|
||||
self.setWindowIcon(get_icon(icon))
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
self.setLayout(layout)
|
||||
title_layout = ImageTitleLayout(self, icon, header,
|
||||
'<i></i>FFDL will remember these URLs and display the note and offer to reject them if you try to download them again later.')
|
||||
layout.addLayout(title_layout)
|
||||
rejects_layout = QHBoxLayout()
|
||||
layout.addLayout(rejects_layout)
|
||||
|
||||
self.rejects_table = RejectListTableWidget(self,rejectreasons=rejectreasons)
|
||||
rejects_layout.addWidget(self.rejects_table)
|
||||
|
||||
button_layout = QVBoxLayout()
|
||||
rejects_layout.addLayout(button_layout)
|
||||
spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
button_layout.addItem(spacerItem)
|
||||
# self.move_up_button = QtGui.QToolButton(self)
|
||||
# self.move_up_button.setToolTip('Move selected books up the list')
|
||||
# self.move_up_button.setIcon(QIcon(I('arrow-up.png')))
|
||||
# self.move_up_button.clicked.connect(self.books_table.move_rows_up)
|
||||
# button_layout.addWidget(self.move_up_button)
|
||||
self.remove_button = QtGui.QToolButton(self)
|
||||
self.remove_button.setToolTip('Remove selected URL(s) from the list')
|
||||
self.remove_button.setIcon(get_icon('list_remove.png'))
|
||||
self.remove_button.clicked.connect(self.remove_from_list)
|
||||
button_layout.addWidget(self.remove_button)
|
||||
# self.move_down_button = QtGui.QToolButton(self)
|
||||
# self.move_down_button.setToolTip('Move selected books down the list')
|
||||
# self.move_down_button.setIcon(QIcon(I('arrow-down.png')))
|
||||
# self.move_down_button.clicked.connect(self.books_table.move_rows_down)
|
||||
# button_layout.addWidget(self.move_down_button)
|
||||
spacerItem1 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
button_layout.addItem(spacerItem1)
|
||||
|
||||
options_layout = QHBoxLayout()
|
||||
|
||||
if show_delete:
|
||||
self.deletebooks = QCheckBox('Delete Books (including books without FanFiction URLs)?',self)
|
||||
self.deletebooks.setToolTip("Delete the selected books after adding them to the Rejected URLs list.")
|
||||
self.deletebooks.setChecked(True)
|
||||
options_layout.addWidget(self.deletebooks)
|
||||
|
||||
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.rejects_table.populate_table(reject_list)
|
||||
|
||||
def remove_from_list(self):
|
||||
self.rejects_table.remove_selected_rows()
|
||||
|
||||
def get_reject_list(self):
|
||||
return self.rejects_table.get_reject_list()
|
||||
|
||||
def get_deletebooks(self):
|
||||
return self.deletebooks.isChecked()
|
||||
|
||||
class EditTextDialog(QDialog):
|
||||
|
||||
def __init__(self, parent, text,
|
||||
icon=None, title=None, label=None, tooltip=None):
|
||||
QDialog.__init__(self, parent)
|
||||
self.resize(600, 500)
|
||||
self.l = QVBoxLayout()
|
||||
self.setLayout(self.l)
|
||||
self.label = QLabel(label)
|
||||
if title:
|
||||
self.setWindowTitle(title)
|
||||
if icon:
|
||||
self.setWindowIcon(icon)
|
||||
self.l.addWidget(self.label)
|
||||
|
||||
self.textedit = QTextEdit(self)
|
||||
self.textedit.setLineWrapMode(QTextEdit.NoWrap)
|
||||
self.textedit.setText(text)
|
||||
self.l.addWidget(self.textedit)
|
||||
|
||||
if tooltip:
|
||||
self.label.setToolTip(tooltip)
|
||||
self.textedit.setToolTip(tooltip)
|
||||
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
button_box.accepted.connect(self.accept)
|
||||
button_box.rejected.connect(self.reject)
|
||||
self.l.addWidget(button_box)
|
||||
|
||||
def get_plain_text(self):
|
||||
return unicode(self.textedit.toPlainText())
|
||||
|
||||
+354
-157
@@ -7,8 +7,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2012, Jim Miller'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import time, os, copy, threading, re
|
||||
from ConfigParser import SafeConfigParser
|
||||
import time, os, copy, threading, re, platform
|
||||
from StringIO import StringIO
|
||||
from functools import partial
|
||||
from datetime import datetime
|
||||
@@ -28,6 +27,8 @@ from calibre.gui2 import error_dialog, warning_dialog, question_dialog, info_dia
|
||||
from calibre.gui2.dialogs.message_box import ViewLog
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
from calibre.utils.date import local_tz
|
||||
from calibre.library.comments import sanitize_comments_html
|
||||
from calibre.constants import config_dir as calibre_config_dir
|
||||
|
||||
# The class that all interface action plugins must inherit from
|
||||
from calibre.gui2.actions import InterfaceAction
|
||||
@@ -35,15 +36,15 @@ from calibre.gui2.actions import InterfaceAction
|
||||
from calibre_plugins.fanfictiondownloader_plugin.common_utils import (set_plugin_icon_resources, get_icon,
|
||||
create_menu_action_unique, get_library_uuid)
|
||||
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters, writers, exceptions
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.htmlcleanup import stripHTML
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters, exceptions
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.configurable import Configuration
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.epubutils import get_dcsource, get_dcsource_chaptercount, get_story_url_from_html
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.geturls import get_urls_from_page
|
||||
|
||||
from calibre_plugins.fanfictiondownloader_plugin.config import (prefs, permitted_values)
|
||||
from calibre_plugins.fanfictiondownloader_plugin.config import (prefs, permitted_values, rejecturllist)
|
||||
from calibre_plugins.fanfictiondownloader_plugin.dialogs import (
|
||||
AddNewDialog, UpdateExistingDialog, display_story_list, DisplayStoryListDialog,
|
||||
LoopProgressDialog, UserPassDialog, AboutDialog, CollectURLDialog,
|
||||
LoopProgressDialog, UserPassDialog, AboutDialog, CollectURLDialog, RejectListDialog,
|
||||
OVERWRITE, OVERWRITEALWAYS, UPDATE, UPDATEALWAYS, ADDNEW, SKIP, CALIBREONLY,
|
||||
NotGoingToDownload )
|
||||
|
||||
@@ -116,15 +117,28 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
# items to prevent GC removing it.
|
||||
self.menu_actions = []
|
||||
self.qaction.setMenu(self.menu)
|
||||
self.menu.aboutToShow.connect(self.about_to_show_menu)
|
||||
|
||||
self.menus_lock = threading.RLock()
|
||||
self.menu.aboutToShow.connect(self.about_to_show_menu)
|
||||
|
||||
def initialization_complete(self):
|
||||
# otherwise configured hot keys won't work until the menu's
|
||||
# been displayed once.
|
||||
self.rebuild_menus()
|
||||
|
||||
## Kludgey, yes, but with the real configuration inside the
|
||||
## library now, how else would a user be able to change this
|
||||
## setting if it's crashing calibre?
|
||||
def check_macmenuhack(self):
|
||||
try:
|
||||
return self.macmenuhack
|
||||
except:
|
||||
file_path = os.path.join(calibre_config_dir,
|
||||
*("plugins/fanfictiondownloader_macmenuhack.txt".split('/')))
|
||||
file_path = os.path.abspath(file_path)
|
||||
print("macmenuhack file_path:%s"%file_path)
|
||||
self.macmenuhack = os.access(file_path, os.F_OK)
|
||||
return self.macmenuhack
|
||||
|
||||
def about_to_show_menu(self):
|
||||
self.rebuild_menus()
|
||||
|
||||
@@ -134,10 +148,6 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
|
||||
def rebuild_menus(self):
|
||||
with self.menus_lock:
|
||||
# Show the config dialog
|
||||
# The config dialog can also be shown from within
|
||||
# Preferences->Plugins, which is why the do_user_config
|
||||
# method is defined on the base plugin class
|
||||
do_user_config = self.interface_action_base_plugin.do_user_config
|
||||
self.menu.clear()
|
||||
self.actions_unique_map = {}
|
||||
@@ -148,8 +158,6 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
triggered=self.add_dialog )
|
||||
|
||||
self.update_action = self.create_menu_item_ex(self.menu, '&Update Existing FanFiction Book(s)', image='plusplus.png',
|
||||
unique_name='Update Existing FanFiction Book(s)',
|
||||
shortcut_name='Update Existing FanFiction Book(s)',
|
||||
triggered=self.update_existing)
|
||||
|
||||
if 'Reading List' in self.gui.iactions and (prefs['addtolists'] or prefs['addtoreadlists']) :
|
||||
@@ -169,55 +177,44 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
|
||||
if addmenutxt:
|
||||
self.add_send_action = self.create_menu_item_ex(self.menu, addmenutxt, image='plusplus.png',
|
||||
unique_name=addmenutxt,
|
||||
shortcut_name=addmenutxt,
|
||||
triggered=partial(self.update_lists,add=True))
|
||||
|
||||
if rmmenutxt:
|
||||
self.add_remove_action = self.create_menu_item_ex(self.menu, rmmenutxt, image='minusminus.png',
|
||||
unique_name=rmmenutxt,
|
||||
shortcut_name=rmmenutxt,
|
||||
triggered=partial(self.update_lists,add=False))
|
||||
|
||||
# try:
|
||||
# self.add_send_action.setEnabled( len(self.gui.library_view.get_selected_ids()) > 0 )
|
||||
# except:
|
||||
# pass
|
||||
# try:
|
||||
# self.add_remove_action.setEnabled( len(self.gui.library_view.get_selected_ids()) > 0 )
|
||||
# except:
|
||||
# pass
|
||||
|
||||
self.menu.addSeparator()
|
||||
self.get_list_action = self.create_menu_item_ex(self.menu, 'Get URLs from Selected Books', image='bookmarks.png',
|
||||
unique_name='Get URLs from Selected Books',
|
||||
shortcut_name='Get URLs from Selected Books',
|
||||
triggered=self.get_list_urls)
|
||||
|
||||
self.get_list_url_action = self.create_menu_item_ex(self.menu, 'Get Story URLs from Web Page', image='view.png',
|
||||
unique_name='Get Story URLs from Web Page',
|
||||
shortcut_name='Get Story URLs from Web Page',
|
||||
triggered=self.get_urls_from_page)
|
||||
|
||||
self.menu.addSeparator()
|
||||
self.config_action = create_menu_action_unique(self, self.menu, '&Configure Plugin', shortcut=False,
|
||||
image= 'config.png',
|
||||
unique_name='Configure FanFictionDownLoader',
|
||||
shortcut_name='Configure FanFictionDownLoader',
|
||||
triggered=partial(do_user_config,parent=self.gui))
|
||||
self.reject_list_action = self.create_menu_item_ex(self.menu, 'Reject Selected Books', image='rotate-right.png',
|
||||
triggered=self.reject_list_urls)
|
||||
|
||||
# print("platform.system():%s"%platform.system())
|
||||
# print("platform.mac_ver()[0]:%s"%platform.mac_ver()[0])
|
||||
if not self.check_macmenuhack(): # not platform.mac_ver()[0]: # Some macs crash on these menu items for unknown reasons.
|
||||
self.menu.addSeparator()
|
||||
self.config_action = self.create_menu_item_ex(self.menu, '&Configure Plugin',
|
||||
image= 'config.png',
|
||||
unique_name='Configure FanFictionDownLoader',
|
||||
shortcut_name='Configure FanFictionDownLoader',
|
||||
triggered=partial(do_user_config,parent=self.gui))
|
||||
|
||||
self.about_action = create_menu_action_unique(self, self.menu, '&About Plugin', shortcut=False,
|
||||
image= 'images/icon.png',
|
||||
unique_name='About FanFictionDownLoader',
|
||||
shortcut_name='About FanFictionDownLoader',
|
||||
triggered=self.about)
|
||||
self.about_action = self.create_menu_item_ex(self.menu, 'About Plugin',
|
||||
image= 'images/icon.png',
|
||||
unique_name='About FanFictionDownLoader',
|
||||
shortcut_name='About FanFictionDownLoader',
|
||||
triggered=self.about)
|
||||
|
||||
# Before we finalize, make sure we delete any actions for menus that are no longer displayed
|
||||
for menu_id, unique_name in self.old_actions_unique_map.iteritems():
|
||||
if menu_id not in self.actions_unique_map:
|
||||
self.gui.keyboard.unregister_shortcut(unique_name)
|
||||
self.old_actions_unique_map = self.actions_unique_map
|
||||
self.gui.keyboard.finalize()
|
||||
self.gui.keyboard.finalize()
|
||||
|
||||
def about(self):
|
||||
# Get the about text from a file inside the plugin zip file
|
||||
@@ -236,31 +233,60 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
def create_menu_item_ex(self, parent_menu, menu_text, image=None, tooltip=None,
|
||||
shortcut=None, triggered=None, is_checked=None, shortcut_name=None,
|
||||
unique_name=None):
|
||||
#print("create_menu_item_ex before %s"%menu_text)
|
||||
ac = create_menu_action_unique(self, parent_menu, menu_text, image, tooltip,
|
||||
shortcut, triggered, is_checked, shortcut_name, unique_name)
|
||||
self.actions_unique_map[ac.calibre_shortcut_unique_name] = ac.calibre_shortcut_unique_name
|
||||
self.menu_actions.append(ac)
|
||||
#print("create_menu_item_ex after %s"%menu_text)
|
||||
return ac
|
||||
|
||||
def is_library_view(self):
|
||||
# 0 = library, 1 = main, 2 = card_a, 3 = card_b
|
||||
return self.gui.stack.currentIndex() == 0
|
||||
|
||||
def plugin_button(self):
|
||||
if len(self.gui.library_view.get_selected_ids()) > 0 and prefs['updatedefault']:
|
||||
if self.is_library_view() and \
|
||||
len(self.gui.library_view.get_selected_ids()) > 0 and \
|
||||
prefs['updatedefault']:
|
||||
self.update_existing()
|
||||
else:
|
||||
self.add_dialog()
|
||||
|
||||
def update_lists(self,add=True):
|
||||
if len(self.gui.library_view.get_selected_ids()) > 0 and \
|
||||
(prefs['addtolists'] or prefs['addtoreadlists']) :
|
||||
if prefs['addtolists'] or prefs['addtoreadlists']:
|
||||
if not self.is_library_view():
|
||||
self.gui.status_bar.show_message(_('Cannot Update Reading Lists from Device View'), 3000)
|
||||
return
|
||||
|
||||
if len(self.gui.library_view.get_selected_ids()) == 0:
|
||||
self.gui.status_bar.show_message(_('No Selected Books to Update Reading Lists'), 3000)
|
||||
return
|
||||
|
||||
self._update_reading_lists(self.gui.library_view.get_selected_ids(),add)
|
||||
|
||||
def get_urls_from_page(self):
|
||||
d = CollectURLDialog(self.gui,"Get Story URLs from Web Page")
|
||||
|
||||
if prefs['urlsfromclip']:
|
||||
try:
|
||||
urltxt = self.get_urls_clip(storyurls=False)[0]
|
||||
except:
|
||||
urltxt = ""
|
||||
|
||||
d = CollectURLDialog(self.gui,"Get Story URLs from Web Page",urltxt)
|
||||
d.exec_()
|
||||
if not d.status:
|
||||
return
|
||||
print("URL:%s"%d.url.text())
|
||||
url = u"%s"%d.url.text()
|
||||
print("get_urls_from_page URL:%s"%url)
|
||||
|
||||
url_list = get_urls_from_page("%s"%d.url.text())
|
||||
if 'archiveofourown.org' in url:
|
||||
configuration = Configuration(adapters.getConfigSectionFor(url),"EPUB")
|
||||
configuration.readfp(StringIO(get_resources("plugin-defaults.ini")))
|
||||
configuration.readfp(StringIO(prefs['personal.ini']))
|
||||
else:
|
||||
configuration = None
|
||||
url_list = get_urls_from_page(url,configuration)
|
||||
|
||||
if url_list:
|
||||
d = ViewLog(_("List of URLs"),"\n".join(url_list),parent=self.gui)
|
||||
@@ -274,19 +300,35 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
|
||||
|
||||
def get_list_urls(self):
|
||||
if len(self.gui.library_view.get_selected_ids()) > 0:
|
||||
book_list = map( partial(self._convert_id_to_book, good=False), self.gui.library_view.get_selected_ids() )
|
||||
if self.gui.current_view().selectionModel().selectedRows() == 0 :
|
||||
self.gui.status_bar.show_message(_('No Selected Books to Get URLs From'),
|
||||
3000)
|
||||
return
|
||||
|
||||
if self.is_library_view():
|
||||
book_list = map( partial(self._convert_id_to_book, good=False),
|
||||
self.gui.library_view.get_selected_ids() )
|
||||
|
||||
LoopProgressDialog(self.gui,
|
||||
book_list,
|
||||
partial(self._get_story_url_for_list, db=self.gui.current_db),
|
||||
self._finish_get_list_urls,
|
||||
init_label="Collecting URLs for stories...",
|
||||
win_title="Get URLs for stories",
|
||||
status_prefix="URL retrieved")
|
||||
else: # device view, get from epubs on device.
|
||||
view = self.gui.current_view()
|
||||
rows = view.selectionModel().selectedRows()
|
||||
# paths = view.model().paths(rows)
|
||||
book_list = map( partial(self._convert_row_to_book, good=False), rows )
|
||||
|
||||
LoopProgressDialog(self.gui,
|
||||
book_list,
|
||||
partial(self._get_story_url_for_list, db=self.gui.current_db),
|
||||
self._finish_get_list_urls,
|
||||
init_label="Collecting URLs for stories...",
|
||||
win_title="Get URLs for stories",
|
||||
status_prefix="URL retrieved")
|
||||
|
||||
def _get_story_url_for_list(self,book,db=None):
|
||||
book['url'] = self._get_story_url(db,book['calibre_id'])
|
||||
if book['calibre_id']:
|
||||
book['url'] = self._get_story_url(db,book_id=book['calibre_id'])
|
||||
elif book['path']:
|
||||
book['url'] = self._get_story_url(db,path=book['path'])
|
||||
|
||||
if book['url'] == None:
|
||||
book['good']=False
|
||||
else:
|
||||
@@ -304,6 +346,78 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
show=True,
|
||||
show_copy_button=False)
|
||||
|
||||
def reject_list_urls(self):
|
||||
if self.is_library_view():
|
||||
book_list = map( partial(self._convert_id_to_book, good=False),
|
||||
self.gui.library_view.get_selected_ids() )
|
||||
|
||||
else: # device view, get from epubs on device.
|
||||
view = self.gui.current_view()
|
||||
rows = view.selectionModel().selectedRows()
|
||||
#paths = view.model().paths(rows)
|
||||
book_list = map( partial(self._convert_row_to_book, good=False), rows )
|
||||
|
||||
if len(book_list) == 0 :
|
||||
self.gui.status_bar.show_message(_('No Selected Books have URLs to Reject'), 3000)
|
||||
return
|
||||
|
||||
LoopProgressDialog(self.gui,
|
||||
book_list,
|
||||
partial(self._reject_story_url_for_list, db=self.gui.current_db),
|
||||
self._finish_reject_list_urls,
|
||||
init_label="Collecting URLs for Reject List...",
|
||||
win_title="Get URLs for Reject List",
|
||||
status_prefix="URL retrieved")
|
||||
|
||||
def _reject_story_url_for_list(self,book,db=None):
|
||||
if book['calibre_id']:
|
||||
# want title/author, too, for rejects.
|
||||
self._populate_book_from_calibre_id(book,db)
|
||||
book['url'] = self._get_story_url(db,book_id=book['calibre_id'])
|
||||
elif book['path']:
|
||||
book['url'] = self._get_story_url(db,path=book['path'])
|
||||
|
||||
if book['url'] == None:
|
||||
book['good']=False
|
||||
else:
|
||||
book['good']=True
|
||||
# get existing note, if there is one.
|
||||
book['oldrejnote']=rejecturllist.check(book['url'])
|
||||
|
||||
def _finish_reject_list_urls(self, book_list):
|
||||
|
||||
# construct reject list of tuples:
|
||||
# (calibre_id, url, "title, authors", old reject note).
|
||||
reject_list = [ ( x['calibre_id'],x['url'],
|
||||
"%s by %s"%(x['title'],
|
||||
', '.join(x['author'])),
|
||||
x['oldrejnote'])
|
||||
for x in book_list if x['good'] ]
|
||||
if reject_list:
|
||||
d = RejectListDialog(self.gui,reject_list,
|
||||
rejectreasons=rejecturllist.get_reject_reasons())
|
||||
d.exec_()
|
||||
|
||||
if d.result() != d.Accepted:
|
||||
return
|
||||
|
||||
bookids=[]
|
||||
rejectlist=[]
|
||||
for (bookid,url,note) in d.get_reject_list():
|
||||
bookids.append(bookid)
|
||||
rejectlist.append((url,note))
|
||||
print("Adding (%s) to Reject List: %s"%(url,note))
|
||||
|
||||
rejecturllist.add(rejectlist)
|
||||
|
||||
if d.get_deletebooks():
|
||||
self.gui.iactions['Remove Books'].delete_books()
|
||||
|
||||
else:
|
||||
message="<p>Rejecting FFDL URLs: None of the books selected have FanFiction URLs.</p><p>Proceed to Remove?</p>"
|
||||
if confirm(message,'fanfictiondownloader_reject_non_fanfiction', self.gui):
|
||||
self.gui.iactions['Remove Books'].delete_books()
|
||||
|
||||
def add_dialog(self):
|
||||
|
||||
#print("add_dialog()")
|
||||
@@ -336,7 +450,12 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
self.start_downloads( options, add_books )
|
||||
|
||||
def update_existing(self):
|
||||
if not self.is_library_view():
|
||||
self.gui.status_bar.show_message(_('Cannot Update Books from Device View'), 3000)
|
||||
return
|
||||
|
||||
if len(self.gui.library_view.get_selected_ids()) == 0:
|
||||
self.gui.status_bar.show_message(_('No Selected Books to Update'), 3000)
|
||||
return
|
||||
#print("update_existing()")
|
||||
|
||||
@@ -377,13 +496,14 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
options['version'] = self.version
|
||||
print(self.version)
|
||||
self.start_downloads( options, update_books )
|
||||
|
||||
def get_urls_clip(self):
|
||||
|
||||
def get_urls_clip(self,storyurls=True):
|
||||
url_list = []
|
||||
if prefs['urlsfromclip']:
|
||||
for url in unicode(QApplication.instance().clipboard().text()).split():
|
||||
if( self._is_good_downloader_url(url) ):
|
||||
if not storyurls or self._is_good_downloader_url(url):
|
||||
url_list.append(url)
|
||||
|
||||
return url_list
|
||||
|
||||
def apply_settings(self):
|
||||
@@ -420,6 +540,29 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
necessary data. To be called from LoopProgressDialog
|
||||
'loop'. Also pops dialogs for is adult, user/pass.
|
||||
'''
|
||||
|
||||
url = book['url']
|
||||
print("url:%s"%url)
|
||||
|
||||
rejnote = rejecturllist.check(url)
|
||||
if rejnote:
|
||||
if question_dialog(self.gui, 'Reject URL?',
|
||||
'<p>Reject URL?</p>'+
|
||||
'<p>%s is on the Reject URL list:<br />"%s"</p>'%(url,rejnote)+
|
||||
"<p>Click 'No' to download anyway.</p>",
|
||||
show_copy_button=False):
|
||||
book['comment'] = "Story on Reject URLs list (%s)."%rejnote
|
||||
book['good']=False
|
||||
book['icon']='rotate-right.png'
|
||||
book['status'] = 'Rejected'
|
||||
return
|
||||
else:
|
||||
if question_dialog(self.gui, 'Remove Reject URL?',
|
||||
"<p>Remove URL from Reject List?</p>"+
|
||||
'<p>%s is on the Reject URL list:<br />"%s"</p>'%(url,rejnote)+
|
||||
"<p>Click 'Yes' to remove it from the list and download,<br /> 'No' to download, but leave it on the Reject list.</p>",
|
||||
show_copy_button=False):
|
||||
rejecturllist.remove(url)
|
||||
|
||||
# The current database shown in the GUI
|
||||
# db is an instance of the class LibraryDatabase2 from database.py
|
||||
@@ -436,17 +579,8 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
# book has already been flagged bad for whatever reason.
|
||||
return
|
||||
|
||||
url = book['url']
|
||||
print("url:%s"%url)
|
||||
skip_date_update = False
|
||||
|
||||
## was self.ffdlconfig, but we need to be able to change it
|
||||
## when doing epub update.
|
||||
ffdlconfig = SafeConfigParser()
|
||||
ffdlconfig.readfp(StringIO(get_resources("plugin-defaults.ini")))
|
||||
ffdlconfig.readfp(StringIO(prefs['personal.ini']))
|
||||
adapter = adapters.getAdapter(ffdlconfig,url,fileform)
|
||||
|
||||
options['personal.ini'] = prefs['personal.ini']
|
||||
if prefs['includeimages']:
|
||||
# this is a cheat to make it easier for users.
|
||||
@@ -456,6 +590,11 @@ keep_summary_html:true
|
||||
make_firstimage_cover:true
|
||||
''' + options['personal.ini']
|
||||
|
||||
configuration = Configuration(adapters.getConfigSectionFor(url),fileform)
|
||||
configuration.readfp(StringIO(get_resources("plugin-defaults.ini")))
|
||||
configuration.readfp(StringIO(options['personal.ini']))
|
||||
adapter = adapters.getAdapter(configuration,url)
|
||||
|
||||
## three tries, that's enough if both user/pass & is_adult needed,
|
||||
## or a couple tries of one or the other
|
||||
for x in range(0,2):
|
||||
@@ -477,20 +616,21 @@ make_firstimage_cover:true
|
||||
|
||||
# let other exceptions percolate up.
|
||||
story = adapter.getStoryMetadataOnly()
|
||||
writer = writers.getWriter(options['fileform'],adapter.config,adapter)
|
||||
|
||||
# set PI version instead of default.
|
||||
if 'version' in options:
|
||||
story.setMetadata('version',options['version'])
|
||||
|
||||
book['all_metadata'] = story.getAllMetadata(removeallentities=True)
|
||||
book['title'] = story.getMetadata("title", removeallentities=True)
|
||||
book['author_sort'] = book['author'] = story.getList("author", removeallentities=True)
|
||||
book['publisher'] = story.getMetadata("site")
|
||||
book['tags'] = writer.getTags() # getTags could be moved up into adapter now. Adapter didn't used to know the fileform
|
||||
book['comments'] = stripHTML(story.getMetadata("description")) #, removeallentities=True) comments handles entities better.
|
||||
book['series'] = story.getMetadata("series")
|
||||
|
||||
# adapter.opener is the element with a threadlock. But del
|
||||
# adapter.opener doesn't work--subproc fails when it tries
|
||||
# to pull in the adapter object that hasn't been imported yet.
|
||||
# book['adapter'] = adapter
|
||||
book['tags'] = story.getSubjectTags(removeallentities=True)
|
||||
if story.getMetadata("description"):
|
||||
book['comments'] = sanitize_comments_html(story.getMetadata("description"))
|
||||
else:
|
||||
book['comments']=''
|
||||
book['series'] = story.getMetadata("series", removeallentities=True)
|
||||
|
||||
book['is_adult'] = adapter.is_adult
|
||||
book['username'] = adapter.username
|
||||
@@ -525,8 +665,8 @@ make_firstimage_cover:true
|
||||
# 'new' book from URL. collision handling applies.
|
||||
print("from URL(%s)"%url)
|
||||
|
||||
# try to find by identifier url first.
|
||||
searchstr = 'identifiers:"=url:%s"'%url.replace(":","|")
|
||||
# try to find by identifier url or uri first.
|
||||
searchstr = 'identifiers:"~ur(i|l):=%s"'%url.replace(":","|")
|
||||
identicalbooks = db.search_getting_ids(searchstr, None)
|
||||
if len(identicalbooks) < 1:
|
||||
# find dups
|
||||
@@ -551,7 +691,7 @@ make_firstimage_cover:true
|
||||
raise NotGoingToDownload("Skipping duplicate story.","list_remove.png")
|
||||
|
||||
if len(identicalbooks) > 1:
|
||||
raise NotGoingToDownload("More than one identical book--can't tell which to update/overwrite.","minusminus.png")
|
||||
raise NotGoingToDownload("More than one identical book by Identifer URL or title/author(s)--can't tell which book to update/overwrite.","minusminus.png")
|
||||
|
||||
## changed: add new book when CALIBREONLY if none found.
|
||||
if collision == CALIBREONLY and not identicalbooks:
|
||||
@@ -622,9 +762,12 @@ make_firstimage_cover:true
|
||||
|
||||
if book['good']: # there shouldn't be any !'good' books at this point.
|
||||
# if still 'good', make a temp file to write the output to.
|
||||
tmp = PersistentTemporaryFile(prefix='new-%s-'%book['calibre_id'],
|
||||
suffix='.'+options['fileform'],
|
||||
dir=options['tdir'])
|
||||
# For HTML format users, make the filename inside the zip something reasonable.
|
||||
# For crazy long titles/authors, limit it to 200chars.
|
||||
# For weird/OS-unsafe characters, use file safe only.
|
||||
tmp = PersistentTemporaryFile(prefix=story.formatFileName("${title}-${author}-",allowunsafefilename=False)[:100],
|
||||
suffix='.'+options['fileform'],
|
||||
dir=options['tdir'])
|
||||
print("title:"+book['title'])
|
||||
print("outfile:"+tmp.name)
|
||||
book['outfile'] = tmp.name
|
||||
@@ -708,7 +851,7 @@ make_firstimage_cover:true
|
||||
self._add_or_update_book(book,options,prefs,mi)
|
||||
|
||||
if options['collision'] == CALIBREONLY or \
|
||||
(options['updatemeta'] and book['good']):
|
||||
( (options['updatemeta'] or book['added']) and book['good'] ):
|
||||
self._update_metadata(db, book['calibre_id'], book, mi, options)
|
||||
|
||||
def _update_bad_book(self,book,db=None,label='errorcol',
|
||||
@@ -770,22 +913,6 @@ make_firstimage_cover:true
|
||||
|
||||
self.previous = self.gui.library_view.currentIndex()
|
||||
db = self.gui.current_db
|
||||
# if display_story_list(self.gui,
|
||||
# 'Downloads finished, confirm to update Calibre',
|
||||
# prefs,
|
||||
# self.qaction.icon(),
|
||||
# job.result,
|
||||
# label_text='Stories will not be added or updated in Calibre without confirmation.',
|
||||
# offer_skip=True):
|
||||
|
||||
# payload = (job.statistics_cols_map, book_statistics_map)
|
||||
# all_ids = set(book_statistics_map.keys())
|
||||
# msg = '<p>Count Pages plugin found <b>%d statistics(s)</b>. ' % len(all_ids) + \
|
||||
# 'Proceed with updating columns in your library?'
|
||||
# self.gui.proceed_question(self._update_database_columns,
|
||||
# payload, job.details,
|
||||
# 'Count log', 'Count complete', msg,
|
||||
# show_copy_button=False)
|
||||
|
||||
book_list = job.result
|
||||
good_list = filter(lambda x : x['good'], book_list)
|
||||
@@ -798,27 +925,27 @@ make_firstimage_cover:true
|
||||
<p>Proceed with updating your library?</p>
|
||||
'''%(len(good_list),len(bad_list))
|
||||
|
||||
htmllog='<html><body><table border="1"><tr><th>Status</th><th>Title</th><th>Author</th><th>URL</th><th>Comment</th></tr>'
|
||||
htmllog='<html><body><table border="1"><tr><th>Status</th><th>Title</th><th>Author</th><th>Comment</th><th>URL</th></tr>'
|
||||
for book in good_list:
|
||||
if 'status' in book:
|
||||
status = book['status']
|
||||
else:
|
||||
status = 'Good'
|
||||
htmllog = htmllog + '<tr><td>' + '</td><td>'.join([status,book['title'],", ".join(book['author']),book['url'],book['comment']]) + '</td></tr>'
|
||||
htmllog = htmllog + '<tr><td>' + '</td><td>'.join([status,book['title'],", ".join(book['author']),book['comment'],book['url']]) + '</td></tr>'
|
||||
|
||||
for book in bad_list:
|
||||
if 'status' in book:
|
||||
status = book['status']
|
||||
else:
|
||||
status = 'Bad'
|
||||
htmllog = htmllog + '<tr><td>' + '</td><td>'.join([status,book['title'],", ".join(book['author']),book['url'],book['comment']]) + '</td></tr>'
|
||||
htmllog = htmllog + '<tr><td>' + '</td><td>'.join([status,book['title'],", ".join(book['author']),book['comment'],book['url']]) + '</td></tr>'
|
||||
|
||||
htmllog = htmllog + '</table></body></html>'
|
||||
|
||||
self.gui.proceed_question(self._do_download_list_update,
|
||||
payload, htmllog,
|
||||
'FFDL log', 'FFDL download complete', msg,
|
||||
show_copy_button=True)
|
||||
show_copy_button=False)
|
||||
|
||||
|
||||
def _do_download_list_update(self, payload):
|
||||
@@ -890,6 +1017,7 @@ make_firstimage_cover:true
|
||||
return book_id
|
||||
|
||||
def _update_metadata(self, db, book_id, book, mi, options):
|
||||
oldmi = db.get_metadata(book_id,index_is_id=True)
|
||||
if prefs['keeptags']:
|
||||
old_tags = db.get_tags(book_id)
|
||||
# remove old Completed/In-Progress only if there's a new one.
|
||||
@@ -901,14 +1029,13 @@ make_firstimage_cover:true
|
||||
# mi.tags needs to be list, but set kills dups.
|
||||
mi.tags = list(set(list(old_tags)+mi.tags))
|
||||
|
||||
if 'langcode' in book['all_metadata']:
|
||||
if book['all_metadata']['langcode']:
|
||||
mi.languages=[book['all_metadata']['langcode']]
|
||||
else:
|
||||
# Set language english, but only if not already set.
|
||||
oldmi = db.get_metadata(book_id,index_is_id=True)
|
||||
if not oldmi.languages:
|
||||
mi.languages=['eng']
|
||||
|
||||
mi.languages=['en']
|
||||
|
||||
if options['fileform'] == 'epub' and prefs['updatecover']:
|
||||
existingepub = db.format(book_id,'EPUB',index_is_id=True, as_file=True)
|
||||
epubmi = get_metadata(existingepub,'EPUB')
|
||||
@@ -922,7 +1049,21 @@ make_firstimage_cover:true
|
||||
autid=db.get_author_id(auth)
|
||||
db.set_link_field_for_author(autid, unicode(authurls[i]),
|
||||
commit=False, notify=False)
|
||||
|
||||
|
||||
# implement 'newonly' flags here by setting to the current
|
||||
# value again.
|
||||
if not book['added']:
|
||||
for (col,newonly) in prefs['std_cols_newonly'].iteritems():
|
||||
if newonly:
|
||||
if col == "identifiers":
|
||||
mi.set_identifiers(oldmi.get_identifiers())
|
||||
else:
|
||||
try:
|
||||
mi.__setattr__(col,oldmi.__getattribute__(col))
|
||||
except AttributeError:
|
||||
print("AttributeError? %s"%col)
|
||||
pass
|
||||
|
||||
db.set_metadata(book_id,mi)
|
||||
|
||||
# do configured column updates here.
|
||||
@@ -936,6 +1077,9 @@ make_firstimage_cover:true
|
||||
print("%s not an existing column, skipping."%col)
|
||||
continue
|
||||
coldef = custom_columns[col]
|
||||
if col in prefs['custom_cols_newonly'] and prefs['custom_cols_newonly'][col] and not book['added']:
|
||||
print("Skipping custom column(%s) update, set to New Books Only"%coldef['name'])
|
||||
continue
|
||||
if not meta.startswith('status-') and meta not in book['all_metadata'] or \
|
||||
meta.startswith('status-') and 'status' not in book['all_metadata']:
|
||||
print("No value for %s, skipping custom column(%s) update."%(meta,coldef['name']))
|
||||
@@ -956,6 +1100,55 @@ make_firstimage_cover:true
|
||||
val = book['all_metadata']['status'] == 'In-Progress'
|
||||
db.set_custom(book_id, val, label=label, commit=False)
|
||||
|
||||
adapter = None
|
||||
if prefs['allow_custcol_from_ini']:
|
||||
configuration = Configuration(adapters.getConfigSectionFor(book['url']),options['fileform'])
|
||||
configuration.readfp(StringIO(get_resources("plugin-defaults.ini")))
|
||||
configuration.readfp(StringIO(options['personal.ini']))
|
||||
adapter = adapters.getAdapter(configuration,book['url'])
|
||||
|
||||
# meta => custcol[,a|n|r]
|
||||
# cliches=>\#acolumn,r
|
||||
for line in adapter.getConfig('custom_columns_settings').splitlines():
|
||||
if "=>" in line:
|
||||
(meta,custcol) = map( lambda x: x.strip(), line.split("=>") )
|
||||
flag='r'
|
||||
if "," in custcol:
|
||||
(custcol,flag) = map( lambda x: x.strip(), custcol.split(",") )
|
||||
|
||||
print("meta:(%s) => custcol:(%s), flag(%s) "%(meta,custcol,flag))
|
||||
|
||||
if meta not in book['all_metadata']:
|
||||
print("No value for %s, skipping custom column(%s) update."%(meta,custcol))
|
||||
continue
|
||||
|
||||
if custcol not in custom_columns:
|
||||
print("No custom column(%s), skipping."%(custcol))
|
||||
continue
|
||||
else:
|
||||
coldef = custom_columns[custcol]
|
||||
label = coldef['label']
|
||||
|
||||
if flag == 'r' or book['added']: # flag 'n' isn't actually needed--*always* set if configured and new book.
|
||||
db.set_custom(book_id, book['all_metadata'][meta], label=label, commit=False)
|
||||
|
||||
if flag == 'a':
|
||||
vallist = []
|
||||
try:
|
||||
existing=db.get_custom(book_id,label=label,index_is_id=True)
|
||||
if isinstance(existing,list):
|
||||
vallist = existing
|
||||
elif existing:
|
||||
vallist = [existing]
|
||||
except:
|
||||
pass
|
||||
|
||||
if book['all_metadata'][meta]:
|
||||
vallist = [book['all_metadata'][meta]]
|
||||
|
||||
db.set_custom(book_id, ", ".join(vallist), label=label, commit=False)
|
||||
|
||||
|
||||
db.commit()
|
||||
|
||||
if 'Generate Cover' in self.gui.iactions and (book['added'] or not prefs['gcnewonly']):
|
||||
@@ -967,10 +1160,11 @@ make_firstimage_cover:true
|
||||
gc_plugin = self.gui.iactions['Generate Cover']
|
||||
setting_name = None
|
||||
if prefs['allow_gc_from_ini']:
|
||||
ffdlconfig = SafeConfigParser()
|
||||
ffdlconfig.readfp(StringIO(get_resources("plugin-defaults.ini")))
|
||||
ffdlconfig.readfp(StringIO(prefs['personal.ini']))
|
||||
adapter = adapters.getAdapter(ffdlconfig,book['url'],options['fileform'])
|
||||
if not adapter: # might already have it from allow_custcol_from_ini
|
||||
configuration = Configuration(adapters.getConfigSectionFor(book['url']),options['fileform'])
|
||||
configuration.readfp(StringIO(get_resources("plugin-defaults.ini")))
|
||||
configuration.readfp(StringIO(options['personal.ini']))
|
||||
adapter = adapters.getAdapter(configuration,book['url'])
|
||||
|
||||
# template => regexp to match => GC Setting to use.
|
||||
# generate_cover_settings:
|
||||
@@ -979,7 +1173,7 @@ make_firstimage_cover:true
|
||||
if "=>" in line:
|
||||
(template,regexp,setting) = map( lambda x: x.strip(), line.split("=>") )
|
||||
value = Template(template).safe_substitute(book['all_metadata']).encode('utf8')
|
||||
print("%s(%s) => %s => %s"%(template,value,regexp,setting))
|
||||
# print("%s(%s) => %s => %s"%(template,value,regexp,setting))
|
||||
if re.search(regexp,value):
|
||||
setting_name = setting
|
||||
break
|
||||
@@ -1049,12 +1243,6 @@ make_firstimage_cover:true
|
||||
message="<p>You configured FanFictionDownLoader to automatically update \"Send to Device\" Reading Lists, but you don't have any lists set?</p>"
|
||||
confirm(message,'fanfictiondownloader_no_send_lists', self.gui)
|
||||
|
||||
# Quick demo of how an 'allow send' list might work.
|
||||
# Issues: allow list per send list? Naming convention? "send(allow)"
|
||||
# allow_list = rl_plugin.get_book_list("Allow Send to Device")
|
||||
# # intersection of book_ids & allow_list
|
||||
# add_book_ids = list(set(book_ids) & set(allow_list))
|
||||
|
||||
for l in lists:
|
||||
if l in rl_plugin.get_list_names():
|
||||
#print("good send l:(%s)"%l)
|
||||
@@ -1067,17 +1255,6 @@ make_firstimage_cover:true
|
||||
message="<p>You configured FanFictionDownLoader to automatically update Reading List '%s', but you don't have a list of that name?</p>"%l
|
||||
confirm(message,'fanfictiondownloader_no_reading_list_%s'%l, self.gui)
|
||||
|
||||
# def _find_existing_book_id(self,db,book,matchurl=True):
|
||||
# mi = MetaInformation(book["title"],book["author"]) # author is a list.
|
||||
# identicalbooks = db.find_identical_books(mi)
|
||||
# if matchurl: # only *really* identical if URL matches, too.
|
||||
# for ib in identicalbooks:
|
||||
# if self._get_story_url(db,ib) == book['url']:
|
||||
# return ib
|
||||
# if identicalbooks:
|
||||
# return identicalbooks.pop()
|
||||
# return None
|
||||
|
||||
def _make_mi_from_book(self,book):
|
||||
mi = MetaInformation(book['title'],book['author']) # author is a list.
|
||||
mi.set_identifiers({'url':book['url']})
|
||||
@@ -1139,7 +1316,25 @@ make_firstimage_cover:true
|
||||
book['comment'] = ''
|
||||
book['url'] = ''
|
||||
book['added'] = False
|
||||
|
||||
|
||||
return book
|
||||
|
||||
def _convert_row_to_book(self, row, good=True):
|
||||
book = {}
|
||||
mi = self.gui.current_view().model().get_book_display_info(row.row())
|
||||
book['title'] = mi.title
|
||||
book['author'] = mi.authors
|
||||
book['path'] = mi.path
|
||||
book['author_sort'] = mi.author_sort
|
||||
book['good'] = good
|
||||
book['calibre_id'] = None
|
||||
book['begin'] = None
|
||||
book['end'] = None
|
||||
|
||||
book['comment'] = ''
|
||||
book['url'] = ''
|
||||
book['added'] = False
|
||||
|
||||
return book
|
||||
|
||||
def _populate_book_from_calibre_id(self, book, db=None):
|
||||
@@ -1174,44 +1369,46 @@ make_firstimage_cover:true
|
||||
book['icon']='dialog_error.png'
|
||||
book['status'] = 'Bad URL'
|
||||
|
||||
def _get_story_url(self, db, book_id):
|
||||
identifiers = db.get_identifiers(book_id,index_is_id=True)
|
||||
def _get_story_url(self, db, book_id=None, path=None):
|
||||
if book_id == None:
|
||||
identifiers={}
|
||||
else:
|
||||
identifiers = db.get_identifiers(book_id,index_is_id=True)
|
||||
if 'url' in identifiers:
|
||||
# identifiers have :->| in url.
|
||||
#print("url from book:"+identifiers['url'].replace('|',':'))
|
||||
# print("url from ident url:%s"%identifiers['url'].replace('|',':'))
|
||||
return identifiers['url'].replace('|',':')
|
||||
elif 'uri' in identifiers:
|
||||
# identifiers have :->| in uri.
|
||||
# print("uri from ident uri:%s"%identifiers['uri'].replace('|',':'))
|
||||
return identifiers['uri'].replace('|',':')
|
||||
else:
|
||||
## only epub has URL in it--at least where I can easily find it.
|
||||
if db.has_format(book_id,'EPUB',index_is_id=True):
|
||||
existingepub = None
|
||||
if path == None and db.has_format(book_id,'EPUB',index_is_id=True):
|
||||
existingepub = db.format(book_id,'EPUB',index_is_id=True, as_file=True)
|
||||
mi = get_metadata(existingepub,'EPUB')
|
||||
identifiers = mi.get_identifiers()
|
||||
if 'url' in identifiers:
|
||||
#print("url from epub:"+identifiers['url'].replace('|',':'))
|
||||
# print("url from get_metadata:%s"%identifiers['url'].replace('|',':'))
|
||||
return identifiers['url'].replace('|',':')
|
||||
# look for dc:source first, then scan HTML if
|
||||
elif path.lower().endswith('.epub'):
|
||||
existingepub = path
|
||||
|
||||
## only epub has URL in it--at least where I can easily find it.
|
||||
if existingepub:
|
||||
# look for dc:source first, then scan HTML if lookforurlinhtml
|
||||
link = get_dcsource(existingepub)
|
||||
if link:
|
||||
# print("url from get_dcsource:%s"%link)
|
||||
return link
|
||||
elif prefs['lookforurlinhtml']:
|
||||
return get_story_url_from_html(existingepub,self._is_good_downloader_url)
|
||||
link = get_story_url_from_html(existingepub,self._is_good_downloader_url)
|
||||
# print("url from get_story_url_from_html:%s"%link)
|
||||
return link
|
||||
return None
|
||||
|
||||
def _is_good_downloader_url(self,url):
|
||||
# this is the accepted way to 'check for existance'? really?
|
||||
try:
|
||||
self.dummyconfig
|
||||
except AttributeError:
|
||||
self.dummyconfig = SafeConfigParser()
|
||||
# pulling up an adapter is pretty low over-head. If
|
||||
# it fails, it's a bad url.
|
||||
try:
|
||||
adapter = adapters.getAdapter(self.dummyconfig,url)
|
||||
url = adapter.url
|
||||
del adapter
|
||||
return url
|
||||
except:
|
||||
return None;
|
||||
return adapters.getNormalStoryURL(url)
|
||||
|
||||
def get_url_list(urls):
|
||||
def f(x):
|
||||
|
||||
+22
-15
@@ -10,16 +10,15 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
import time, os, traceback
|
||||
|
||||
from ConfigParser import SafeConfigParser
|
||||
from StringIO import StringIO
|
||||
|
||||
from calibre.utils.ipc.server import Server
|
||||
from calibre.utils.ipc.job import ParallelJob
|
||||
from calibre.utils.logging import Log
|
||||
|
||||
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
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
@@ -111,25 +110,33 @@ def do_download_for_worker(book,options):
|
||||
try:
|
||||
book['comment'] = 'Download started...'
|
||||
|
||||
ffdlconfig = SafeConfigParser()
|
||||
ffdlconfig.readfp(StringIO(get_resources("plugin-defaults.ini")))
|
||||
ffdlconfig.readfp(StringIO(options['personal.ini']))
|
||||
configuration = Configuration(adapters.getConfigSectionFor(book['url']),options['fileform'])
|
||||
configuration.readfp(StringIO(get_resources("plugin-defaults.ini")))
|
||||
configuration.readfp(StringIO(options['personal.ini']))
|
||||
|
||||
if not options['updateepubcover'] and 'epub_for_update' in book and options['collision'] in (UPDATE, UPDATEALWAYS):
|
||||
ffdlconfig.set("overrides","never_make_cover","true")
|
||||
configuration.set("overrides","never_make_cover","true")
|
||||
|
||||
# images only for epub, even if the user mistakenly turned it
|
||||
# on else where.
|
||||
if options['fileform'] not in ("epub","html"):
|
||||
configuration.set("overrides","include_images","false")
|
||||
|
||||
adapter = adapters.getAdapter(ffdlconfig,book['url'],options['fileform'])
|
||||
adapter = adapters.getAdapter(configuration,book['url'])
|
||||
adapter.is_adult = book['is_adult']
|
||||
adapter.username = book['username']
|
||||
adapter.password = book['password']
|
||||
adapter.setChaptersRange(book['begin'],book['end'])
|
||||
|
||||
story = adapter.getStoryMetadataOnly()
|
||||
if 'calibre_series' in book:
|
||||
# print("calibre_series:%s [%d]"%book['calibre_series'])
|
||||
adapter.setSeries(book['calibre_series'][0],book['calibre_series'][1])
|
||||
# else:
|
||||
# print("no calibre_series")
|
||||
writer = writers.getWriter(options['fileform'],adapter.config,adapter)
|
||||
|
||||
# set PI version instead of default.
|
||||
if 'version' in options:
|
||||
story.setMetadata('version',options['version'])
|
||||
|
||||
writer = writers.getWriter(options['fileform'],configuration,adapter)
|
||||
|
||||
outfile = book['outfile']
|
||||
|
||||
@@ -142,8 +149,6 @@ 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)):
|
||||
|
||||
adapter.setChaptersRange(book['begin'],book['end'])
|
||||
|
||||
print("write to %s"%outfile)
|
||||
writer.writeStory(outfilename=outfile, forceOverwrite=True)
|
||||
book['comment'] = 'Download %s completed, %s chapters.'%(options['fileform'],story.getMetadata("numChapters"))
|
||||
@@ -154,11 +159,13 @@ def do_download_for_worker(book,options):
|
||||
# update now handled by pre-populating the old images and
|
||||
# chapters in the adapter rather than merging epubs.
|
||||
urlchaptercount = int(story.getMetadata('numChapters'))
|
||||
(url,chaptercount,
|
||||
(url,
|
||||
chaptercount,
|
||||
adapter.oldchapters,
|
||||
adapter.oldimgs,
|
||||
adapter.oldcover,
|
||||
adapter.calibrebookmark) = get_update_data(book['epub_for_update'])
|
||||
adapter.calibrebookmark,
|
||||
adapter.logfile) = get_update_data(book['epub_for_update'])
|
||||
|
||||
print("Do update - epub(%d) vs url(%d)" % (chaptercount, urlchaptercount))
|
||||
print("write to %s"%outfile)
|
||||
|
||||
+1255
-780
File diff suppressed because it is too large
Load Diff
+44
-29
@@ -15,8 +15,6 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import logging
|
||||
|
||||
import sys, os
|
||||
from os.path import normpath, expanduser, isfile, join
|
||||
from StringIO import StringIO
|
||||
@@ -26,7 +24,16 @@ import string
|
||||
import ConfigParser
|
||||
from subprocess import call
|
||||
|
||||
import logging
|
||||
if sys.version_info >= (2, 7):
|
||||
# suppresses default logger. Logging is setup in fanficdownload/__init__.py so it works in calibre, too.
|
||||
rootlogger = logging.getLogger()
|
||||
loghandler=logging.NullHandler()
|
||||
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
|
||||
|
||||
@@ -78,11 +85,9 @@ def main():
|
||||
|
||||
(options, args) = parser.parse_args()
|
||||
|
||||
if options.debug:
|
||||
logging.basicConfig(level=logging.DEBUG,format="%(levelname)s:%(filename)s(%(lineno)d):%(message)s")
|
||||
else:
|
||||
logging.basicConfig(level=logging.INFO,format="%(levelname)s:%(filename)s(%(lineno)d):%(message)s")
|
||||
|
||||
if not options.debug:
|
||||
logger = logging.getLogger("fanficdownloader")
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
if len(args) != 1:
|
||||
parser.error("incorrect number of arguments")
|
||||
@@ -90,7 +95,15 @@ def main():
|
||||
if options.update and options.format != 'epub':
|
||||
parser.error("-u/--update-epub only works with epub")
|
||||
|
||||
config = ConfigParser.SafeConfigParser()
|
||||
## Attempt to update an existing epub.
|
||||
if options.update:
|
||||
(url,chaptercount) = get_dcsource_chaptercount(args[0])
|
||||
print "Updating %s, URL: %s" % (args[0],url)
|
||||
output_filename = args[0]
|
||||
else:
|
||||
url = args[0]
|
||||
|
||||
configuration = Configuration(adapters.getConfigSectionFor(url),options.format)
|
||||
|
||||
conflist = []
|
||||
homepath = join(expanduser("~"),".fanficdownloader")
|
||||
@@ -109,42 +122,44 @@ def main():
|
||||
conflist.extend(options.configfile)
|
||||
|
||||
logging.debug('reading %s config file(s), if present'%conflist)
|
||||
config.read(conflist)
|
||||
configuration.read(conflist)
|
||||
|
||||
try:
|
||||
config.add_section("overrides")
|
||||
configuration.add_section("overrides")
|
||||
except ConfigParser.DuplicateSectionError:
|
||||
pass
|
||||
|
||||
if options.force:
|
||||
config.set("overrides","always_overwrite","true")
|
||||
configuration.set("overrides","always_overwrite","true")
|
||||
|
||||
if options.update:
|
||||
configuration.set("overrides","output_filename",args[0])
|
||||
|
||||
if options.update and not options.updatecover:
|
||||
config.set("overrides","never_make_cover","true")
|
||||
configuration.set("overrides","never_make_cover","true")
|
||||
|
||||
# images only for epub, even if the user mistakenly turned it
|
||||
# on else where.
|
||||
if options.format not in ("epub","html"):
|
||||
configuration.set("overrides","include_images","false")
|
||||
|
||||
if options.options:
|
||||
for opt in options.options:
|
||||
(var,val) = opt.split('=')
|
||||
config.set("overrides",var,val)
|
||||
configuration.set("overrides",var,val)
|
||||
|
||||
if options.list:
|
||||
retlist = get_urls_from_page(args[0])
|
||||
retlist = get_urls_from_page(args[0], configuration)
|
||||
print "\n".join(retlist)
|
||||
|
||||
return
|
||||
|
||||
try:
|
||||
## Attempt to update an existing epub.
|
||||
if options.update:
|
||||
(url,chaptercount) = get_dcsource_chaptercount(args[0])
|
||||
print "Updating %s, URL: %s" % (args[0],url)
|
||||
output_filename = args[0]
|
||||
config.set("overrides","output_filename",args[0])
|
||||
else:
|
||||
url = args[0]
|
||||
|
||||
adapter = adapters.getAdapter(config,url,options.format)
|
||||
adapter = adapters.getAdapter(configuration,url)
|
||||
|
||||
adapter.setChaptersRange(options.begin,options.end)
|
||||
|
||||
## Check for include_images and absence of PIL, give warning.
|
||||
if adapter.getConfig('include_images'):
|
||||
try:
|
||||
@@ -193,22 +208,22 @@ def main():
|
||||
# update now handled by pre-populating the old
|
||||
# images and chapters in the adapter rather than
|
||||
# merging epubs.
|
||||
(url,chaptercount,
|
||||
(url,
|
||||
chaptercount,
|
||||
adapter.oldchapters,
|
||||
adapter.oldimgs,
|
||||
adapter.oldcover,
|
||||
adapter.calibrebookmark) = get_update_data(args[0])
|
||||
adapter.calibrebookmark,
|
||||
adapter.logfile) = get_update_data(args[0])
|
||||
|
||||
writeStory(config,adapter,"epub")
|
||||
writeStory(configuration,adapter,"epub")
|
||||
|
||||
else:
|
||||
# regular download
|
||||
if options.metaonly:
|
||||
print adapter.getStoryMetadataOnly()
|
||||
|
||||
adapter.setChaptersRange(options.begin,options.end)
|
||||
|
||||
output_filename=writeStory(config,adapter,options.format,options.metaonly)
|
||||
output_filename=writeStory(configuration,adapter,options.format,options.metaonly)
|
||||
|
||||
if not options.metaonly and adapter.getConfig("post_process_cmd"):
|
||||
metadata = adapter.story.metadata
|
||||
|
||||
+78
-15
@@ -1,40 +1,103 @@
|
||||
## This is an example of what your personal configuration might look
|
||||
## like.
|
||||
## like. Uncomment options by removing the '#' in front of them.
|
||||
|
||||
[defaults]
|
||||
## Some sites also require the user to confirm they are adult for
|
||||
## adult content. In commandline version, this should go in your
|
||||
## personal.ini, not defaults.ini.
|
||||
## adult content. Uncomment by removing '#' in front of is_adult. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## Don't like the numbers at the start of chapter titles on some
|
||||
## sites? You can use strip_chapter_numbers to strip them off. Just
|
||||
## want to make them all look the same? Strip them off, then add them
|
||||
## back on with add_chapter_numbers. Don't like the way it strips
|
||||
## numbers or adds them back? See chapter_title_strip_pattern and
|
||||
## chapter_title_add_pattern.
|
||||
#strip_chapter_numbers:true
|
||||
#add_chapter_numbers:true
|
||||
|
||||
[epub]
|
||||
## include images from img tags in the body and summary of stories.
|
||||
## Images will be converted to jpg for size if possible. Images work
|
||||
## in epub format only. To get mobi or other format with images,
|
||||
## download as epub and use Calibre to convert.
|
||||
#include_images:true
|
||||
|
||||
## If not set, the summary will have all html stripped for safety.
|
||||
## Both this and include_images must be true to get images in the
|
||||
## summary.
|
||||
#keep_summary_html:true
|
||||
|
||||
## If set, the first image found will be made the cover image. If
|
||||
## keep_summary_html is true, any images in summary will be before any
|
||||
## in chapters.
|
||||
#make_firstimage_cover:true
|
||||
|
||||
## Resize images down to width, height, preserving aspect ratio.
|
||||
## Nook size, with margin.
|
||||
#image_max_size: 580, 725
|
||||
|
||||
## Change image to grayscale, if graphics library allows, to save
|
||||
## space.
|
||||
#grayscale_images: false
|
||||
|
||||
|
||||
## Most common, I expect will be using this to save username/passwords
|
||||
## for different sites.
|
||||
## for different sites. Here are a few examples. See defaults.ini
|
||||
## for the full list.
|
||||
|
||||
[www.twilighted.net]
|
||||
#username:YourPenname
|
||||
#password:YourPassword
|
||||
## default is false
|
||||
#collect_series: true
|
||||
|
||||
[www.ficwad.com]
|
||||
#username:YourUsername
|
||||
#password:YourPassword
|
||||
|
||||
[www.twiwrite.net]
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
## default is false
|
||||
#collect_series: true
|
||||
|
||||
[www.adastrafanfic.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content.
|
||||
#is_adult:true
|
||||
|
||||
## The [defaults] section here will override the system [defaults],
|
||||
## but not format, site for site:format sections.
|
||||
[defaults]
|
||||
## Directories only useful in commandline or zip files.
|
||||
#output_filename: books/${title}-${siteabbrev}_${storyId}${formatext}
|
||||
#output_filename: books/${site}/${authorId}/${title}-${storyId}${formatext}
|
||||
[www.thewriterscoffeeshop.com]
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
#is_adult:true
|
||||
## default is false
|
||||
#collect_series: true
|
||||
|
||||
[www.fictionalley.org]
|
||||
#is_adult:true
|
||||
|
||||
[www.harrypotterfanfiction.com]
|
||||
#is_adult:true
|
||||
|
||||
[www.fimfiction.net]
|
||||
#is_adult:true
|
||||
#fail_on_password: false
|
||||
|
||||
[www.tthfanfic.org]
|
||||
#is_adult:true
|
||||
## tth is a little unusual--it doesn't require user/pass, but the site
|
||||
## keeps track of which chapters you've read and won't send another
|
||||
## update until it thinks you're up to date. This way, on download,
|
||||
## it thinks you're up to date.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
## For example, zip_output here will turn on zip for html and txt, but
|
||||
## not epub because the system [epub] section explicitly says
|
||||
## zip_output: false (epubs *are* specially formated zip files.)
|
||||
#zip_output: true
|
||||
#zip_filename: ${title}-${siteabbrev}_${storyId}${formatext}.zip
|
||||
|
||||
## This section will override anything in the system defaults or other
|
||||
## sections here.
|
||||
[overrides]
|
||||
## default varies by site. Set true here to force all sites to
|
||||
## collect series.
|
||||
#collect_series: true
|
||||
|
||||
@@ -1 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
try:
|
||||
# just a way to switch between web service and CLI/PI
|
||||
import google.appengine.api
|
||||
except:
|
||||
import sys
|
||||
if sys.version_info >= (2, 7):
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
loghandler=logging.StreamHandler()
|
||||
loghandler.setFormatter(logging.Formatter("FFDL:%(levelname)s:%(filename)s(%(lineno)d):%(message)s"))
|
||||
logger.addHandler(loghandler)
|
||||
loghandler.setLevel(logging.DEBUG)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@@ -1,154 +1,200 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2011 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 os, re, sys, glob, types
|
||||
from os.path import dirname, basename, normpath
|
||||
import logging
|
||||
import urlparse as up
|
||||
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
## must import each adapter here.
|
||||
|
||||
import adapter_test1
|
||||
import adapter_fanfictionnet
|
||||
import adapter_castlefansorg
|
||||
import adapter_fictionalleyorg
|
||||
import adapter_fictionpresscom
|
||||
import adapter_ficwadcom
|
||||
import adapter_fimfictionnet
|
||||
import adapter_harrypotterfanfictioncom
|
||||
import adapter_mediaminerorg
|
||||
import adapter_potionsandsnitchesnet
|
||||
import adapter_tenhawkpresentscom
|
||||
import adapter_adastrafanficcom
|
||||
import adapter_thewriterscoffeeshopcom
|
||||
import adapter_tthfanficorg
|
||||
import adapter_twilightednet
|
||||
import adapter_twiwritenet
|
||||
import adapter_whoficcom
|
||||
import adapter_siyecouk
|
||||
import adapter_archiveofourownorg
|
||||
import adapter_ficbooknet
|
||||
import adapter_portkeyorg
|
||||
import adapter_mugglenetcom
|
||||
import adapter_hpfandomnet
|
||||
import adapter_thequidditchpitchorg
|
||||
import adapter_nfacommunitycom
|
||||
import adapter_midnightwhispersca
|
||||
import adapter_ksarchivecom
|
||||
import adapter_archiveskyehawkecom
|
||||
import adapter_squidgeorgpeja
|
||||
import adapter_libraryofmoriacom
|
||||
import adapter_wraithbaitcom
|
||||
import adapter_checkmatedcom
|
||||
import adapter_chaossycophanthexcom
|
||||
import adapter_dramioneorg
|
||||
import adapter_erosnsapphosycophanthexcom
|
||||
import adapter_lumossycophanthexcom
|
||||
import adapter_occlumencysycophanthexcom
|
||||
import adapter_phoenixsongnet
|
||||
import adapter_walkingtheplankorg
|
||||
import adapter_ashwindersycophanthexcom
|
||||
import adapter_thehexfilesnet
|
||||
import adapter_dokugacom
|
||||
import adapter_iketernalnet
|
||||
import adapter_onedirectionfanfictioncom
|
||||
import adapter_prisonbreakficnet
|
||||
import adapter_storiesofardacom
|
||||
import adapter_yourfanfictioncom
|
||||
import adapter_samdeanarchivenu
|
||||
import adapter_destinysgatewaycom
|
||||
import adapter_ncisfictioncom
|
||||
import adapter_stargateatlantisorg
|
||||
import adapter_thealphagatecom
|
||||
import adapter_fanfiktionde
|
||||
import adapter_ponyfictionarchivenet
|
||||
import adapter_sg1heliopoliscom
|
||||
import adapter_ncisficcom
|
||||
import adapter_nationallibrarynet
|
||||
import adapter_themasquenet
|
||||
import adapter_pretendercentrecom
|
||||
import adapter_darksolaceorg
|
||||
import adapter_finestoriescom
|
||||
import adapter_hpfanficarchivecom
|
||||
import adapter_svufictioncom
|
||||
import adapter_twilightarchivescom
|
||||
import adapter_wizardtalesnet
|
||||
import adapter_nhamagicalworldsus
|
||||
import adapter_hlfictionnet
|
||||
import adapter_grangerenchantedcom
|
||||
|
||||
## This bit of complexity allows adapters to be added by just adding
|
||||
## importing. It eliminates the long if/else clauses we used to need
|
||||
## to pick out the adapter.
|
||||
|
||||
## List of registered site adapters.
|
||||
__class_list = []
|
||||
|
||||
def imports():
|
||||
for name, val in globals().items():
|
||||
if isinstance(val, types.ModuleType):
|
||||
yield val.__name__
|
||||
|
||||
for x in imports():
|
||||
if "fanficdownloader.adapters.adapter_" in x:
|
||||
#print x
|
||||
__class_list.append(sys.modules[x].getClass())
|
||||
|
||||
def getAdapter(config,url,fileform=None):
|
||||
## fix up leading protocol.
|
||||
fixedurl = re.sub(r"(?i)^[htp]+[:/]+","http://",url.strip())
|
||||
if not fixedurl.startswith("http"):
|
||||
fixedurl = "http://%s"%url
|
||||
## remove any trailing '#' locations.
|
||||
fixedurl = re.sub(r"#.*$","",fixedurl)
|
||||
|
||||
## remove any trailing '&' parameters--?sid=999 will be left.
|
||||
## that's all that any of the current adapters need or want.
|
||||
fixedurl = re.sub(r"&.*$","",fixedurl)
|
||||
|
||||
parsedUrl = up.urlparse(fixedurl)
|
||||
domain = parsedUrl.netloc.lower()
|
||||
if( domain != parsedUrl.netloc ):
|
||||
fixedurl = fixedurl.replace(parsedUrl.netloc,domain)
|
||||
|
||||
logging.debug("site:"+domain)
|
||||
cls = getClassFor(domain)
|
||||
if not cls and domain.startswith("www."):
|
||||
domain = domain.replace("www.","")
|
||||
logging.debug("trying site:without www: "+domain)
|
||||
cls = getClassFor(domain)
|
||||
fixedurl = fixedurl.replace("http://www.","http://")
|
||||
if not cls:
|
||||
logging.debug("trying site:www."+domain)
|
||||
cls = getClassFor("www."+domain)
|
||||
fixedurl = fixedurl.replace("http://","http://www.")
|
||||
if cls:
|
||||
adapter = cls(config,fixedurl) # raises InvalidStoryURL
|
||||
adapter.setSectionOrder(adapter.getConfigSection(),fileform)
|
||||
return adapter
|
||||
# No adapter found.
|
||||
raise exceptions.UnknownSite( url, [cls.getSiteDomain() for cls in __class_list] )
|
||||
|
||||
def getConfigSections():
|
||||
return [cls.getConfigSection() for cls in __class_list]
|
||||
|
||||
def getClassFor(domain):
|
||||
for cls in __class_list:
|
||||
if cls.matchesSite(domain):
|
||||
return cls
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2011 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 os, re, sys, glob, types
|
||||
from os.path import dirname, basename, normpath
|
||||
import logging
|
||||
import urlparse as up
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from .. import exceptions as exceptions
|
||||
from ..configurable import Configuration
|
||||
|
||||
## must import each adapter here.
|
||||
|
||||
import adapter_test1
|
||||
import adapter_fanfictionnet
|
||||
import adapter_castlefansorg
|
||||
import adapter_fictionalleyorg
|
||||
import adapter_fictionpresscom
|
||||
import adapter_ficwadcom
|
||||
import adapter_fimfictionnet
|
||||
import adapter_harrypotterfanfictioncom
|
||||
import adapter_mediaminerorg
|
||||
import adapter_potionsandsnitchesnet
|
||||
import adapter_tenhawkpresentscom
|
||||
import adapter_adastrafanficcom
|
||||
import adapter_thewriterscoffeeshopcom
|
||||
import adapter_tthfanficorg
|
||||
import adapter_twilightednet
|
||||
import adapter_twiwritenet
|
||||
import adapter_whoficcom
|
||||
import adapter_siyecouk
|
||||
import adapter_archiveofourownorg
|
||||
import adapter_ficbooknet
|
||||
import adapter_portkeyorg
|
||||
import adapter_mugglenetcom
|
||||
import adapter_hpfandomnet
|
||||
import adapter_thequidditchpitchorg
|
||||
import adapter_nfacommunitycom
|
||||
import adapter_midnightwhispersca
|
||||
import adapter_ksarchivecom
|
||||
import adapter_archiveskyehawkecom
|
||||
import adapter_squidgeorgpeja
|
||||
import adapter_libraryofmoriacom
|
||||
import adapter_wraithbaitcom
|
||||
import adapter_checkmatedcom
|
||||
import adapter_chaossycophanthexcom
|
||||
import adapter_dramioneorg
|
||||
import adapter_erosnsapphosycophanthexcom
|
||||
import adapter_lumossycophanthexcom
|
||||
import adapter_occlumencysycophanthexcom
|
||||
import adapter_phoenixsongnet
|
||||
import adapter_walkingtheplankorg
|
||||
import adapter_ashwindersycophanthexcom
|
||||
import adapter_thehexfilesnet
|
||||
import adapter_dokugacom
|
||||
import adapter_iketernalnet
|
||||
import adapter_onedirectionfanfictioncom
|
||||
import adapter_prisonbreakficnet
|
||||
import adapter_storiesofardacom
|
||||
import adapter_yourfanfictioncom
|
||||
import adapter_samdeanarchivenu
|
||||
import adapter_destinysgatewaycom
|
||||
import adapter_ncisfictionnet
|
||||
import adapter_stargateatlantisorg
|
||||
import adapter_thealphagatecom
|
||||
import adapter_fanfiktionde
|
||||
import adapter_ponyfictionarchivenet
|
||||
import adapter_sg1heliopoliscom
|
||||
import adapter_ncisficcom
|
||||
import adapter_nationallibrarynet
|
||||
import adapter_themasquenet
|
||||
import adapter_pretendercentrecom
|
||||
import adapter_darksolaceorg
|
||||
import adapter_finestoriescom
|
||||
import adapter_hpfanficarchivecom
|
||||
import adapter_svufictioncom
|
||||
import adapter_twilightarchivescom
|
||||
import adapter_wizardtalesnet
|
||||
import adapter_nhamagicalworldsus
|
||||
import adapter_hlfictionnet
|
||||
import adapter_grangerenchantedcom
|
||||
import adapter_dracoandginnycom
|
||||
import adapter_scarvesandcoffeenet
|
||||
import adapter_thepetulantpoetesscom
|
||||
import adapter_wolverineandroguecom
|
||||
import adapter_sinfuldesireorg
|
||||
import adapter_merlinficdtwinscouk
|
||||
import adapter_thehookupzonenet
|
||||
import adapter_bloodtiesfancom
|
||||
import adapter_indeathnet
|
||||
import adapter_jlaunlimitedcom
|
||||
import adapter_qafficcom
|
||||
import adapter_efpfanficnet
|
||||
|
||||
|
||||
## This bit of complexity allows adapters to be added by just adding
|
||||
## importing. It eliminates the long if/else clauses we used to need
|
||||
## to pick out the adapter.
|
||||
|
||||
## List of registered site adapters.
|
||||
__class_list = []
|
||||
|
||||
def imports():
|
||||
for name, val in globals().items():
|
||||
if isinstance(val, types.ModuleType):
|
||||
yield val.__name__
|
||||
|
||||
for x in imports():
|
||||
if "fanficdownloader.adapters.adapter_" in x:
|
||||
#print x
|
||||
__class_list.append(sys.modules[x].getClass())
|
||||
|
||||
def getNormalStoryURL(url):
|
||||
if not getNormalStoryURL.__dummyconfig:
|
||||
getNormalStoryURL.__dummyconfig = Configuration("test1.com","EPUB")
|
||||
# pulling up an adapter is pretty low over-head. If
|
||||
# it fails, it's a bad url.
|
||||
try:
|
||||
adapter = getAdapter(getNormalStoryURL.__dummyconfig,url)
|
||||
url = adapter.url
|
||||
del adapter
|
||||
return url
|
||||
except:
|
||||
return None;
|
||||
|
||||
# kludgey function static/singleton
|
||||
getNormalStoryURL.__dummyconfig = None
|
||||
|
||||
def getAdapter(config,url):
|
||||
|
||||
logger.debug("trying url:"+url)
|
||||
(cls,fixedurl) = getClassFor(url)
|
||||
logger.debug("fixedurl:"+fixedurl)
|
||||
if cls:
|
||||
adapter = cls(config,fixedurl) # raises InvalidStoryURL
|
||||
return adapter
|
||||
# No adapter found.
|
||||
raise exceptions.UnknownSite( url, [cls.getSiteDomain() for cls in __class_list] )
|
||||
|
||||
def getConfigSections():
|
||||
return [cls.getConfigSection() for cls in __class_list]
|
||||
|
||||
def getConfigSectionFor(url):
|
||||
(cls,fixedurl) = getClassFor(url)
|
||||
if cls:
|
||||
return cls.getConfigSection()
|
||||
|
||||
# No adapter found.
|
||||
raise exceptions.UnknownSite( url, [cls.getSiteDomain() for cls in __class_list] )
|
||||
|
||||
def getClassFor(url):
|
||||
## fix up leading protocol.
|
||||
fixedurl = re.sub(r"(?i)^[htps]+[:/]+","http://",url.strip())
|
||||
if not fixedurl.startswith("http"):
|
||||
fixedurl = "http://%s"%url
|
||||
## remove any trailing '#' locations.
|
||||
fixedurl = re.sub(r"#.*$","",fixedurl)
|
||||
|
||||
## remove any trailing '&' parameters--?sid=999 will be left.
|
||||
## that's all that any of the current adapters need or want.
|
||||
fixedurl = re.sub(r"&.*$","",fixedurl)
|
||||
|
||||
parsedUrl = up.urlparse(fixedurl)
|
||||
domain = parsedUrl.netloc.lower()
|
||||
if( domain != parsedUrl.netloc ):
|
||||
fixedurl = fixedurl.replace(parsedUrl.netloc,domain)
|
||||
|
||||
cls = getClassFromList(domain)
|
||||
if not cls and domain.startswith("www."):
|
||||
domain = domain.replace("www.","")
|
||||
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)
|
||||
cls = getClassFromList("www."+domain)
|
||||
fixedurl = fixedurl.replace("http://","http://www.")
|
||||
|
||||
return (cls,fixedurl)
|
||||
|
||||
def getClassFromList(domain):
|
||||
for cls in __class_list:
|
||||
if cls.matchesSite(domain):
|
||||
return cls
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib
|
||||
import urllib2
|
||||
@@ -37,12 +38,11 @@ class AdAstraFanficComSiteAdapter(BaseSiteAdapter):
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
self.story.addToList("category","Star Trek")
|
||||
self.is_adult=False
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -66,7 +66,7 @@ class AdAstraFanficComSiteAdapter(BaseSiteAdapter):
|
||||
addurl=""
|
||||
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -205,7 +205,7 @@ class AdAstraFanficComSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
data = self._fetchUrl(url)
|
||||
# problems with some stories, but only in calibre. I suspect
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -30,6 +31,8 @@ def getClass():
|
||||
return ArchiveOfOurOwnOrgAdapter
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
@@ -48,10 +51,19 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/works/'+self.story.getMetadata('storyId'))
|
||||
# get storyId from url--url validation guarantees query correct
|
||||
m = re.match(self.getSiteURLPattern(),url)
|
||||
if m:
|
||||
self.story.setMetadata('storyId',m.group('id'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/works/'+self.story.getMetadata('storyId'))
|
||||
else:
|
||||
raise exceptions.InvalidStoryURL(url,
|
||||
self.getSiteDomain(),
|
||||
self.getSiteExampleURLs())
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','ao3')
|
||||
@@ -65,15 +77,12 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'archiveofourown.org'
|
||||
|
||||
@classmethod
|
||||
def getAcceptDomains(cls):
|
||||
return ['www.archiveofourown.org','archiveofourown.org']
|
||||
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://"+self.getSiteDomain()+"/works/123456"
|
||||
return "http://"+self.getSiteDomain()+"/works/123456 http://"+self.getSiteDomain()+"/collections/Some_Archive/works/123456"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://")+"(www\.)?"+re.escape(self.getSiteDomain()+"/works/")+r"\d+(/chapters/\d+)?/?$"
|
||||
# http://archiveofourown.org/collections/Smallville_Slash_Archive/works/159770
|
||||
return re.escape("http://")+re.escape(self.getSiteDomain())+r"(/collections/[^/]+)?/works/(?P<id>\d+)"
|
||||
|
||||
## Login
|
||||
def needToLoginCheck(self, data):
|
||||
@@ -98,14 +107,14 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
params['authenticity_token'] = data.split('input name="authenticity_token" type="hidden" value="')[1].split('" /></div>')[0]
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/user_sessions'
|
||||
logging.info("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
logger.info("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['user_session[login]']))
|
||||
|
||||
d = self._postUrl(loginUrl, params)
|
||||
#logging.info(d)
|
||||
#logger.info(d)
|
||||
|
||||
if "Successfully logged in" not in d : #Member Account
|
||||
logging.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['user_session[login]']))
|
||||
raise exceptions.FailedToLogin(url,params['user_session[login]'])
|
||||
return False
|
||||
@@ -122,8 +131,8 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
|
||||
metaurl = self.url+addurl
|
||||
url = self.url+'/navigate'+addurl
|
||||
logging.info("url: "+url)
|
||||
logging.info("metaurl: "+metaurl)
|
||||
logger.info("url: "+url)
|
||||
logger.info("metaurl: "+metaurl)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -158,7 +167,8 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
alist = soup.findAll('a', href=re.compile(r"^/users/\w+/pseuds/\w+"))
|
||||
if len(alist) < 1: # ao3 allows for author 'Anonymous' with no author link.
|
||||
self.story.setMetadata('author','Anonymous')
|
||||
self.story.setMetadata('authorUrl',self.url)
|
||||
self.story.setMetadata('authorUrl','http://archiveofourown.org/')
|
||||
self.story.setMetadata('authorId','0')
|
||||
else:
|
||||
for a in alist:
|
||||
self.story.addToList('authorId',a['href'].split('/')[2])
|
||||
@@ -168,7 +178,7 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
# Find the chapters:
|
||||
chapters=soup.findAll('a', href=re.compile(r'/works/'+self.story.getMetadata('storyId')+"/chapters/\d+$"))
|
||||
self.story.setMetadata('numChapters',len(chapters))
|
||||
logging.debug("numChapters: (%s)"%self.story.getMetadata('numChapters'))
|
||||
logger.debug("numChapters: (%s)"%self.story.getMetadata('numChapters'))
|
||||
for x in range(0,len(chapters)):
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
chapter=chapters[x]
|
||||
@@ -181,7 +191,7 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
|
||||
a = metasoup.find('blockquote',{'class':'userstuff'})
|
||||
if a != None:
|
||||
self.setDescription(url,a.text)
|
||||
self.setDescription(url,a)
|
||||
#self.story.setMetadata('description',a.text)
|
||||
|
||||
a = metasoup.find('dd',{'class':"rating tags"})
|
||||
@@ -191,6 +201,7 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
a = metasoup.find('dd',{'class':"fandom tags"})
|
||||
fandoms = a.findAll('a',{'class':"tag"})
|
||||
for fandom in fandoms:
|
||||
self.story.addToList('fandoms',fandom.string)
|
||||
self.story.addToList('category',fandom.string)
|
||||
|
||||
a = metasoup.find('dd',{'class':"warning tags"})
|
||||
@@ -206,12 +217,15 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
if a != None:
|
||||
genres = a.findAll('a',{'class':"tag"})
|
||||
for genre in genres:
|
||||
self.story.addToList('freefromtags',genre.string)
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
a = metasoup.find('dd',{'class':"category tags"})
|
||||
if a != None:
|
||||
genres = a.findAll('a',{'class':"tag"})
|
||||
for genre in genres:
|
||||
if genre != "Gen":
|
||||
self.story.addToList('ao3categories',genre.string)
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
a = metasoup.find('dd',{'class':"character tags"})
|
||||
@@ -221,9 +235,9 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
self.story.addToList('characters',char.string)
|
||||
a = metasoup.find('dd',{'class':"relationship tags"})
|
||||
if a != None:
|
||||
chars = a.findAll('a',{'class':"tag"})
|
||||
for char in chars:
|
||||
self.story.addToList('characters',char.string)
|
||||
ships = a.findAll('a',{'class':"tag"})
|
||||
for ship in ships:
|
||||
self.story.addToList('ships',ship.string)
|
||||
|
||||
|
||||
stats = metasoup.find('dl',{'class':'stats'})
|
||||
@@ -236,6 +250,18 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
if 'Words:' in label:
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
if 'Comments:' in label:
|
||||
self.story.setMetadata('comments', value)
|
||||
|
||||
if 'Kudos:' in label:
|
||||
self.story.setMetadata('kudos', value)
|
||||
|
||||
if 'Hits:' in label:
|
||||
self.story.setMetadata('hits', value)
|
||||
|
||||
if 'Bookmarks:' in label:
|
||||
self.story.setMetadata('bookmarks', value)
|
||||
|
||||
if 'Chapters:' in label:
|
||||
if value.split('/')[0] == value.split('/')[1]:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
@@ -269,7 +295,7 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
chapter=bs.BeautifulSoup('<div class="story"></div>')
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -48,7 +49,7 @@ class ArchiveSkyeHawkeComAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/story.php?no='+self.story.getMetadata('storyId'))
|
||||
@@ -78,7 +79,7 @@ class ArchiveSkyeHawkeComAdapter(BaseSiteAdapter):
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
url = self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -151,7 +152,7 @@ class ArchiveSkyeHawkeComAdapter(BaseSiteAdapter):
|
||||
rating.find('br').replaceWith('split')
|
||||
rating=rating.text.replace("This story is rated",'').split('split')[0]
|
||||
self.story.setMetadata('rating',rating)
|
||||
logging.debug(self.story.getMetadata('rating'))
|
||||
logger.debug(self.story.getMetadata('rating'))
|
||||
|
||||
warnings=box.find('ol')
|
||||
if warnings != None:
|
||||
@@ -177,7 +178,7 @@ class ArchiveSkyeHawkeComAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,7 +48,7 @@ class AshwinderSycophantHexComAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
@@ -56,11 +57,6 @@ class AshwinderSycophantHexComAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','asph')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Harry Potter")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%m/%d/%Y"
|
||||
@@ -99,13 +95,13 @@ class AshwinderSycophantHexComAdapter(BaseSiteAdapter):
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/user.php'
|
||||
logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Logout" not in d : #Member Account
|
||||
logging.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
@@ -117,7 +113,7 @@ class AshwinderSycophantHexComAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -242,7 +238,7 @@ class AshwinderSycophantHexComAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2011 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
|
||||
|
||||
# By virtue of being recent and requiring both is_adult and user/pass,
|
||||
# adapter_fanficcastletvnet.py is the best choice for learning to
|
||||
# write adapters--especially for sites that use the eFiction system.
|
||||
# Most sites that have ".../viewstory.php?sid=123" in the story URL
|
||||
# are eFiction.
|
||||
|
||||
# For non-eFiction sites, it can be considerably more complex, but
|
||||
# this is still a good starting point.
|
||||
|
||||
# In general an 'adapter' needs to do these five things:
|
||||
|
||||
# - 'Register' correctly with the downloader
|
||||
# - Site Login (if needed)
|
||||
# - 'Are you adult?' check (if needed--some do one, some the other, some both)
|
||||
# - Grab the chapter list
|
||||
# - Grab the story meta-data (some (non-eFiction) adapters have to get it from the author page)
|
||||
# - Grab the chapter texts
|
||||
|
||||
# Search for XXX comments--that's where things are most likely to need changing.
|
||||
|
||||
# This function is called by the downloader in all adapter_*.py files
|
||||
# in this dir to register the adapter class. So it needs to be
|
||||
# updated to reflect the class below it. That, plus getSiteDomain()
|
||||
# take care of 'Registering'.
|
||||
def getClass():
|
||||
return BloodTiesFansComAdapter # XXX
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class BloodTiesFansComAdapter(BaseSiteAdapter): # XXX
|
||||
|
||||
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.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.
|
||||
# XXX Most sites don't have the /fanfic part. Replace all to remove it usually.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/fiction/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','btf') # XXX
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%b %d, %Y" # XXX
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'bloodties-fans.com' # XXX
|
||||
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://"+self.getSiteDomain()+"/fiction/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain()+"/fiction/viewstory.php?sid=")+r"\d+$"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
if 'Registered Users Only' in data \
|
||||
or 'There is no such account on our website' in data \
|
||||
or "That password doesn't match the one in our database" in data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def performLogin(self, url):
|
||||
params = {}
|
||||
|
||||
if self.password:
|
||||
params['penname'] = self.username
|
||||
params['password'] = self.password
|
||||
else:
|
||||
params['penname'] = self.getConfig("username")
|
||||
params['password'] = self.getConfig("password")
|
||||
params['cookiecheck'] = '1'
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/fiction/user.php?action=login'
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Member Account" not in d : #Member Account
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
# Weirdly, different sites use different warning numbers.
|
||||
# If the title search below fails, there's a good chance
|
||||
# you need a different number. print data at that point
|
||||
# and see what the 'click here to continue' url says.
|
||||
|
||||
# Furthermore, there's a couple sites now with more than
|
||||
# one warning level for different ratings. And they're
|
||||
# fussy about it. midnightwhispers has three: 4, 2 & 1.
|
||||
# we'll try 1 first.
|
||||
addurl = "&ageconsent=ok&warning=4" # XXX
|
||||
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
|
||||
|
||||
# The actual text that is used to announce you need to be an
|
||||
# adult varies from site to site. Again, print data before
|
||||
# the title search to troubleshoot.
|
||||
|
||||
# Since the warning text can change by warning level, let's
|
||||
# look for the warning pass url. nfacommunity uses
|
||||
# &warning= -- actually, so do other sites. Must be an
|
||||
# eFiction book.
|
||||
|
||||
# viewstory.php?sid=561&warning=4
|
||||
# viewstory.php?sid=561&warning=1
|
||||
# viewstory.php?sid=561&warning=2
|
||||
#print data
|
||||
#m = re.search(r"'viewstory.php\?sid=1882(&warning=4)'",data)
|
||||
m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&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 & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
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+'/fiction/'+chapter['href']+addurl))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
# eFiction sites don't help us out a lot with their meta data
|
||||
# formating, so it's a little ugly.
|
||||
|
||||
# utility method
|
||||
def defaultGetattr(d,k):
|
||||
try:
|
||||
return d[k]
|
||||
except:
|
||||
return ""
|
||||
|
||||
# <span class="label">Rated:</span> NC-17<br /> etc
|
||||
labels = soup.findAll('span',{'class':'label'})
|
||||
for labelspan in labels:
|
||||
value = labelspan.nextSibling
|
||||
label = labelspan.string
|
||||
|
||||
if 'Summary' in label:
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
if 'Rated' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
if 'Categories' in label:
|
||||
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
catstext = [cat.string for cat in cats]
|
||||
for cat in catstext:
|
||||
self.story.addToList('category',cat.string)
|
||||
|
||||
if 'Characters' in label:
|
||||
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
|
||||
charstext = [char.string for char in chars]
|
||||
for char in charstext:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
## Not all sites use Genre, but there's no harm to
|
||||
## leaving it in. Check to make sure the type_id number
|
||||
## is correct, though--it's site specific.
|
||||
if 'Genre' in label:
|
||||
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX
|
||||
genrestext = [genre.string for genre in genres]
|
||||
self.genre = ', '.join(genrestext)
|
||||
for genre in genrestext:
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
## Not all sites use Warnings, but there's no harm to
|
||||
## leaving it in. Check to make sure the type_id number
|
||||
## is correct, though--it's site specific.
|
||||
if 'Warnings' in label:
|
||||
warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX
|
||||
warningstext = [warning.string for warning in warnings]
|
||||
self.warning = ', '.join(warningstext)
|
||||
for warning in warningstext:
|
||||
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:
|
||||
# there's a stray [ at the end.
|
||||
#value = value[0:-1]
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
try:
|
||||
# Find Series name from series URL.
|
||||
a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+"))
|
||||
series_name = a.string
|
||||
series_url = 'http://'+self.host+'/fiction/'+a['href']
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url))
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$'))
|
||||
i=1
|
||||
for a in storyas:
|
||||
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)
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -71,7 +72,7 @@ class CastleFansOrgAdapter(BaseSiteAdapter): # XXX
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
# XXX Most sites don't have the /fanfic part. Replace all to remove it usually.
|
||||
@@ -80,11 +81,6 @@ class CastleFansOrgAdapter(BaseSiteAdapter): # XXX
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','cslf') # XXX
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Castle") # XXX
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%b %d, %Y" # XXX
|
||||
@@ -122,13 +118,13 @@ class CastleFansOrgAdapter(BaseSiteAdapter): # XXX
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/fanfic/user.php?action=login'
|
||||
logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Member Account" not in d : #Member Account
|
||||
logging.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
@@ -150,7 +146,7 @@ class CastleFansOrgAdapter(BaseSiteAdapter): # XXX
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -179,13 +175,14 @@ class CastleFansOrgAdapter(BaseSiteAdapter): # XXX
|
||||
# print data
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
|
||||
pagetitle = soup.find('div',{'id':'pagetitle'})
|
||||
## Title
|
||||
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
|
||||
a = pagetitle.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+"))
|
||||
a = pagetitle.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)
|
||||
@@ -297,7 +294,7 @@ class CastleFansOrgAdapter(BaseSiteAdapter): # XXX
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,7 +48,7 @@ class ChaosSycophantHexComAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
@@ -56,11 +57,6 @@ class ChaosSycophantHexComAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','csph')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
#self.story.addToList("category","Harry Potter")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%m/%d/%Y"
|
||||
@@ -91,7 +87,7 @@ class ChaosSycophantHexComAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -226,7 +222,7 @@ class ChaosSycophantHexComAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -44,25 +45,20 @@ class CheckmatedComAdapter(BaseSiteAdapter):
|
||||
self.username = "" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
self.is_adult=False
|
||||
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
self._setURL('http://' + self.getSiteDomain() + '/story.php?story='+self.story.getMetadata('storyId'))
|
||||
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','chm')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Harry Potter")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%b %d, %Y"
|
||||
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
@@ -82,7 +78,7 @@ class CheckmatedComAdapter(BaseSiteAdapter):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def performLogin(self, url):
|
||||
params = {}
|
||||
|
||||
@@ -93,14 +89,14 @@ class CheckmatedComAdapter(BaseSiteAdapter):
|
||||
params['name'] = self.getConfig("username")
|
||||
params['pass'] = self.getConfig("password")
|
||||
params['login'] = 'yes'
|
||||
params['submit'] = 'login'
|
||||
|
||||
params['submit'] = 'login'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain()+'/login.php'
|
||||
d = self._fetchUrl(loginUrl,params)
|
||||
e = self._fetchUrl(url)
|
||||
|
||||
if "Welcome back," not in d : #Member Account
|
||||
logging.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['name']))
|
||||
raise exceptions.FailedToLogin(url,params['name'])
|
||||
return False
|
||||
@@ -117,7 +113,7 @@ class CheckmatedComAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -135,33 +131,33 @@ class CheckmatedComAdapter(BaseSiteAdapter):
|
||||
# The actual text that is used to announce you need to be an
|
||||
# adult varies from site to site. Again, print data before
|
||||
# the title search to troubleshoot.
|
||||
|
||||
|
||||
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.findAll('span', {'class' : 'storytitle'})
|
||||
self.story.setMetadata('title',a[0].string)
|
||||
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
a = a[1].find('a', href=re.compile(r"authors.php\?name\=\w+"))
|
||||
self.story.setMetadata('authorId',a['href'].split('=')[1])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href'])
|
||||
self.story.setMetadata('author',a.string)
|
||||
|
||||
|
||||
a = soup.find('select', {'name' : 'chapter'})
|
||||
if a == None:
|
||||
self.chapterUrls.append((self.story.getMetadata('title'),url))
|
||||
else:
|
||||
for chapter in a.findAll('option'):
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/story.php?story='+self.story.getMetadata('storyId')+'&chapter='+chapter['value']))
|
||||
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
# eFiction sites don't help us out a lot with their meta data
|
||||
@@ -174,7 +170,7 @@ class CheckmatedComAdapter(BaseSiteAdapter):
|
||||
except:
|
||||
return ""
|
||||
|
||||
|
||||
|
||||
# website does not keep track of word count, and there is no convenient way to calculate it
|
||||
|
||||
summary = soup.find('fieldset')
|
||||
@@ -182,21 +178,23 @@ class CheckmatedComAdapter(BaseSiteAdapter):
|
||||
summary.name='div'
|
||||
self.setDescription(url,summary)
|
||||
|
||||
|
||||
|
||||
# <span class="label">Rated:</span> NC-17<br /> etc
|
||||
table = soup.findAll('div', {'class' : 'text'})[1]
|
||||
for labels in table.findAll('tr'):
|
||||
value = labels.findAll('td')[1]
|
||||
label = labels.findAll('td')[0]
|
||||
|
||||
|
||||
|
||||
if 'Rating' in stripHTML(label):
|
||||
self.story.setMetadata('rating', stripHTML(value))
|
||||
|
||||
if 'Ship' in stripHTML(label):
|
||||
if value.string != "none/none":
|
||||
self.story.addToList('ships',value.string)
|
||||
for char in value.string.split('/'):
|
||||
if char != 'none':
|
||||
self.story.addToList('characters',char)
|
||||
self.story.addToList('characters',char)
|
||||
|
||||
if 'Status' in stripHTML(label):
|
||||
if value.find('img', {'src' : 'img/incomplete.gif'}) == None:
|
||||
@@ -206,11 +204,11 @@ class CheckmatedComAdapter(BaseSiteAdapter):
|
||||
|
||||
if 'Published' in stripHTML(label):
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
|
||||
if 'Updated' in stripHTML(label):
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
a = self._fetchUrl(self.story.getMetadata('authorUrl')+'&cat=stories')
|
||||
|
||||
a = self._fetchUrl(self.story.getMetadata('authorUrl')+'&cat=stories')
|
||||
for story in bs.BeautifulSoup(a).findAll('table', {'class' : 'storyinfo'}):
|
||||
a = story.find('a', href=re.compile(r"review.php\?s\="+self.story.getMetadata('storyId')+'&act=view'))
|
||||
if a != None:
|
||||
@@ -220,20 +218,20 @@ class CheckmatedComAdapter(BaseSiteAdapter):
|
||||
if 'genre' in stripHTML(label):
|
||||
for genre in value.findAll('img'):
|
||||
self.story.addToList('genre',genre['title'])
|
||||
|
||||
|
||||
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(url),
|
||||
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
|
||||
|
||||
|
||||
div = soup.find('div', {'id' : 'resizeableText'})
|
||||
div.find('div', {'class' : 'storyTools'}).extract()
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,7 +48,7 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
@@ -56,11 +57,6 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','dksl')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Buffy the Vampire Slayer")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%d %B %Y"
|
||||
@@ -103,13 +99,13 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/elysian/user.php'
|
||||
logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "User Account Page" not in d : #Member Account
|
||||
logging.info("Failed to login to URL %s as %s, or have no authorization to access the story" % (loginUrl, params['penname']))
|
||||
logger.info("Failed to login to URL %s as %s, or have no authorization to access the story" % (loginUrl, params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
else:
|
||||
@@ -120,7 +116,7 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -266,7 +262,7 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,7 +48,7 @@ class DestinysGatewayComAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -86,7 +87,7 @@ class DestinysGatewayComAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -106,7 +107,7 @@ class DestinysGatewayComAdapter(BaseSiteAdapter):
|
||||
# correct stupid & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL 2nd try: "+url)
|
||||
logger.debug("URL 2nd try: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -227,7 +228,7 @@ class DestinysGatewayComAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,8 +48,14 @@ class DokugaComAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[3])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
self.story.setMetadata('section',self.parsedUrl.path.split('/',)[1])
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# www.dokuga.com has two 'sections', shown in URL as
|
||||
# 'fanfiction' and 'spark' that change how things should be
|
||||
# handled.
|
||||
# http://www.dokuga.com/fanfiction/story/7528/1
|
||||
# http://www.dokuga.com/spark/story/7299/1
|
||||
self.section=self.parsedUrl.path.split('/',)[1]
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/'+self.parsedUrl.path.split('/',)[1]+'/story/'+self.story.getMetadata('storyId'))
|
||||
@@ -56,14 +63,9 @@ class DokugaComAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','dkg')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","InuYasha")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
if 'fanfiction' in self.story.getMetadata('section'):
|
||||
if 'fanfiction' in self.section:
|
||||
self.dateformat = "%d %b %Y"
|
||||
else:
|
||||
self.dateformat = "%m-%d-%y"
|
||||
@@ -86,7 +88,7 @@ class DokugaComAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -122,17 +124,17 @@ class DokugaComAdapter(BaseSiteAdapter):
|
||||
# Find the chapters:
|
||||
chapters = soup.find('select').findAll('option')
|
||||
if len(chapters)==1:
|
||||
self.chapterUrls.append((self.story.getMetadata('title'),'http://'+self.host+'/'+self.story.getMetadata('section')+'/story/'+self.story.getMetadata('storyId')+'/1'))
|
||||
self.chapterUrls.append((self.story.getMetadata('title'),'http://'+self.host+'/'+self.section+'/story/'+self.story.getMetadata('storyId')+'/1'))
|
||||
else:
|
||||
for chapter in chapters:
|
||||
# just in case there's tags, like <i> in chapter titles. /fanfiction/story/7406/1
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+self.story.getMetadata('section')+'/story/'+self.story.getMetadata('storyId')+'/'+chapter['value']))
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+self.section+'/story/'+self.story.getMetadata('storyId')+'/'+chapter['value']))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
asoup = bs.BeautifulSoup(self._fetchUrl(alink))
|
||||
|
||||
if 'fanfiction' in self.story.getMetadata('section'):
|
||||
if 'fanfiction' in self.section:
|
||||
asoup=asoup.find('div', {'id' : 'cb_tabid_52'}).find('div')
|
||||
|
||||
#grab the rest of the metadata from the author's page
|
||||
@@ -223,7 +225,7 @@ class DokugaComAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2012 Fanficdownloader team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import time
|
||||
import logging
|
||||
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 DracoAndGinnyComAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class DracoAndGinnyComAdapter(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','dcagn')
|
||||
|
||||
# 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.dracoandginny.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+$"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
if 'Registered Users Only' in data \
|
||||
or 'There is no such account on our website' in data \
|
||||
or "That password doesn't match the one in our database" in data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def performLogin(self, url):
|
||||
params = {}
|
||||
|
||||
if self.password:
|
||||
params['penname'] = self.username
|
||||
params['password'] = self.password
|
||||
else:
|
||||
params['penname'] = self.getConfig("username")
|
||||
params['password'] = self.getConfig("password")
|
||||
params['cookiecheck'] = '1'
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/user.php?action=login'
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Member Account" not in d : #Member Account
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
# Weirdly, different sites use different warning numbers.
|
||||
# If the title search below fails, there's a good chance
|
||||
# you need a different number. print data at that point
|
||||
# and see what the 'click here to continue' url says.
|
||||
addurl = "&ageconsent=ok&warning=2"
|
||||
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
|
||||
|
||||
if self.needToLoginCheck(data):
|
||||
# need to log in for this one.
|
||||
self.performLogin(url)
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&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 & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
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
|
||||
content=soup.find('div',{'class' : 'listbox'})
|
||||
|
||||
self.setDescription(url,content.find('blockquote'))
|
||||
|
||||
for genre in content.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')):
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
for warning in content.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')):
|
||||
self.story.addToList('warnings',warning.string)
|
||||
|
||||
labels = content.findAll('b')
|
||||
|
||||
for labelspan in labels:
|
||||
value = labelspan.nextSibling
|
||||
label = labelspan.string
|
||||
|
||||
if 'Summary' in label:
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value.split(' |')[0])
|
||||
|
||||
if 'Rating' in label:
|
||||
self.story.setMetadata('rating', 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.nextSibling).split(' |')[0], 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', {'class' : 'listbox'})
|
||||
|
||||
if None == div:
|
||||
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)
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,7 +48,7 @@ class DramioneOrgAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -55,11 +56,6 @@ class DramioneOrgAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','drmn')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Harry Potter")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%d %B %Y"
|
||||
@@ -97,13 +93,13 @@ class DramioneOrgAdapter(BaseSiteAdapter):
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/user.php?action=login'
|
||||
logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Member Account" not in d : #Member Account
|
||||
logging.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
@@ -125,7 +121,7 @@ class DramioneOrgAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -165,6 +161,20 @@ class DramioneOrgAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href'])
|
||||
self.story.setMetadata('author',a.string)
|
||||
|
||||
# Use banner as cover if found
|
||||
coverurl = ''
|
||||
img = soup.find('img',{'class':'banner'})
|
||||
if img:
|
||||
coverurl = img['src']
|
||||
#print "Cover: "+coverurl
|
||||
a = soup.find(text="This story has a banner; click to view.")
|
||||
if a:
|
||||
#print "A: "+ ', '.join("(%s, %s)" %tup for tup in a.parent.attrs)
|
||||
coverurl = a.parent['href']
|
||||
#print "Cover: "+coverurl
|
||||
if coverurl:
|
||||
self.setCoverImage(url,coverurl)
|
||||
|
||||
# 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.
|
||||
@@ -183,6 +193,22 @@ class DramioneOrgAdapter(BaseSiteAdapter):
|
||||
for warning in warnings:
|
||||
self.story.addToList('warnings',warning.string)
|
||||
|
||||
themes=soup.findAll('a', {'class' : "tag-3"})
|
||||
for theme in themes:
|
||||
self.story.addToList('themes',theme.string)
|
||||
|
||||
hermiones=soup.findAll('a', {'class' : "tag-4"})
|
||||
for hermione in hermiones:
|
||||
self.story.addToList('hermiones',hermione.string)
|
||||
|
||||
dracos=soup.findAll('a', {'class' : "tag-5"})
|
||||
for draco in dracos:
|
||||
self.story.addToList('dracos',draco.string)
|
||||
|
||||
timelines=soup.findAll('a', {'class' : "tag-6"})
|
||||
for timeline in timelines:
|
||||
self.story.addToList('timeline',timeline.string)
|
||||
|
||||
# utility method
|
||||
def defaultGetattr(d,k):
|
||||
try:
|
||||
@@ -258,7 +284,7 @@ class DramioneOrgAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2012 Fanficdownloader team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import time
|
||||
import logging
|
||||
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 EFPFanFicNet
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class EFPFanFicNet(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','efp')
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%d/%m/%y"
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'www.efpfanfic.net'
|
||||
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
if 'Fai il login e leggi la storia!' in data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def performLogin(self, url):
|
||||
params = {}
|
||||
|
||||
if self.password:
|
||||
params['penname'] = self.username
|
||||
params['password'] = self.password
|
||||
else:
|
||||
params['penname'] = self.getConfig("username")
|
||||
params['password'] = self.getConfig("password")
|
||||
params['cookiecheck'] = '1'
|
||||
params['submit'] = 'Invia'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/user.php?sid='+self.story.getMetadata('storyId')
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if '<a class="menu" href="newaccount.php">' in d : # register for new account link
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
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 self.needToLoginCheck(data):
|
||||
# need to log in for this one.
|
||||
self.performLogin(url)
|
||||
data = self._fetchUrl(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',stripHTML(a))
|
||||
|
||||
# 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 chapter selector
|
||||
select = soup.find('select', { 'name' : 'sid' } )
|
||||
|
||||
if select is None:
|
||||
# no selector found, so it's a one-chapter story.
|
||||
self.chapterUrls.append((self.story.getMetadata('title'),url))
|
||||
else:
|
||||
allOptions = select.findAll('option', {'value' : re.compile(r'viewstory')})
|
||||
for o in allOptions:
|
||||
url = u'http://%s/%s' % ( self.getSiteDomain(),
|
||||
o['value'])
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
title = stripHTML(o)
|
||||
self.chapterUrls.append((title,url))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
self.story.setMetadata('language','Italian')
|
||||
|
||||
# normalize story URL to first chapter if later chapter URL was given:
|
||||
url = self.chapterUrls[0][1].replace('&i=1','')
|
||||
logger.debug("Normalizing to URL: "+url)
|
||||
self._setURL(url)
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# eFiction sites don't help us out a lot with their meta data
|
||||
# formating, so it's a little ugly.
|
||||
|
||||
storya = None
|
||||
authsoup = None
|
||||
storyblock = None
|
||||
authurl = self.story.getMetadata('authorUrl')
|
||||
|
||||
## author can have more than one page of stories.
|
||||
while storyblock == None:
|
||||
|
||||
# no storya, but do have authsoup--we're looping on author pages.
|
||||
if authsoup != None:
|
||||
# last author link with offset should be the 'next' link.
|
||||
authurl = u'http://%s/%s' % ( self.getSiteDomain(),
|
||||
authsoup.findAll('a',href=re.compile(r'viewuser\.php\?uid=\d+&catid=&offset='))[-1]['href'] )
|
||||
|
||||
# Need author page for most of the metadata.
|
||||
logger.debug("fetching author page: (%s)"%authurl)
|
||||
authsoup = bs.BeautifulSoup(self._fetchUrl(authurl))
|
||||
#print("authsoup:%s"%authsoup)
|
||||
|
||||
storyas = authsoup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+r'&i=1$'))
|
||||
for storya in storyas:
|
||||
#print("======storya:%s"%storya)
|
||||
storyblock = storya.findParent('div',{'class':'storybloc'})
|
||||
#print("======storyblock:%s"%storyblock)
|
||||
if storyblock != None:
|
||||
continue
|
||||
|
||||
self.setDescription(url,storyblock.find('div', {'class':'introbloc'}))
|
||||
|
||||
noteblock = storyblock.find('div', {'class':'notebloc'})
|
||||
#print("%s"%noteblock)
|
||||
notetext = ("%s" % noteblock).replace("<br />"," |")
|
||||
# <div class="notebloc">Autore: <a href="viewuser.php?uid=243036">Cendrillon89</a> | Pubblicata: 23/10/12 | Aggiornata: 30/10/12 | Rating: Arancione | Genere: Drammatico, Sentimentale | Capitoli: 10 | Completa<br />␍
|
||||
# Tipo di coppia: Het | Personaggi: Akasuna no Sasori , Akatsuki, Nuovo Personaggio | Note: OOC | Avvertimenti: Tematiche delicate<br />␍
|
||||
# Categoria: <a href="categories.php?catid=1&parentcatid=1">Anime & Manga</a> > <a href="categories.php?catid=108&parentcatid=108">Naruto</a> | Contesto: Naruto Shippuuden | Leggi le <a href="reviews.php?sid=1331275&a=">3</a> recensioni</div>
|
||||
|
||||
cats = noteblock.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
for cat in cats:
|
||||
self.story.addToList('category',cat.string)
|
||||
|
||||
for item in notetext.split("|"):
|
||||
if ":" in item:
|
||||
(label,value) = item.split(":")
|
||||
label=label.strip()
|
||||
value=value.strip()
|
||||
else:
|
||||
label=value=item.strip()
|
||||
|
||||
if 'Pubblicata' in label:
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
if 'Aggiornata' in label:
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
if label == "Completa":
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
|
||||
if label == "In corso":
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
if 'Rating' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
if 'Personaggi' in label:
|
||||
for val in value.split(","):
|
||||
self.story.addToList('characters',val)
|
||||
|
||||
if 'Genere' in label:
|
||||
for val in value.split(","):
|
||||
self.story.addToList('genre',val)
|
||||
|
||||
if 'Coppie' in label:
|
||||
for val in value.split(","):
|
||||
self.story.addToList('ships',val)
|
||||
|
||||
if 'Avvertimenti' in label:
|
||||
for val in value.split(","):
|
||||
if val != "None":
|
||||
self.story.addToList('warnings',val)
|
||||
|
||||
# 'extra' metadata for this adapter:
|
||||
|
||||
if 'Tipo di coppia' in label:
|
||||
for val in value.split(","):
|
||||
self.story.addToList('type',val)
|
||||
|
||||
if 'Note' in label:
|
||||
for val in value.split(","):
|
||||
if val != "None":
|
||||
self.story.addToList('notes',val)
|
||||
|
||||
if 'Contesto' in label:
|
||||
self.story.setMetadata('context', value)
|
||||
|
||||
## Note--efp doesn't provide word count.
|
||||
|
||||
try:
|
||||
# Find Series name from series URL.
|
||||
a = soup.find('a', href=re.compile(r"viewseries.php\?ssid=\d+&i=1"))
|
||||
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'))
|
||||
i=1
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId'))+'&i=1':
|
||||
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.BeautifulSoup(self._fetchUrl(url))
|
||||
|
||||
div = soup.find('div', {'class' : 'storia'})
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
# remove any header and 'o:p' tags.
|
||||
for tag in div.findAll("head") + div.findAll("o:p"):
|
||||
tag.extract()
|
||||
|
||||
# change any html and body tags to div.
|
||||
for tag in div.findAll("html") + div.findAll("body"):
|
||||
tag.name='div'
|
||||
|
||||
# remove extra bogus doctype.
|
||||
#<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
|
||||
return re.sub(r"<!DOCTYPE[^>]+>","",self.utf8FromSoup(url,div))
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,7 +48,7 @@ class ErosnSapphoSycophantHexComAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
@@ -56,11 +57,6 @@ class ErosnSapphoSycophantHexComAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','essph')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Harry Potter")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%d/%m/%y"
|
||||
@@ -91,7 +87,7 @@ class ErosnSapphoSycophantHexComAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -111,7 +107,7 @@ class ErosnSapphoSycophantHexComAdapter(BaseSiteAdapter):
|
||||
# correct stupid & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL 2nd try: "+url)
|
||||
logger.debug("URL 2nd try: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -244,7 +240,7 @@ class ErosnSapphoSycophantHexComAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
from urllib import unquote_plus
|
||||
@@ -75,12 +76,12 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
# metadata and chapter list
|
||||
|
||||
url = self.origurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
#print("\n===================\n%s\n===================\n"%data)
|
||||
#logger.debug("\n===================\n%s\n===================\n"%data)
|
||||
soup = bs.BeautifulSoup(data)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
@@ -108,11 +109,11 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
tryurl = "http://%s/s/%s/%d/"%(self.getSiteDomain(),
|
||||
self.story.getMetadata('storyId'),
|
||||
chapcount+1)
|
||||
print('=Trying newer chapter: %s' % tryurl)
|
||||
logger.debug('=Trying newer chapter: %s' % tryurl)
|
||||
newdata = self._fetchUrl(tryurl)
|
||||
if "not found. Please check to see you are not using an outdated url." \
|
||||
not in newdata:
|
||||
print('=======Found newer chapter: %s' % tryurl)
|
||||
logger.debug('=======Found newer chapter: %s' % tryurl)
|
||||
soup = bs.BeautifulSoup(newdata)
|
||||
except:
|
||||
pass
|
||||
@@ -160,7 +161,7 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
metatext = stripHTML(gui_table1i.find('div', {'style':'color:gray;'})).replace('Hurt/Comfort','Hurt-Comfort')
|
||||
metalist = metatext.split(" - ")
|
||||
#print("metatext:(%s)"%metalist)
|
||||
#logger.debug("metatext:(%s)"%metalist)
|
||||
|
||||
# Rated: Fiction K - English - Words: 158,078 - Published: 02-04-11
|
||||
|
||||
@@ -176,7 +177,9 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
genrelist = metalist[0].split('/') # Hurt/Comfort already changed above.
|
||||
goodgenres=True
|
||||
for g in genrelist:
|
||||
if g not in ffnetgenres:
|
||||
#logger.debug("g:(%s)"%g)
|
||||
if g.strip() not in ffnetgenres:
|
||||
logger.info("g not in ffnetgenres")
|
||||
goodgenres=False
|
||||
if goodgenres:
|
||||
self.story.extendList('genre',genrelist)
|
||||
@@ -184,8 +187,14 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
donechars = False
|
||||
while len(metalist) > 0:
|
||||
if metalist[0].startswith('Reviews') or metalist[0].startswith('Chapters') or metalist[0].startswith('Status') or metalist[0].startswith('id:') or metalist[0].startswith('Favs:') or metalist[0].startswith('Follows:'):
|
||||
if metalist[0].startswith('Chapters') or metalist[0].startswith('Status') or metalist[0].startswith('id:'):
|
||||
pass
|
||||
elif metalist[0].startswith('Reviews'):
|
||||
self.story.setMetadata('reviews',metalist[0].split(':')[1].strip())
|
||||
elif metalist[0].startswith('Favs:'):
|
||||
self.story.setMetadata('favs',metalist[0].split(':')[1].strip())
|
||||
elif metalist[0].startswith('Follows:'):
|
||||
self.story.setMetadata('follows',metalist[0].split(':')[1].strip())
|
||||
elif metalist[0].startswith('Updated'):
|
||||
self.story.setMetadata('dateUpdated',makeDate(metalist[0].split(':')[1].strip(), '%m-%d-%y'))
|
||||
elif metalist[0].startswith('Published'):
|
||||
@@ -206,10 +215,9 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
if self.getConfig('include_images'):
|
||||
img = soup.find('img',{'class':'cimage'})
|
||||
if img:
|
||||
self.story.addImgUrl(self,url,img['src'],self._fetchUrlRaw,cover=True)
|
||||
img = soup.find('img',{'class':'cimage'})
|
||||
if img:
|
||||
self.setCoverImage(url,img['src'])
|
||||
|
||||
# Find the chapter selector
|
||||
select = soup.find('select', { 'name' : 'chapter' } )
|
||||
@@ -233,12 +241,21 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
return
|
||||
|
||||
def getChapterText(self, url):
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
time.sleep(0.5) ## ffnet(and, I assume, fpcom) tends to fail
|
||||
## more if hit too fast. This is in
|
||||
## additional to what ever the
|
||||
## slow_down_sleep_time setting is.
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
# some ancient stories have body tags inside them that cause
|
||||
# soup parsing to discard the content. For story text we
|
||||
# don't care about anything before "<div class='storytextp"
|
||||
# (there's a space after storytextp, so no close quote(')) and
|
||||
# this kills any body tags.
|
||||
data = data[data.index("<div class='storytextp"):]
|
||||
data.replace("<body","<notbody").replace("<BODY","<NOTBODY")
|
||||
|
||||
soup = bs.BeautifulSoup(data)
|
||||
|
||||
## Remove the 'share' button.
|
||||
@@ -249,7 +266,7 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
div = soup.find('div', {'id' : 'storytextp'})
|
||||
|
||||
if None == div:
|
||||
logging.debug('div id=storytextp not found. data:%s'%data)
|
||||
logger.debug('div id=storytextp not found. data:%s'%data)
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib
|
||||
import urllib2
|
||||
@@ -25,6 +26,7 @@ from .. import BeautifulSoup as bs
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
def getClass():
|
||||
@@ -48,7 +50,7 @@ class FanFiktionDeAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/s/'+self.story.getMetadata('storyId') + '/1')
|
||||
@@ -94,12 +96,12 @@ class FanFiktionDeAdapter(BaseSiteAdapter):
|
||||
params['submit'] = 'Login...'
|
||||
|
||||
loginUrl = 'https://ssl.fanfiktion.de/'
|
||||
logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['nickname']))
|
||||
d = self._postUrl(loginUrl,params)
|
||||
|
||||
if "Login erfolgreich" not in d : #Member Account
|
||||
logging.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['nickname']))
|
||||
raise exceptions.FailedToLogin(url,params['nickname'])
|
||||
return False
|
||||
@@ -110,7 +112,7 @@ class FanFiktionDeAdapter(BaseSiteAdapter):
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
url = self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -139,7 +141,7 @@ class FanFiktionDeAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('title',a.string)
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
head = soup.find('div', {'style' : 'width:85%;float:left;'})
|
||||
head = soup.find('div', {'class' : 'story-metadata-left-top'})
|
||||
a = head.find('a')
|
||||
self.story.setMetadata('authorId',a['href'].split('/')[2])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href'])
|
||||
@@ -175,7 +177,7 @@ class FanFiktionDeAdapter(BaseSiteAdapter):
|
||||
self.setDescription(url,a['onmouseover'].split("', '")[1])
|
||||
|
||||
td = tr[i].findAll('td')
|
||||
self.story.addToList('category',td[1].string)
|
||||
self.story.addToList('category',stripHTML(td[1]))
|
||||
self.story.setMetadata('rating', td[4].string)
|
||||
self.story.setMetadata('numWords', td[5].string)
|
||||
|
||||
@@ -183,7 +185,7 @@ class FanFiktionDeAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
time.sleep(0.5) ## ffde has "floodlock" protection
|
||||
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(url),
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import time
|
||||
import datetime
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
from .. import translit
|
||||
@@ -33,6 +34,8 @@ def getClass():
|
||||
return FicBookNetAdapter
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class FicBookNetAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
@@ -49,7 +52,7 @@ class FicBookNetAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/readfic/'+self.story.getMetadata('storyId'))
|
||||
@@ -75,7 +78,7 @@ class FicBookNetAdapter(BaseSiteAdapter):
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
url=self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
@@ -95,14 +98,14 @@ class FicBookNetAdapter(BaseSiteAdapter):
|
||||
## Title
|
||||
a = soup.find('h1')
|
||||
self.story.setMetadata('title',stripHTML(a))
|
||||
logging.debug("Title: (%s)"%self.story.getMetadata('title'))
|
||||
logger.debug("Title: (%s)"%self.story.getMetadata('title'))
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
a = table.find('a')
|
||||
self.story.setMetadata('authorId',a.text) # Author's name is unique
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href'])
|
||||
self.story.setMetadata('author',a.text)
|
||||
logging.debug("Author: (%s)"%self.story.getMetadata('author'))
|
||||
logger.debug("Author: (%s)"%self.story.getMetadata('author'))
|
||||
|
||||
# Find the chapters:
|
||||
chapters = soup.find('div', {'class' : 'part_list'})
|
||||
@@ -123,7 +126,7 @@ class FicBookNetAdapter(BaseSiteAdapter):
|
||||
pubdate=translit.translit(stripHTML(soup.find('div', {'class' : 'part_added'}).find('span')))
|
||||
update=pubdate
|
||||
|
||||
logging.debug("numChapters: (%s)"%self.story.getMetadata('numChapters'))
|
||||
logger.debug("numChapters: (%s)"%self.story.getMetadata('numChapters'))
|
||||
|
||||
if not ',' in pubdate:
|
||||
pubdate=datetime.date.today().strftime(self.dateformat)
|
||||
@@ -201,13 +204,13 @@ class FicBookNetAdapter(BaseSiteAdapter):
|
||||
break
|
||||
|
||||
summary=soup.find('span', {'class' : 'urlize'})
|
||||
self.setDescription(url,summary.text)
|
||||
self.setDescription(url,summary)
|
||||
#self.story.setMetadata('description', summary.text)
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib
|
||||
import urllib2
|
||||
@@ -37,7 +38,6 @@ class FictionAlleyOrgSiteAdapter(BaseSiteAdapter):
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
self.story.addToList("category","Harry Potter")
|
||||
self.is_adult=False
|
||||
|
||||
# get storyId from url--url validation guarantees query correct
|
||||
@@ -45,7 +45,7 @@ class FictionAlleyOrgSiteAdapter(BaseSiteAdapter):
|
||||
if m:
|
||||
self.story.setMetadata('authorId',m.group('auth'))
|
||||
self.story.setMetadata('storyId',m.group('id'))
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
# normalized story URL.
|
||||
self._setURL(url)
|
||||
else:
|
||||
@@ -69,7 +69,7 @@ class FictionAlleyOrgSiteAdapter(BaseSiteAdapter):
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
params={'iamold':'Yes',
|
||||
'action':'ageanswer'}
|
||||
logging.info("Attempting to get cookie for %s" % url)
|
||||
logger.info("Attempting to get cookie for %s" % url)
|
||||
## posting on list doesn't work, but doesn't hurt, either.
|
||||
data = self._postUrl(url,params)
|
||||
else:
|
||||
@@ -80,7 +80,7 @@ class FictionAlleyOrgSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
## could be either chapter list page or one-shot text page.
|
||||
url = self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._postFetchWithIAmOld(url)
|
||||
@@ -108,7 +108,7 @@ class FictionAlleyOrgSiteAdapter(BaseSiteAdapter):
|
||||
storya = soup.find('div',{'class':'breadcrumbs'}).findAll('a')[1]
|
||||
self._setURL(storya['href'])
|
||||
url=self.url
|
||||
logging.debug("Normalizing to URL: "+url)
|
||||
logger.debug("Normalizing to URL: "+url)
|
||||
## title's right there...
|
||||
self.story.setMetadata('title',storya.string)
|
||||
data = self._fetchUrl(url)
|
||||
@@ -194,7 +194,7 @@ class FictionAlleyOrgSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
data = self._fetchUrl(url)
|
||||
# find <!-- headerend --> & <!-- footerstart --> and
|
||||
@@ -223,7 +223,16 @@ class FictionAlleyOrgSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
if not data or not text:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
|
||||
# not sure how, but we can get html, etc tags still in some
|
||||
# stories. That breaks later updates because it confuses
|
||||
# epubutils.py
|
||||
for tag in text.findAll('head'):
|
||||
tag.extract()
|
||||
|
||||
for tag in text.findAll('body') + text.findAll('html'):
|
||||
tag.name = 'div'
|
||||
|
||||
return self.utf8FromSoup(url,text)
|
||||
|
||||
def getClass():
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
import time
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
import time
|
||||
@@ -61,12 +62,12 @@ class FicwadComSiteAdapter(BaseSiteAdapter):
|
||||
params['password'] = self.getConfig("password")
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/account/login'
|
||||
logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['username']))
|
||||
d = self._postUrl(loginUrl,params)
|
||||
|
||||
if "Login attempt failed..." in d:
|
||||
logging.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['username']))
|
||||
raise exceptions.FailedToLogin(url,params['username'])
|
||||
return False
|
||||
@@ -79,7 +80,7 @@ class FicwadComSiteAdapter(BaseSiteAdapter):
|
||||
# metadata and chapter list
|
||||
|
||||
url = self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
try:
|
||||
@@ -96,7 +97,7 @@ class FicwadComSiteAdapter(BaseSiteAdapter):
|
||||
# normalize story URL on chapter list.
|
||||
self.story.setMetadata('storyId',storya['href'].split('/',)[2])
|
||||
url = "http://"+self.getSiteDomain()+storya['href']
|
||||
logging.debug("Normalizing to URL: "+url)
|
||||
logger.debug("Normalizing to URL: "+url)
|
||||
self._setURL(url)
|
||||
try:
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(url))
|
||||
@@ -124,7 +125,7 @@ class FicwadComSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
# description
|
||||
storydiv = soup.find("div",{"id":"story"})
|
||||
self.setDescription(url,storydiv.find("blockquote",{'class':'summary'}).p.string)
|
||||
self.setDescription(url,storydiv.find("blockquote",{'class':'summary'}).p)
|
||||
#self.story.setMetadata('description', storydiv.find("blockquote",{'class':'summary'}).p.string)
|
||||
|
||||
# most of the meta data is here:
|
||||
@@ -201,7 +202,7 @@ class FicwadComSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
|
||||
def getChapterText(self, url):
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
import cookielib as cl
|
||||
@@ -27,6 +28,8 @@ 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():
|
||||
@@ -122,10 +125,12 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata("numChapters", len(self.chapterUrls))
|
||||
|
||||
# In the case of fimfiction.net, possible statuses are 'Completed', 'Incomplete', 'On Hiatus' and 'Cancelled'
|
||||
# For the sake of bringing it in line with the other adapters, 'Incomplete' and 'On Hiatus' become 'In-Progress'
|
||||
# For the sake of bringing it in line with the other adapters, 'Incomplete' becomes 'In-Progress'
|
||||
# and 'Complete' beomes 'Completed'. 'Cancelled' seems an important enough (not to mention more strictly true)
|
||||
# status to leave unchanged.
|
||||
status = storyMetadata["status"].replace("Incomplete", "In-Progress").replace("On Hiatus", "In-Progress").replace("Complete", "Completed")
|
||||
# Nov2012 - 'On Hiatus' is now passed, too. It's easy now for users to change/remove if they want
|
||||
# with replace_metadata
|
||||
status = storyMetadata["status"].replace("Incomplete", "In-Progress").replace("Complete", "Completed")
|
||||
self.story.setMetadata("status", status)
|
||||
self.story.setMetadata("rating", storyMetadata["content_rating_text"])
|
||||
|
||||
@@ -133,19 +138,23 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
if storyMetadata["categories"][category]:
|
||||
self.story.addToList("genre", category)
|
||||
|
||||
self.story.addToList("category", "My Little Pony")
|
||||
|
||||
self.story.setMetadata("numWords", str(storyMetadata["words"]))
|
||||
|
||||
|
||||
# fimfic is the first site with an explicit cover image.
|
||||
if self.getConfig('include_images') and "image" in storyMetadata.keys():
|
||||
coverurl = storyMetadata["image"]
|
||||
if coverurl.startswith('//static.fimfiction.net'): # fix for img urls missing 'http:'
|
||||
if "image" in storyMetadata.keys():
|
||||
if "full_image" in storyMetadata:
|
||||
coverurl = storyMetadata["full_image"]
|
||||
else:
|
||||
coverurl = storyMetadata["image"]
|
||||
if coverurl.startswith('//'): # fix for img urls missing 'http:'
|
||||
coverurl = "http:"+coverurl
|
||||
self.story.addImgUrl(self,self.url,coverurl,self._fetchUrlRaw,cover=True)
|
||||
|
||||
self.setDescription(self.url, storyMetadata["description"])
|
||||
|
||||
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 />'))
|
||||
|
||||
# Dates are in Unix time
|
||||
# Take the publish date from the first chapter posted
|
||||
@@ -153,14 +162,33 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata("datePublished", datetime.fromtimestamp(rawDatePublished))
|
||||
rawDateUpdated = storyMetadata["date_modified"]
|
||||
self.story.setMetadata("dateUpdated", datetime.fromtimestamp(rawDateUpdated))
|
||||
|
||||
|
||||
soup = bs.BeautifulSoup(data).find("div", {"class":"story"})
|
||||
for character in [character_icon["title"] for character_icon in soup.findAll("a", {"class":"character_icon"})]:
|
||||
self.story.addToList("characters", character)
|
||||
# fimfic stopped putting the char name on or around the char
|
||||
# icon now for some reason. Pull it from the image name with
|
||||
# some heuristics.
|
||||
for character in [character_icon["src"] for character_icon in soup.findAll("img", {"class":"character_icon"})]:
|
||||
# //static.fimfiction.net/images/characters/twilight_sparkle.png
|
||||
# 5th split /, remove last four, replace _, capitolize every word(title())
|
||||
char = character.split('/')[5][:-4].replace('_',' ').title()
|
||||
if char == 'Oc':
|
||||
char = "OC"
|
||||
if char == 'Cmc':
|
||||
char = "Cutie Mark Crusaders"
|
||||
self.story.addToList("characters", char)
|
||||
|
||||
# extra site specific metadata
|
||||
extralist = ["likes","dislikes","views","total_views","short_description"]
|
||||
for metakey in extralist:
|
||||
if metakey in storyMetadata:
|
||||
value = storyMetadata[metakey]
|
||||
if not isinstance(value,basestring):
|
||||
value = unicode(value)
|
||||
self.story.setMetadata(metakey, value)
|
||||
|
||||
|
||||
def getChapterText(self, url):
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(url),selfClosingTags=('br','hr')).find('div', {'id' : 'chapter_container'})
|
||||
if soup == None:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -49,7 +50,7 @@ class FineStoriesComAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2].split(':')[0])
|
||||
if 'storyInfo' in self.story.getMetadata('storyId'):
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/s/storyInfo.php?id='+self.story.getMetadata('storyId'))
|
||||
@@ -95,13 +96,13 @@ class FineStoriesComAdapter(BaseSiteAdapter):
|
||||
params['submit'] = 'Login'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/login.php'
|
||||
logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['theusername']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "My Account" not in d : #Member Account
|
||||
logging.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['theusername']))
|
||||
raise exceptions.FailedToLogin(url,params['theusername'])
|
||||
return False
|
||||
@@ -114,7 +115,7 @@ class FineStoriesComAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -232,7 +233,7 @@ class FineStoriesComAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(url),
|
||||
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,8 +48,8 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
self.story.setMetadata('section',self.parsedUrl.path.split('/',)[1])
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
self.section=self.parsedUrl.path.split('/',)[1]
|
||||
|
||||
# normalized story URL.
|
||||
if "malfoymanor" in self.parsedUrl.netloc:
|
||||
@@ -60,11 +61,6 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','gech')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Harry Potter")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%d/%b/%Y"
|
||||
@@ -105,17 +101,17 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
params['cookiecheck'] = '1'
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
if "enchant" in self.story.getMetadata('section'):
|
||||
if "enchant" in self.section:
|
||||
loginUrl = 'http://grangerenchanted.com/enchant/user.php?action=login'
|
||||
else:
|
||||
loginUrl = 'http://malfoymanor.grangerenchanted.com/themanor/user.php?action=login'
|
||||
logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Member Account" not in d : #Member Account
|
||||
logging.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
@@ -137,7 +133,7 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -162,7 +158,7 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
# correct stupid & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL 2nd try: "+url)
|
||||
logger.debug("URL 2nd try: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -196,7 +192,7 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
# Find the chapters:
|
||||
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")):
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+self.story.getMetadata('section')+'/'+chapter['href']+addurl))
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+self.section+'/'+chapter['href']+addurl))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
@@ -267,7 +263,7 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
# Find Series name from series URL.
|
||||
a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+"))
|
||||
series_name = a.string
|
||||
series_url = 'http://'+self.host+'/'+self.story.getMetadata('section')+'/'+a['href']
|
||||
series_url = 'http://'+self.host+'/'+self.section+'/'+a['href']
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url))
|
||||
@@ -289,10 +285,10 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(url),
|
||||
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
|
||||
|
||||
div = soup.find('div', {'id' : 'story1'})
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib
|
||||
import urllib2
|
||||
@@ -37,12 +38,11 @@ class HarryPotterFanFictionComSiteAdapter(BaseSiteAdapter):
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
self.story.addToList("category","Harry Potter")
|
||||
self.is_adult=False
|
||||
|
||||
# get storyId from url--url validation guarantees query is only psid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?psid='+self.story.getMetadata('storyId'))
|
||||
@@ -73,7 +73,7 @@ class HarryPotterFanFictionComSiteAdapter(BaseSiteAdapter):
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
url = self.url+'&index=1'
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -182,7 +182,7 @@ class HarryPotterFanFictionComSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
## most adapters use BeautifulStoneSoup here, but non-Stone
|
||||
## allows nested div tags.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,7 +48,7 @@ class HLFictionNetAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -55,11 +56,6 @@ class HLFictionNetAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','hlf')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Highlander")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%m/%d/%y"
|
||||
@@ -81,7 +77,7 @@ class HLFictionNetAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -220,7 +216,7 @@ class HLFictionNetAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -51,7 +52,7 @@ class HPFandomNetAdapterAdapter(BaseSiteAdapter): # XXX
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
# XXX Most sites don't have the /eff part. Replace all to remove it usually.
|
||||
@@ -60,11 +61,6 @@ class HPFandomNetAdapterAdapter(BaseSiteAdapter): # XXX
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','hpfdm') # XXX
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Harry Potter") # XXX
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%Y.%m.%d" # XXX
|
||||
@@ -84,7 +80,7 @@ class HPFandomNetAdapterAdapter(BaseSiteAdapter): # XXX
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
url = self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -185,21 +181,22 @@ class HPFandomNetAdapterAdapter(BaseSiteAdapter): # XXX
|
||||
value = td.nextSibling.string
|
||||
#print("\nlabel:%s\nvalue:%s\n"%(label,value))
|
||||
|
||||
if 'Category' in label:
|
||||
if 'Category' in label and value:
|
||||
cats = td.parent.findAll('a',href=re.compile(r'categories.php'))
|
||||
catstext = [cat.string for cat in cats]
|
||||
for cat in catstext:
|
||||
self.story.addToList('category',cat.string)
|
||||
|
||||
if 'Characters' in label:
|
||||
if 'Characters' in label and value: # this site can have Character label with no
|
||||
# values, apparently. Others as a precaution.
|
||||
for char in value.split(','):
|
||||
self.story.addToList('characters',char.strip())
|
||||
|
||||
if 'Genre' in label:
|
||||
if 'Genre' in label and value:
|
||||
for genre in value.split(','):
|
||||
self.story.addToList('genre',genre.strip())
|
||||
|
||||
if 'Warnings' in label:
|
||||
if 'Warnings' in label and value:
|
||||
for warning in value.split(','):
|
||||
if warning.strip() != 'none':
|
||||
self.story.addToList('warnings',warning.strip())
|
||||
@@ -213,7 +210,7 @@ class HPFandomNetAdapterAdapter(BaseSiteAdapter): # XXX
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
data = self._fetchUrl(url)
|
||||
# There's no good wrapper around the chapter text. :-/
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,7 +48,7 @@ class HPFanficArchiveComAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/stories/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -55,11 +56,6 @@ class HPFanficArchiveComAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','hpffa')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Harry Potter")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%B %d, %Y"
|
||||
@@ -81,7 +77,7 @@ class HPFanficArchiveComAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -207,7 +203,7 @@ class HPFanficArchiveComAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(url),
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,7 +48,7 @@ class IkEternalNetAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -55,11 +56,6 @@ class IkEternalNetAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','ike')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","InuYasha")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%B %d, %Y"
|
||||
@@ -97,13 +93,13 @@ class IkEternalNetAdapter(BaseSiteAdapter):
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/user.php?action=login'
|
||||
logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Member Account" not in d : #Member Account
|
||||
logging.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
@@ -125,7 +121,7 @@ class IkEternalNetAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -163,7 +159,7 @@ class IkEternalNetAdapter(BaseSiteAdapter):
|
||||
# correct stupid & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL 2nd try: "+url)
|
||||
logger.debug("URL 2nd try: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -272,7 +268,7 @@ class IkEternalNetAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2012 Fanficdownloader team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import time
|
||||
import logging
|
||||
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 InDeathNetAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class InDeathNetAdapter(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 correct
|
||||
m = re.match(self.getSiteURLPattern(),url)
|
||||
if m:
|
||||
self.story.setMetadata('storyId',m.group('id'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
# normalized story URL.
|
||||
self._setURL('http://www.' + self.getSiteDomain() + '/blog/archive/'+self.story.getMetadata('storyId')+'-'+m.group('name')+'/')
|
||||
else:
|
||||
raise exceptions.InvalidStoryURL(url,
|
||||
self.getSiteDomain(),
|
||||
self.getSiteExampleURLs())
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','idn')
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%d %B %Y"
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'indeath.net'
|
||||
|
||||
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://"+self.getSiteDomain()+"/blog/archive/123-story-in-death/"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
# http://www.indeath.net/blog/archive/169-ransom-in-death/
|
||||
return re.escape("http://")+re.escape(self.getSiteDomain())+r"/blog/(archive/)?(?P<id>\d+)\-(?P<name>[a-z0-9\-]*)/?$"
|
||||
|
||||
|
||||
def getDateFromComponents(self, postmonth, postday):
|
||||
ym = re.search("Entries\ in\ (?P<mon>January|February|March|April|May|June|July|August|September|October|November|December)\ (?P<year>\d{4})",postmonth)
|
||||
d = re.search("(?P<day>\d{2})\ (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)",postday)
|
||||
postdate = makeDate(d.group('day')+' '+ym.group('mon')+' '+ym.group('year'),self.dateformat)
|
||||
return postdate
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
url = self.url
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.meta)
|
||||
else:
|
||||
raise e
|
||||
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = bs.BeautifulSoup(data)
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
## Title
|
||||
h = soup.find('a', id="blog_title")
|
||||
t = h.find('span')
|
||||
self.story.setMetadata('title',t.contents[0].string.strip())
|
||||
|
||||
s = t.find('div')
|
||||
if s != None:
|
||||
self.setDescription(url,s)
|
||||
|
||||
# Find authorid and URL from first link in Recent Entries (don't yet reference 'recent entries' - let's see if that is required)
|
||||
a = soup.find('a', href=re.compile(r"http://www.indeath.net/user/\d+\-[a-z0-9]+/$")) #http://www.indeath.net/user/9083-cyrex/
|
||||
m = re.search('http://www.indeath.net/user/(?P<id>\d+)\-(?P<name>[a-z0-9]*)/$',a['href'])
|
||||
self.story.setMetadata('authorId',m.group('id'))
|
||||
self.story.setMetadata('authorUrl',a['href'])
|
||||
self.story.setMetadata('author',m.group('name'))
|
||||
|
||||
|
||||
|
||||
|
||||
# Find the chapters:
|
||||
chapters=soup.findAll('a', title="View entry", href=re.compile(r'http://www.indeath.net/blog/'+self.story.getMetadata('storyId')+"/entry\-(\d+)\-([^/]*)/$"))
|
||||
|
||||
#reverse the list since newest at the top
|
||||
chapters.reverse()
|
||||
|
||||
# Get date published & updated from first & last entries
|
||||
posttable=soup.find('div', id="main_column")
|
||||
|
||||
postmonths=posttable.findAll('th', text=re.compile(r'Entries\ in\ '))
|
||||
postmonths.reverse()
|
||||
|
||||
postdates=posttable.findAll('span', _class="desc", text=re.compile('\d{2}\ (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)'))
|
||||
postdates.reverse()
|
||||
|
||||
self.story.setMetadata('datePublished',self.getDateFromComponents(postmonths[0],postdates[0]))
|
||||
self.story.setMetadata('dateUpdated',self.getDateFromComponents(postmonths[len(postmonths)-1],postdates[len(postdates)-1]))
|
||||
|
||||
# Process List of Chapters
|
||||
self.story.setMetadata('numChapters',len(chapters))
|
||||
logger.debug("numChapters: (%s)"%self.story.getMetadata('numChapters'))
|
||||
for x in range(0,len(chapters)):
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
chapter=chapters[x]
|
||||
if len(chapters)==1:
|
||||
self.chapterUrls.append((self.story.getMetadata('title'),chapter['href']))
|
||||
else:
|
||||
ct = stripHTML(chapter)
|
||||
tnew = re.match("(?i)"+self.story.getMetadata('title')+r" - (?P<newtitle>.*)$",ct)
|
||||
if tnew:
|
||||
chaptertitle = tnew.group('newtitle')
|
||||
else:
|
||||
chaptertitle = ct
|
||||
self.chapterUrls.append((chaptertitle,chapter['href']))
|
||||
|
||||
|
||||
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
#chapter=bs.BeautifulSoup('<div class="story"></div>')
|
||||
data = self._fetchUrl(url)
|
||||
soup = bs.BeautifulSoup(data,selfClosingTags=('br','hr','span','center'))
|
||||
|
||||
chapter = soup.find("div", "entry_content")
|
||||
|
||||
if None == chapter:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
return self.utf8FromSoup(url,chapter)
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2011 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 JLAUnlimitedComAdapter
|
||||
|
||||
|
||||
class JLAUnlimitedComAdapter(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 = "" # 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'))
|
||||
|
||||
self._setURL('http://' + self.getSiteDomain() + '/eFiction1.1/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','jla')
|
||||
|
||||
# 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.jlaunlimited.com'
|
||||
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://"+self.getSiteDomain()+"/eFiction1.1/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain()+"/eFiction1.1/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" # XXX
|
||||
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
|
||||
|
||||
# Assume that if there is a url with 'warning=#' in the page then it is a 'check' page
|
||||
m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&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 & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
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)
|
||||
|
||||
# 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+'/eFiction1.1/'+chapter['href']+addurl))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
# eFiction sites don't help us out a lot with their meta data
|
||||
# formating, so it's a little ugly.
|
||||
|
||||
# utility method
|
||||
def defaultGetattr(d,k):
|
||||
try:
|
||||
return d[k]
|
||||
except:
|
||||
return ""
|
||||
|
||||
# <span class="label">Rated:</span> NC-17<br /> etc
|
||||
labels = soup.findAll('span',{'class':'label'})
|
||||
for labelspan in labels:
|
||||
value = labelspan.nextSibling
|
||||
label = labelspan.string
|
||||
|
||||
if 'Summary' in label:
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
if 'Rated' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
if 'Categories' in label:
|
||||
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
catstext = [cat.string for cat in cats]
|
||||
for cat in catstext:
|
||||
self.story.addToList('category',cat.string)
|
||||
|
||||
if 'Characters' in label:
|
||||
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
|
||||
charstext = [char.string for char in chars]
|
||||
for char in charstext:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
## Not all sites use Genre, but there's no harm to
|
||||
## leaving it in. Check to make sure the type_id number
|
||||
## is correct, though--it's site specific.
|
||||
if 'Genre' in label:
|
||||
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) # XXX
|
||||
genrestext = [genre.string for genre in genres]
|
||||
self.genre = ', '.join(genrestext)
|
||||
for genre in genrestext:
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
## Not all sites use Warnings, but there's no harm to
|
||||
## leaving it in. Check to make sure the type_id number
|
||||
## is correct, though--it's site specific.
|
||||
if 'Warnings' in label:
|
||||
warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX
|
||||
warningstext = [warning.string for warning in warnings]
|
||||
self.warning = ', '.join(warningstext)
|
||||
for warning in warningstext:
|
||||
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:
|
||||
# there's a stray [ at the end.
|
||||
#value = value[0:-1]
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
try:
|
||||
# Find Series name from series URL.
|
||||
a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+"))
|
||||
series_name = a.string
|
||||
series_url = 'http://'+self.host+'/fanfic/'+a['href']
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url))
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$'))
|
||||
i=1
|
||||
for a in storyas:
|
||||
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.BeautifulSoup(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)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -51,7 +52,7 @@ class KSArchiveComAdapter(BaseSiteAdapter): # XXX
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
# XXX Most sites don't have the /fanfic part. Replace all to remove it usually.
|
||||
@@ -60,11 +61,6 @@ class KSArchiveComAdapter(BaseSiteAdapter): # XXX
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','ksa') # XXX
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Star Trek") # XXX
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%b/%d/%Y" # XXX
|
||||
@@ -104,7 +100,7 @@ class KSArchiveComAdapter(BaseSiteAdapter): # XXX
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -137,7 +133,7 @@ class KSArchiveComAdapter(BaseSiteAdapter): # XXX
|
||||
# correct stupid & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL 2nd try: "+url)
|
||||
logger.debug("URL 2nd try: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -167,7 +163,7 @@ class KSArchiveComAdapter(BaseSiteAdapter): # XXX
|
||||
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)
|
||||
self.story.setMetadata('author',stripHTML(a))
|
||||
|
||||
# Find the chapters:
|
||||
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")):
|
||||
@@ -190,7 +186,7 @@ class KSArchiveComAdapter(BaseSiteAdapter): # XXX
|
||||
labels = soup.findAll('span',{'class':'label'})
|
||||
for labelspan in labels:
|
||||
value = labelspan.nextSibling
|
||||
label = labelspan.string
|
||||
label = stripHTML(labelspan)
|
||||
|
||||
if 'Summary' in label:
|
||||
## Everything until the next span class='label'
|
||||
@@ -215,50 +211,50 @@ class KSArchiveComAdapter(BaseSiteAdapter): # XXX
|
||||
|
||||
if 'Categories' in label:
|
||||
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
catstext = [cat.string for cat in cats]
|
||||
catstext = [stripHTML(cat) for cat in cats]
|
||||
for cat in catstext:
|
||||
# ran across one story with an empty <a href="browse.php?type=categories&catid=1"></a>
|
||||
# tag in the desc once.
|
||||
if cat and cat.strip() in ('Poetry','Essays'):
|
||||
self.story.addToList('category',cat.string)
|
||||
self.story.addToList('category',stripHTML(cat))
|
||||
|
||||
if 'Characters' in label:
|
||||
self.story.addToList('characters','Kirk')
|
||||
self.story.addToList('characters','Spock')
|
||||
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
|
||||
charstext = [char.string for char in chars]
|
||||
charstext = [stripHTML(char) for char in chars]
|
||||
for char in charstext:
|
||||
self.story.addToList('characters',char.string)
|
||||
self.story.addToList('characters',stripHTML(char))
|
||||
|
||||
## Not all sites use Genre, but there's no harm to
|
||||
## leaving it in. Check to make sure the type_id number
|
||||
## is correct, though--it's site specific.
|
||||
if 'Genre' in label:
|
||||
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) # XXX
|
||||
genrestext = [genre.string for genre in genres]
|
||||
genrestext = [stripHTML(genre) for genre in genres]
|
||||
self.genre = ', '.join(genrestext)
|
||||
for genre in genrestext:
|
||||
self.story.addToList('genre',genre.string)
|
||||
self.story.addToList('genre',stripHTML(genre))
|
||||
|
||||
## In addition to Genre (which is very site specific) KSA
|
||||
## has 'Story Type', which is much more what most sites
|
||||
## call genre.
|
||||
if 'Story Type' in label:
|
||||
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=5')) # XXX
|
||||
genrestext = [genre.string for genre in genres]
|
||||
genrestext = [stripHTML(genre) for genre in genres]
|
||||
self.genre = ', '.join(genrestext)
|
||||
for genre in genrestext:
|
||||
self.story.addToList('genre',genre.string)
|
||||
self.story.addToList('genre',stripHTML(genre))
|
||||
|
||||
## Not all sites use Warnings, but there's no harm to
|
||||
## leaving it in. Check to make sure the type_id number
|
||||
## is correct, though--it's site specific.
|
||||
if 'Warnings' in label:
|
||||
warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX
|
||||
warningstext = [warning.string for warning in warnings]
|
||||
warningstext = [stripHTML(warning) for warning in warnings]
|
||||
self.warning = ', '.join(warningstext)
|
||||
for warning in warningstext:
|
||||
self.story.addToList('warnings',warning.string)
|
||||
self.story.addToList('warnings',stripHTML(warning))
|
||||
|
||||
if 'Completed' in label:
|
||||
if 'Yes' in value:
|
||||
@@ -277,7 +273,7 @@ class KSArchiveComAdapter(BaseSiteAdapter): # XXX
|
||||
try:
|
||||
# Find Series name from series URL.
|
||||
a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+"))
|
||||
series_name = a.string
|
||||
series_name = stripHTML(a)
|
||||
series_url = 'http://'+self.host+'/'+a['href']
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
@@ -299,7 +295,7 @@ class KSArchiveComAdapter(BaseSiteAdapter): # XXX
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
data = self._fetchUrl(url)
|
||||
soup = bs.BeautifulStoneSoup(data,
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -48,7 +49,7 @@ class LibraryOfMoriaComAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/a/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -56,11 +57,6 @@ class LibraryOfMoriaComAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','lom')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Lord of the Rings")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%B %d, %Y"
|
||||
@@ -89,7 +85,7 @@ class LibraryOfMoriaComAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -109,7 +105,7 @@ class LibraryOfMoriaComAdapter(BaseSiteAdapter):
|
||||
# correct stupid & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL 2nd try: "+url)
|
||||
logger.debug("URL 2nd try: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -239,7 +235,7 @@ class LibraryOfMoriaComAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,7 +48,7 @@ class LumosSycophantHexComAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
@@ -56,11 +57,6 @@ class LumosSycophantHexComAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','lsph')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Harry Potter")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%m/%d/%Y"
|
||||
@@ -91,7 +87,7 @@ class LumosSycophantHexComAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -226,7 +222,7 @@ class LumosSycophantHexComAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib
|
||||
import urllib2
|
||||
@@ -42,7 +43,7 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
|
||||
m = re.match(self.getSiteURLPattern(),url)
|
||||
if m:
|
||||
self.story.setMetadata('storyId',m.group('id'))
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/fanfic/view_st.php/'+self.story.getMetadata('storyId'))
|
||||
else:
|
||||
@@ -66,7 +67,7 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
url = self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -188,7 +189,7 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
data=self._fetchUrl(url)
|
||||
soup = bs.BeautifulStoneSoup(data,
|
||||
@@ -214,7 +215,7 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
|
||||
return self.utf8FromSoup(url,anchor)
|
||||
|
||||
else:
|
||||
logging.debug('Using kludgey text find for older mediaminer story.')
|
||||
logger.debug('Using kludgey text find for older mediaminer story.')
|
||||
## Some older mediaminer stories are unparsable with BeautifulSoup.
|
||||
## Really nasty formatting. Sooo... Cheat! Parse it ourselves a bit first.
|
||||
## Story stuff falls between:
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2012 Fanficdownloader team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import time
|
||||
import logging
|
||||
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 MerlinFicDtwinsCoUk
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class MerlinFicDtwinsCoUk(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','mrfd')
|
||||
|
||||
# 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 'merlinfic.dtwins.co.uk'
|
||||
|
||||
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+$"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
if 'Registered Users Only' in data \
|
||||
or 'There is no such account on our website' in data \
|
||||
or "That password doesn't match the one in our database" in data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def performLogin(self, url):
|
||||
params = {}
|
||||
|
||||
if self.password:
|
||||
params['penname'] = self.username
|
||||
params['password'] = self.password
|
||||
else:
|
||||
params['penname'] = self.getConfig("username")
|
||||
params['password'] = self.getConfig("password")
|
||||
params['cookiecheck'] = '1'
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/user.php?action=login'
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Member Account" not in d : #Member Account
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
# Weirdly, different sites use different warning numbers.
|
||||
# If the title search below fails, there's a good chance
|
||||
# you need a different number. print data at that point
|
||||
# and see what the 'click here to continue' url says.
|
||||
addurl = "&ageconsent=ok&warning=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
|
||||
|
||||
if self.needToLoginCheck(data):
|
||||
# need to log in for this one.
|
||||
self.performLogin(url)
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&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 & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
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.string
|
||||
|
||||
if 'Summary' in label:
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
if 'Rated' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
if 'Categories' in label:
|
||||
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
for cat in cats:
|
||||
self.story.addToList('category',cat.string)
|
||||
|
||||
if 'Characters' in label:
|
||||
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
|
||||
for char in chars:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
if 'Pairing' in label:
|
||||
ships = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1'))
|
||||
for ship in ships:
|
||||
self.story.addToList('ships',ship.string)
|
||||
|
||||
if 'Genre' in label:
|
||||
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=3'))
|
||||
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)
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -51,7 +52,7 @@ class MidnightwhispersCaAdapter(BaseSiteAdapter): # XXX
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
# XXX Most sites don't have the /fanfic part. Replace all to remove it usually.
|
||||
@@ -60,11 +61,6 @@ class MidnightwhispersCaAdapter(BaseSiteAdapter): # XXX
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','mw') # XXX
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Queer as Folk") # XXX
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%B %d, %Y" # XXX
|
||||
@@ -100,7 +96,7 @@ class MidnightwhispersCaAdapter(BaseSiteAdapter): # XXX
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -133,7 +129,7 @@ class MidnightwhispersCaAdapter(BaseSiteAdapter): # XXX
|
||||
# correct stupid & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL 2nd try: "+url)
|
||||
logger.debug("URL 2nd try: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -274,7 +270,7 @@ class MidnightwhispersCaAdapter(BaseSiteAdapter): # XXX
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
data = self._fetchUrl(url)
|
||||
soup = bs.BeautifulStoneSoup(data,
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -51,7 +52,7 @@ class MuggleNetComAdapter(BaseSiteAdapter): # XXX
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -59,11 +60,6 @@ class MuggleNetComAdapter(BaseSiteAdapter): # XXX
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','mgln') # XXX
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Harry Potter") # XXX
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%m/%d/%y" # XXX
|
||||
@@ -101,13 +97,13 @@ class MuggleNetComAdapter(BaseSiteAdapter): # XXX
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/user.php?action=login&sid='+self.story.getMetadata('storyId')
|
||||
logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Member Account" not in d : #Member Account
|
||||
logging.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
@@ -130,7 +126,7 @@ class MuggleNetComAdapter(BaseSiteAdapter): # XXX
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -169,7 +165,7 @@ class MuggleNetComAdapter(BaseSiteAdapter): # XXX
|
||||
# correct stupid & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL 2nd try: "+url)
|
||||
logger.debug("URL 2nd try: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -320,7 +316,7 @@ class MuggleNetComAdapter(BaseSiteAdapter): # XXX
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,7 +48,7 @@ class NationalLibraryNetAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only storyid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?storyid='+self.story.getMetadata('storyId'))
|
||||
@@ -55,11 +56,6 @@ class NationalLibraryNetAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','ntlb')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","West Wing")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%m-%d-%y"
|
||||
@@ -85,7 +81,7 @@ class NationalLibraryNetAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -154,6 +150,10 @@ class NationalLibraryNetAdapter(BaseSiteAdapter):
|
||||
for char in value.string.split(', '):
|
||||
self.story.addToList('characters',char)
|
||||
|
||||
if 'Pairing' in label:
|
||||
for char in value.string.split(', '):
|
||||
self.story.addToList('ships',char)
|
||||
|
||||
if 'Warnings' in label:
|
||||
for warning in value.string.split(', '):
|
||||
self.story.addToList('warnings',warning)
|
||||
@@ -178,7 +178,7 @@ class NationalLibraryNetAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(url),
|
||||
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,7 +48,7 @@ class NCISFicComAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only storyid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?storyid='+self.story.getMetadata('storyId'))
|
||||
@@ -55,11 +56,6 @@ class NCISFicComAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','ncisf')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","NCIS")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%m-%d-%y"
|
||||
@@ -85,7 +81,7 @@ class NCISFicComAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -154,6 +150,10 @@ class NCISFicComAdapter(BaseSiteAdapter):
|
||||
for char in value.string.split(', '):
|
||||
self.story.addToList('characters',char)
|
||||
|
||||
if 'Pairing' in label:
|
||||
for char in value.string.split(', '):
|
||||
self.story.addToList('ships',char)
|
||||
|
||||
if 'Warnings' in label:
|
||||
for warning in value.string.split(', '):
|
||||
self.story.addToList('warnings',warning)
|
||||
@@ -185,7 +185,7 @@ class NCISFicComAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(url),
|
||||
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
|
||||
|
||||
+26
-12
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -27,15 +28,20 @@ from .. import exceptions as exceptions
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
def getClass():
|
||||
return NCISFictionComAdapter
|
||||
return NCISFictionNetAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class NCISFictionComAdapter(BaseSiteAdapter):
|
||||
class NCISFictionNetAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
# ncisfiction.net blocks IPs the default user-agent. However,
|
||||
# when asked, they said it was just general anti-spam, not
|
||||
# targeted at us. That lets me do this in good conscience:
|
||||
self.opener.addheaders = [('User-agent', 'FFDL/1.6')]
|
||||
|
||||
self.decode = ["iso-8859-1",
|
||||
"Windows-1252"] # 1252 is a superset of iso-8859-1.
|
||||
# Most sites that claim to be
|
||||
@@ -47,7 +53,7 @@ class NCISFictionComAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL("http://"+self.getSiteDomain()\
|
||||
@@ -56,11 +62,6 @@ class NCISFictionComAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','ncisfn')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","NCIS")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%d/%m/%Y"
|
||||
@@ -68,13 +69,21 @@ class NCISFictionComAdapter(BaseSiteAdapter):
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'www.ncisfiction.com'
|
||||
return 'www.ncisfiction.net'
|
||||
|
||||
## Changed from www.ncisfiction.com to www.ncisfiction.net Oct
|
||||
## 2012 due to the ncisfiction.com domain expiring. Still accept
|
||||
## .com domains for existing updates, etc.
|
||||
|
||||
@classmethod
|
||||
def getAcceptDomains(cls):
|
||||
return ['www.ncisfiction.net','www.ncisfiction.com']
|
||||
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://"+self.getSiteDomain()+"/story.php?stid=01234 http://"+self.getSiteDomain()+"/chapters.php?stid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return "http://"+self.getSiteDomain()+r'/(chapters|story)?.php\?stid=\d+'
|
||||
return r'http://www\.ncisfiction\.(net|com)/(chapters|story)?.php\?stid=\d+'
|
||||
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
@@ -83,7 +92,7 @@ class NCISFictionComAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -157,6 +166,11 @@ class NCISFictionComAdapter(BaseSiteAdapter):
|
||||
for char in chars:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
if 'Pairing' in label:
|
||||
ships = value.findAll('a')
|
||||
for ship in ships:
|
||||
self.story.addToList('ships',ship.string)
|
||||
|
||||
if 'Genre' in label:
|
||||
genres = value.findAll('a')
|
||||
for genre in genres:
|
||||
@@ -187,7 +201,7 @@ class NCISFictionComAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -51,7 +52,7 @@ class NfaCommunityComAdapter(BaseSiteAdapter): # XXX
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
# XXX Most sites don't have the /fanfic part. Replace all to remove it usually.
|
||||
@@ -60,11 +61,6 @@ class NfaCommunityComAdapter(BaseSiteAdapter): # XXX
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','nfa') # XXX
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","NCIS") # XXX
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%m/%d/%Y" # XXX
|
||||
@@ -104,7 +100,7 @@ class NfaCommunityComAdapter(BaseSiteAdapter): # XXX
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -137,7 +133,7 @@ class NfaCommunityComAdapter(BaseSiteAdapter): # XXX
|
||||
# correct stupid & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL 2nd try: "+url)
|
||||
logger.debug("URL 2nd try: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -278,7 +274,7 @@ class NfaCommunityComAdapter(BaseSiteAdapter): # XXX
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,7 +48,7 @@ class NHAMagicalWorldsUsAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
@@ -56,11 +57,6 @@ class NHAMagicalWorldsUsAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','nha')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Buffy the Vampire Slayer")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = " %m/%d/%y"
|
||||
@@ -79,7 +75,7 @@ class NHAMagicalWorldsUsAdapter(BaseSiteAdapter):
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
url = self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -122,7 +118,7 @@ class NHAMagicalWorldsUsAdapter(BaseSiteAdapter):
|
||||
|
||||
|
||||
# Find the chapters:
|
||||
chapters=soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+'&chapter=\d$'))
|
||||
chapters=soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+'&chapter=\d+$'))
|
||||
if len(chapters) == 0:
|
||||
self.chapterUrls.append((self.story.getMetadata('title'),url))
|
||||
else:
|
||||
@@ -202,7 +198,7 @@ class NHAMagicalWorldsUsAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,7 +48,7 @@ class OcclumencySycophantHexComAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
@@ -56,11 +57,6 @@ class OcclumencySycophantHexComAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','osph')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Harry Potter")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%m/%d/%Y"
|
||||
@@ -99,13 +95,13 @@ class OcclumencySycophantHexComAdapter(BaseSiteAdapter):
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/user.php'
|
||||
logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Logout" not in d : #Member Account
|
||||
logging.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
@@ -117,7 +113,7 @@ class OcclumencySycophantHexComAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -250,7 +246,7 @@ class OcclumencySycophantHexComAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
data = self._fetchUrl(url)
|
||||
data = data.replace('<div align="left"', '<div align="left">')
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,7 +48,7 @@ class OneDirectionFanfictionComAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -55,11 +56,6 @@ class OneDirectionFanfictionComAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','odf')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","One Direction")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%m/%d/%y"
|
||||
@@ -102,13 +98,13 @@ class OneDirectionFanfictionComAdapter(BaseSiteAdapter):
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/user.php?action=login'
|
||||
logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Member Account" not in d : #Member Account
|
||||
logging.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
@@ -130,7 +126,7 @@ class OneDirectionFanfictionComAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -258,7 +254,7 @@ class OneDirectionFanfictionComAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2, urllib, cookielib
|
||||
|
||||
@@ -47,7 +48,7 @@ class PhoenixSongNetAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[3])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/fanfiction/story/' +self.story.getMetadata('storyId')+'/')
|
||||
@@ -55,11 +56,6 @@ class PhoenixSongNetAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','phs')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Harry Potter")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%B %d %Y"
|
||||
@@ -95,12 +91,12 @@ class PhoenixSongNetAdapter(BaseSiteAdapter):
|
||||
params['login'] = 'Login'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/users/processlogin.php'
|
||||
logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['txtusername']))
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if 'Please login to continue.' in d : #Member Account
|
||||
logging.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['txtusername']))
|
||||
raise exceptions.FailedToLogin(url,params['txtusername'])
|
||||
return False
|
||||
@@ -113,7 +109,7 @@ class PhoenixSongNetAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -213,7 +209,7 @@ class PhoenixSongNetAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(url),
|
||||
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,8 +48,8 @@ class PonyFictionArchiveNetAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logging.info(self.parsedUrl.netloc)
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.info(self.parsedUrl.netloc)
|
||||
# normalized story URL.
|
||||
if "explicit" in self.parsedUrl.netloc:
|
||||
self._setURL('http://explicit.' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -60,11 +61,6 @@ class PonyFictionArchiveNetAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','pffa')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","My Little Pony")
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
@@ -96,7 +92,7 @@ class PonyFictionArchiveNetAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -117,7 +113,7 @@ class PonyFictionArchiveNetAdapter(BaseSiteAdapter):
|
||||
# correct stupid & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL 2nd try: "+url)
|
||||
logger.debug("URL 2nd try: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -239,10 +235,9 @@ class PonyFictionArchiveNetAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(url)) # otherwise soup eats the br/hr tags.
|
||||
|
||||
div = soup.find('div', {'id' : 'story'})
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
import cookielib as cl
|
||||
@@ -54,7 +55,7 @@ class PortkeyOrgAdapter(BaseSiteAdapter): # XXX
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/story/'+self.story.getMetadata('storyId'))
|
||||
@@ -62,11 +63,6 @@ class PortkeyOrgAdapter(BaseSiteAdapter): # XXX
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','prtky') # XXX
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Harry Potter") # XXX
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%d/%m/%y" # XXX
|
||||
@@ -86,7 +82,7 @@ class PortkeyOrgAdapter(BaseSiteAdapter): # XXX
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
url = self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
# portkey screws around with using a different URL to set the
|
||||
# cookie and it's a pain. So... cheat!
|
||||
@@ -252,7 +248,7 @@ class PortkeyOrgAdapter(BaseSiteAdapter): # XXX
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
data = self._fetchUrl(url)
|
||||
soup = bs.BeautifulStoneSoup(data,
|
||||
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib
|
||||
import urllib2
|
||||
@@ -37,11 +38,10 @@ class PotionsAndSnitchesNetSiteAdapter(BaseSiteAdapter):
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
self.story.addToList("category","Harry Potter")
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/fanfiction/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -64,7 +64,7 @@ class PotionsAndSnitchesNetSiteAdapter(BaseSiteAdapter):
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
url = self.url+'&index=1'
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -142,7 +142,7 @@ class PotionsAndSnitchesNetSiteAdapter(BaseSiteAdapter):
|
||||
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
|
||||
charstext = [char.string for char in chars]
|
||||
for char in charstext:
|
||||
if char == "!Snape and Harry (required)":
|
||||
if "Snape and Harry (required)" in char:
|
||||
self.story.addToList('characters',"Snape")
|
||||
self.story.addToList('characters',"Harry")
|
||||
else:
|
||||
@@ -162,12 +162,12 @@ class PotionsAndSnitchesNetSiteAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
if 'Published' in label:
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(value), "%b %d %Y"))
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(value), "%d %b %Y"))
|
||||
|
||||
if 'Updated' in label:
|
||||
# there's a stray [ at the end.
|
||||
#value = value[0:-1]
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), "%b %d %Y"))
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), "%d %b %Y"))
|
||||
|
||||
try:
|
||||
# Find Series name from series URL.
|
||||
@@ -192,7 +192,7 @@ class PotionsAndSnitchesNetSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,7 +48,7 @@ class PretenderCenterComAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/missingpieces/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -55,11 +56,6 @@ class PretenderCenterComAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','ptdc')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","The Pretender")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%d/%m/%y"
|
||||
@@ -94,7 +90,7 @@ class PretenderCenterComAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -114,7 +110,7 @@ class PretenderCenterComAdapter(BaseSiteAdapter):
|
||||
# correct stupid & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL 2nd try: "+url)
|
||||
logger.debug("URL 2nd try: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -242,7 +238,7 @@ class PretenderCenterComAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,7 +48,7 @@ class PrisonBreakFicNetAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -55,11 +56,6 @@ class PrisonBreakFicNetAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','pbf')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Prison Break")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%B %d, %Y"
|
||||
@@ -81,7 +77,7 @@ class PrisonBreakFicNetAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -157,6 +153,11 @@ class PrisonBreakFicNetAdapter(BaseSiteAdapter):
|
||||
for char in chars:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
if 'Pairing' in label:
|
||||
ships = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=3')) # XXX
|
||||
for ship in ships:
|
||||
self.story.addToList('ships',ship.string.replace(" and ","/"))
|
||||
|
||||
if 'Genre' in label:
|
||||
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) # XXX
|
||||
for genre in genres:
|
||||
@@ -202,7 +203,7 @@ class PrisonBreakFicNetAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2012 Fanficdownloader team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import time
|
||||
import logging
|
||||
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 QafFicComAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class QafFicComAdapter(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() + '/atp/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','atp')
|
||||
|
||||
# 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.qaf-fic.com'
|
||||
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://"+self.getSiteDomain()+"/atp/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain()+"/atp/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 = "&warning=NC-17"
|
||||
else:
|
||||
addurl=""
|
||||
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+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+((?:&ageconsent=ok)?&warning=\s+)'",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 & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
url = self.url+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 and author
|
||||
a = soup.find('div', {'id' : 'pagetitle'})
|
||||
|
||||
aut = a.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
|
||||
self.story.setMetadata('authorId',aut['href'].split('=')[1])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/atp/'+aut['href'])
|
||||
self.story.setMetadata('author',aut.string)
|
||||
aut.extract()
|
||||
|
||||
self.story.setMetadata('title',a.string[:(len(a.string)-3)])
|
||||
|
||||
# Find the chapters:
|
||||
chapters=soup.find('select')
|
||||
if chapters != None:
|
||||
for chapter in chapters.findAll('option'):
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/atp/viewstory.php?sid='+self.story.getMetadata('storyId')+'&chapter='+chapter['value']))
|
||||
else:
|
||||
self.chapterUrls.append((self.story.getMetadata('title'),url))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')))
|
||||
for list in asoup.findAll('div', {'class' : re.compile('listbox\s+')}):
|
||||
a = list.find('a')
|
||||
if ('viewstory.php?sid='+self.story.getMetadata('storyId')) in a['href']:
|
||||
break
|
||||
|
||||
# eFiction sites don't help us out a lot with their meta data
|
||||
# formating, so it's a little ugly.
|
||||
|
||||
# utility method
|
||||
def defaultGetattr(d,k):
|
||||
try:
|
||||
return d[k]
|
||||
except:
|
||||
return ""
|
||||
|
||||
# <span class="label">Rated:</span> NC-17<br /> etc
|
||||
labels = list.findAll('span', {'class' : 'classification'})
|
||||
for labelspan in labels:
|
||||
label = labelspan.string
|
||||
value = labelspan.nextSibling
|
||||
|
||||
if 'Summary' in label:
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'classification' and value != None:
|
||||
if "Featured Stories" not in value:
|
||||
svalue += str(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
if 'Rated' in label:
|
||||
self.story.setMetadata('rating', value[:len(value)-2])
|
||||
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
if 'Categories' in label:
|
||||
cats = labelspan.parent.findAll('a',href=re.compile(r'categories.php\?catid=\d+'))
|
||||
for cat in cats:
|
||||
self.story.addToList('category',cat.string)
|
||||
|
||||
if 'Characters' in label:
|
||||
for char in value.string.split(', '):
|
||||
if not 'None' in char:
|
||||
self.story.addToList('characters',char)
|
||||
|
||||
if 'Genre' in label:
|
||||
for genre in value.string.split(', '):
|
||||
if not 'None' in genre:
|
||||
self.story.addToList('genre',genre)
|
||||
|
||||
if 'Warnings' in label:
|
||||
for warning in value.string.split(', '):
|
||||
if not 'None' in warning:
|
||||
self.story.addToList('warnings',warning)
|
||||
|
||||
if 'Completed' in label:
|
||||
if 'Yes' in value:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
if 'Published' in label:
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(value.split(' ::')[0]), self.dateformat))
|
||||
|
||||
if 'Updated' in label:
|
||||
# there's a stray [ at the end.
|
||||
#value = value[0:-1]
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
try:
|
||||
if list.find('a', href=re.compile(r"series.php")) != None:
|
||||
for series in asoup.findAll('a', href=re.compile(r"series.php\?seriesid=\d+")):
|
||||
# Find Series name from series URL.
|
||||
series_url = 'http://'+self.host+'/atp/'+series['href']
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url))
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$'))
|
||||
i=1
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
name=seriessoup.find('div', {'id' : 'pagetitle'})
|
||||
name.find('a').extract()
|
||||
self.setSeries(name.text.split(' by[')[0], i)
|
||||
i=0
|
||||
break
|
||||
i+=1
|
||||
if i == 0:
|
||||
break
|
||||
|
||||
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.BeautifulSoup(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)
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,7 +48,7 @@ class SamDeanArchiveNuAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -55,11 +56,6 @@ class SamDeanArchiveNuAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','sda')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Supernatural")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%m/%d/%y"
|
||||
@@ -85,7 +81,7 @@ class SamDeanArchiveNuAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -221,7 +217,7 @@ class SamDeanArchiveNuAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2012 Fanficdownloader team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import time
|
||||
import logging
|
||||
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 ScarvesAndCoffeeNetAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class ScarvesAndCoffeeNetAdapter(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','scacf')
|
||||
|
||||
# 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.scarvesandcoffee.net'
|
||||
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
if 'Registered Users Only' in data \
|
||||
or 'There is no such account on our website' in data \
|
||||
or "That password doesn't match the one in our database" in data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
## 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=20"
|
||||
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+((?:&ageconsent=ok)?&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 & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
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.string
|
||||
|
||||
if 'Summary' in label:
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
if 'Rated' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
if 'Categories' in label:
|
||||
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
for cat in cats:
|
||||
self.story.addToList('genre',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 '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)
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,19 +48,19 @@ class SG1HeliopolisComAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
self.story.setMetadata('section',self.parsedUrl.path.split('/',)[1])
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
self.section=self.parsedUrl.path.split('/',)[1]
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/'+self.story.getMetadata('section')+'/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
self._setURL('http://' + self.getSiteDomain() + '/'+self.section+'/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','sghp')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
if 'atlantis' in self.story.getMetadata('section'):
|
||||
# do. Can't use extracategories, could be Atlantis or SG-1
|
||||
if 'atlantis' in self.section:
|
||||
self.story.addToList("category","Stargate: Atlantis")
|
||||
else:
|
||||
self.story.addToList("category","Stargate: SG-1")
|
||||
@@ -95,7 +96,7 @@ class SG1HeliopolisComAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -115,7 +116,7 @@ class SG1HeliopolisComAdapter(BaseSiteAdapter):
|
||||
# correct stupid & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL 2nd try: "+url)
|
||||
logger.debug("URL 2nd try: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -149,7 +150,7 @@ class SG1HeliopolisComAdapter(BaseSiteAdapter):
|
||||
# Find the chapters:
|
||||
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")):
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+self.story.getMetadata('section')+'/'+chapter['href']+addurl))
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+self.section+'/'+chapter['href']+addurl))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
@@ -220,7 +221,7 @@ class SG1HeliopolisComAdapter(BaseSiteAdapter):
|
||||
# Find Series name from series URL.
|
||||
a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+"))
|
||||
series_name = a.string
|
||||
series_url = 'http://'+self.host+'/'+self.story.getMetadata('section')+'/'+a['href']
|
||||
series_url = 'http://'+self.host+'/'+self.section+'/'+a['href']
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url))
|
||||
@@ -242,7 +243,7 @@ class SG1HeliopolisComAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2012 Fanficdownloader team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import time
|
||||
import logging
|
||||
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 SinfulDesireOrgAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class SinfulDesireOrgAdapter(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() + '/archive/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','snds')
|
||||
|
||||
# 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.sinful-desire.org'
|
||||
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://"+self.getSiteDomain()+"/archive/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain()+"/archive/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 = "&warning=5"
|
||||
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+((?:&ageconsent=ok)?&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 & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
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+'/archive/'+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+'/archive/'+chapter['href']+addurl))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
# eFiction sites don't help us out a lot with their meta data
|
||||
# formating, so it's a little ugly.
|
||||
|
||||
# utility method
|
||||
def defaultGetattr(d,k):
|
||||
try:
|
||||
return d[k]
|
||||
except:
|
||||
return ""
|
||||
|
||||
# <span class="label">Rated:</span> NC-17<br /> etc
|
||||
labels = soup.findAll('span',{'class':'label'})
|
||||
for labelspan in labels:
|
||||
value = labelspan.nextSibling
|
||||
label = labelspan.string
|
||||
|
||||
if 'Summary' in label:
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
if 'Rated' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
if 'Categories' in label:
|
||||
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
for cat in cats:
|
||||
self.story.addToList('category',cat.string)
|
||||
|
||||
if 'Characters' in label:
|
||||
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
|
||||
for char in chars:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
if 'Genre' in label:
|
||||
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1'))
|
||||
for genre in genres:
|
||||
self.story.addToList('genre',genre.string)
|
||||
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=3'))
|
||||
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+'/archive/'+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)
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -48,7 +49,7 @@ class SiyeCoUkAdapter(BaseSiteAdapter): # XXX
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/siye/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -56,11 +57,6 @@ class SiyeCoUkAdapter(BaseSiteAdapter): # XXX
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','siye') # XXX
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Harry Potter") # XXX
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%Y.%m.%d" # XXX
|
||||
@@ -87,7 +83,7 @@ class SiyeCoUkAdapter(BaseSiteAdapter): # XXX
|
||||
# sites skip that for one-chapter stories.
|
||||
# Except it doesn't this time. :-/
|
||||
url = self.url #+'&index=1'+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -229,7 +225,7 @@ class SiyeCoUkAdapter(BaseSiteAdapter): # XXX
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
# soup = bs.BeautifulSoup(self._fetchUrl(url))
|
||||
# BeautifulSoup objects to <p> inside <span>, which
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -58,7 +59,7 @@ class SquidgeOrgPejaAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/peja/cgi-bin/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -90,7 +91,7 @@ class SquidgeOrgPejaAdapter(BaseSiteAdapter):
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
url = self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -156,6 +157,8 @@ class SquidgeOrgPejaAdapter(BaseSiteAdapter):
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
if 'Rated' in label:
|
||||
if value.endswith("["):
|
||||
value = value[:-1]
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
if 'Characters' in label:
|
||||
@@ -221,7 +224,7 @@ class SquidgeOrgPejaAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,7 +48,7 @@ class StargateAtlantisOrgAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/fanfics/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -55,11 +56,6 @@ class StargateAtlantisOrgAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','stat')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Stargate: Atlantis")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%B %d %Y"
|
||||
@@ -81,7 +77,7 @@ class StargateAtlantisOrgAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -218,7 +214,7 @@ class StargateAtlantisOrgAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(url),
|
||||
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,7 +48,7 @@ class StoriesOfArdaComAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/chapterlistview.asp?SID='+self.story.getMetadata('storyId'))
|
||||
@@ -55,11 +56,6 @@ class StoriesOfArdaComAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','soa')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Lord of the Rings")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%m/%d/%Y"
|
||||
@@ -81,7 +77,7 @@ class StoriesOfArdaComAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -138,7 +134,7 @@ class StoriesOfArdaComAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,7 +48,7 @@ class SVUFictionComAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -92,13 +93,13 @@ class SVUFictionComAdapter(BaseSiteAdapter):
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/user.php?action=login'
|
||||
logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Member Account" not in d : #Member Account
|
||||
logging.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
@@ -120,7 +121,7 @@ class SVUFictionComAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -145,7 +146,7 @@ class SVUFictionComAdapter(BaseSiteAdapter):
|
||||
# correct stupid & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL 2nd try: "+url)
|
||||
logger.debug("URL 2nd try: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -258,7 +259,7 @@ class SVUFictionComAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib
|
||||
import urllib2
|
||||
@@ -43,7 +44,7 @@ class TenhawkPresentsComSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -81,13 +82,13 @@ class TenhawkPresentsComSiteAdapter(BaseSiteAdapter):
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/user.php?action=login'
|
||||
logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Member Account" not in d : #Member Account
|
||||
logging.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
@@ -102,7 +103,7 @@ class TenhawkPresentsComSiteAdapter(BaseSiteAdapter):
|
||||
addurl=""
|
||||
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -116,7 +117,7 @@ class TenhawkPresentsComSiteAdapter(BaseSiteAdapter):
|
||||
# need to log in for this one.
|
||||
addurl = "&ageconsent=ok&warning=4"
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("Changing URL: "+url)
|
||||
logger.debug("Changing URL: "+url)
|
||||
self.performLogin(url)
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
@@ -229,7 +230,7 @@ class TenhawkPresentsComSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import datetime
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from .. import BeautifulSoup as bs
|
||||
from .. import exceptions
|
||||
@@ -48,7 +49,7 @@ class TestSiteAdapter(BaseSiteAdapter):
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
if self.story.getMetadata('storyId') == '665' and not (self.is_adult or self.getConfig("is_adult")):
|
||||
logging.warn("self.is_adult:%s"%self.is_adult)
|
||||
logger.warn("self.is_adult:%s"%self.is_adult)
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
if self.story.getMetadata('storyId') == '666':
|
||||
@@ -73,8 +74,8 @@ class TestSiteAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata(u'title',"Test Story Title "+self.story.getMetadata('storyId'))
|
||||
self.story.setMetadata('author','Test Author aa')
|
||||
self.story.setMetadata('storyUrl',self.url)
|
||||
self.story.setMetadata('description',u'Description '+self.crazystring+u''' Done
|
||||
|
||||
self.setDescription(self.url,u'Description '+self.crazystring+u''' Done
|
||||
<p>
|
||||
Some more longer description. "I suck at summaries!" "Better than it sounds!" "My first fic"
|
||||
''')
|
||||
self.story.setMetadata('datePublished',makeDate("1975-03-15","%Y-%m-%d"))
|
||||
@@ -127,44 +128,64 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
|
||||
self.story.addToList('category',u'Magical Girl Lyrical Nanoha')
|
||||
|
||||
self.story.addToList('genre','Fantasy')
|
||||
self.story.addToList('genre','SF')
|
||||
self.story.addToList('genre','Comedy')
|
||||
self.story.addToList('genre','Sci-Fi')
|
||||
self.story.addToList('genre','Noir')
|
||||
|
||||
self.story.addToList('characters','Bob Smith')
|
||||
self.story.addToList('characters','George Johnson')
|
||||
self.story.addToList('characters','Fred Smythe')
|
||||
|
||||
self.story.addToList('listX','xVal1')
|
||||
self.story.addToList('listX','xVal2')
|
||||
self.story.addToList('listX','xVal3')
|
||||
self.story.addToList('listX','xVal4')
|
||||
|
||||
self.story.addToList('listY','yVal1')
|
||||
self.story.addToList('listY','yVal2')
|
||||
self.story.addToList('listY','yVal3')
|
||||
self.story.addToList('listY','yVal4')
|
||||
|
||||
self.story.addToList('listZ','zVal1')
|
||||
self.story.addToList('listZ','zVal2')
|
||||
self.story.addToList('listZ','zVal3')
|
||||
self.story.addToList('listZ','zVal4')
|
||||
|
||||
self.story.setMetadata('metaA','98765')
|
||||
self.story.setMetadata('metaB','01245')
|
||||
self.story.setMetadata('metaC','The mighty metaC!')
|
||||
|
||||
self.chapterUrls = [(u'Prologue '+self.crazystring,self.url+"&chapter=1"),
|
||||
('Chapter 1, Xenos on Cinnabar',self.url+"&chapter=2"),
|
||||
('Chapter 2, Sinmay on Kintikin',self.url+"&chapter=3"),
|
||||
('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=6"),
|
||||
# ('Chapter 7',self.url+"&chapter=6"),
|
||||
# ('Chapter 8',self.url+"&chapter=6"),
|
||||
# ('Chapter 9',self.url+"&chapter=6"),
|
||||
# ('Chapter 0',self.url+"&chapter=6"),
|
||||
# ('Chapter a',self.url+"&chapter=6"),
|
||||
# ('Chapter b',self.url+"&chapter=6"),
|
||||
# ('Chapter c',self.url+"&chapter=6"),
|
||||
# ('Chapter d',self.url+"&chapter=6"),
|
||||
# ('Chapter e',self.url+"&chapter=6"),
|
||||
# ('Chapter f',self.url+"&chapter=6"),
|
||||
# ('Chapter g',self.url+"&chapter=6"),
|
||||
# ('Chapter h',self.url+"&chapter=6"),
|
||||
# ('Chapter i',self.url+"&chapter=6"),
|
||||
# ('Chapter j',self.url+"&chapter=6"),
|
||||
# ('Chapter k',self.url+"&chapter=6"),
|
||||
# ('Chapter l',self.url+"&chapter=6"),
|
||||
# ('Chapter m',self.url+"&chapter=6"),
|
||||
# ('Chapter n',self.url+"&chapter=6"),
|
||||
#('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"),
|
||||
#('Chapter b',self.url+"&chapter=c"),
|
||||
#('Chapter c',self.url+"&chapter=d"),
|
||||
#('Chapter d',self.url+"&chapter=e"),
|
||||
#('Chapter e',self.url+"&chapter=f"),
|
||||
#('Chapter f',self.url+"&chapter=g"),
|
||||
#('Chapter g',self.url+"&chapter=h"),
|
||||
#('Chapter h',self.url+"&chapter=i"),
|
||||
#('Chapter i',self.url+"&chapter=j"),
|
||||
#('Chapter j',self.url+"&chapter=k"),
|
||||
#('Chapter k',self.url+"&chapter=l"),
|
||||
#('Chapter l',self.url+"&chapter=m"),
|
||||
#('Chapter m',self.url+"&chapter=n"),
|
||||
#('Chapter n',self.url+"&chapter=o"),
|
||||
]
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
|
||||
def getChapterText(self, url):
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
if self.story.getMetadata('storyId') == '667':
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s!" % url)
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,7 +48,7 @@ class TheAlphaGateComAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -55,11 +56,6 @@ class TheAlphaGateComAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','tag')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Stargate: SG-1")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%d %b %Y"
|
||||
@@ -81,7 +77,7 @@ class TheAlphaGateComAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -203,7 +199,7 @@ class TheAlphaGateComAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,7 +48,7 @@ class TheHexFilesNetAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -55,11 +56,6 @@ class TheHexFilesNetAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','thf')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Harry Potter")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%Y.%m.%d"
|
||||
@@ -85,7 +81,7 @@ class TheHexFilesNetAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -151,6 +147,7 @@ class TheHexFilesNetAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
summary = info.find('td', {'class' : 'summary'})
|
||||
summary.name='div' # change td to div so it doesn't mess up the display when using table titlepage.
|
||||
self.setDescription(url,summary)
|
||||
|
||||
rating=stripHTML(info.find('td', {'align' : 'left'})).split('(')[1].split(')')[0]
|
||||
@@ -187,7 +184,7 @@ class TheHexFilesNetAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
soup = bs.BeautifulStoneSoup(self._fetchUrl(url),
|
||||
selfClosingTags=('br','hr','img')) # otherwise soup eats the br/hr tags.
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2011 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
|
||||
|
||||
# By virtue of being recent and requiring both is_adult and user/pass,
|
||||
# adapter_fanficcastletvnet.py is the best choice for learning to
|
||||
# write adapters--especially for sites that use the eFiction system.
|
||||
# Most sites that have ".../viewstory.php?sid=123" in the story URL
|
||||
# are eFiction.
|
||||
|
||||
# For non-eFiction sites, it can be considerably more complex, but
|
||||
# this is still a good starting point.
|
||||
|
||||
# In general an 'adapter' needs to do these five things:
|
||||
|
||||
# - 'Register' correctly with the downloader
|
||||
# - Site Login (if needed)
|
||||
# - 'Are you adult?' check (if needed--some do one, some the other, some both)
|
||||
# - Grab the chapter list
|
||||
# - Grab the story meta-data (some (non-eFiction) adapters have to get it from the author page)
|
||||
# - Grab the chapter texts
|
||||
|
||||
# Search for XXX comments--that's where things are most likely to need changing.
|
||||
|
||||
# This function is called by the downloader in all adapter_*.py files
|
||||
# in this dir to register the adapter class. So it needs to be
|
||||
# updated to reflect the class below it. That, plus getSiteDomain()
|
||||
# take care of 'Registering'.
|
||||
def getClass():
|
||||
return TheHookupZoneNetAdapter # XXX
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class TheHookupZoneNetAdapter(BaseSiteAdapter): # XXX
|
||||
|
||||
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.
|
||||
# XXX Most sites don't have the /fanfic part. Replace all to remove it usually.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/CriminalMinds/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','thupz') # XXX
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%b %d, %Y" # XXX
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'thehookupzone.net' # XXX
|
||||
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://"+self.getSiteDomain()+"/CriminalMinds/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain()+"/CriminalMinds/viewstory.php?sid=")+r"\d+$"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
if 'Registered Users Only' in data \
|
||||
or 'There is no such account on our website' in data \
|
||||
or "That password doesn't match the one in our database" in data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def performLogin(self, url):
|
||||
params = {}
|
||||
|
||||
if self.password:
|
||||
params['penname'] = self.username
|
||||
params['password'] = self.password
|
||||
else:
|
||||
params['penname'] = self.getConfig("username")
|
||||
params['password'] = self.getConfig("password")
|
||||
params['cookiecheck'] = '1'
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/CriminalMinds/user.php?action=login'
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Member Account" not in d : #Member Account
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
# Weirdly, different sites use different warning numbers.
|
||||
# If the title search below fails, there's a good chance
|
||||
# you need a different number. print data at that point
|
||||
# and see what the 'click here to continue' url says.
|
||||
addurl = "&ageconsent=ok&warning=4" # XXX
|
||||
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
|
||||
|
||||
if self.needToLoginCheck(data):
|
||||
# need to log in for this one.
|
||||
self.performLogin(url)
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
# The actual text that is used to announce you need to be an
|
||||
# adult varies from site to site. Again, print data before
|
||||
# the title search to troubleshoot.
|
||||
if "Age Consent Required" in data: # XXX
|
||||
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+'/CriminalMinds/'+chapter['href']+addurl))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
# eFiction sites don't help us out a lot with their meta data
|
||||
# formating, so it's a little ugly.
|
||||
|
||||
# utility method
|
||||
def defaultGetattr(d,k):
|
||||
try:
|
||||
return d[k]
|
||||
except:
|
||||
return ""
|
||||
|
||||
# <span class="label">Rated:</span> NC-17<br /> etc
|
||||
labels = soup.findAll('span',{'class':'label'})
|
||||
for labelspan in labels:
|
||||
value = labelspan.nextSibling
|
||||
label = labelspan.string
|
||||
|
||||
if 'Summary' in label:
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
if 'Rated' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
if 'Categories' in label:
|
||||
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
catstext = [cat.string for cat in cats]
|
||||
for cat in catstext:
|
||||
self.story.addToList('category',cat.string)
|
||||
|
||||
if 'Characters' in label:
|
||||
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
|
||||
charstext = [char.string for char in chars]
|
||||
for char in charstext:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
## Not all sites use Genre, but there's no harm to
|
||||
## leaving it in. Check to make sure the type_id number
|
||||
## is correct, though--it's site specific.
|
||||
if 'Genre' in label:
|
||||
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX
|
||||
genrestext = [genre.string for genre in genres]
|
||||
self.genre = ', '.join(genrestext)
|
||||
for genre in genrestext:
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
## Not all sites use Warnings, but there's no harm to
|
||||
## leaving it in. Check to make sure the type_id number
|
||||
## is correct, though--it's site specific.
|
||||
if 'Warnings' in label:
|
||||
warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX
|
||||
warningstext = [warning.string for warning in warnings]
|
||||
self.warning = ', '.join(warningstext)
|
||||
for warning in warningstext:
|
||||
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:
|
||||
# there's a stray [ at the end.
|
||||
#value = value[0:-1]
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
try:
|
||||
# Find Series name from series URL.
|
||||
a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+"))
|
||||
series_name = a.string
|
||||
series_url = 'http://'+self.host+'/CriminalMinds/'+a['href']
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url))
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$'))
|
||||
i=1
|
||||
for a in storyas:
|
||||
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)
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,20 +48,20 @@ class TheMasqueNetAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
if self.parsedUrl.path.split('/',)[1] == 'wiktt':
|
||||
self.story.addToList("category","Harry Potter")
|
||||
self.story.setMetadata('section','/wiktt/efiction/')
|
||||
self.section='/wiktt/efiction/'
|
||||
self.dateformat = "%m/%d/%Y"
|
||||
else:
|
||||
self.story.addToList("category","Originals")
|
||||
self.story.setMetadata('section','/efiction/')
|
||||
self.section='/efiction/'
|
||||
self.dateformat = "%b %d, %Y"
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + self.story.getMetadata('section') + 'viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
self._setURL('http://' + self.getSiteDomain() + self.section + 'viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','msq')
|
||||
@@ -98,14 +99,14 @@ class TheMasqueNetAdapter(BaseSiteAdapter):
|
||||
params['cookiecheck'] = '1'
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + self.story.getMetadata('section') + 'user.php?action=login'
|
||||
logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
loginUrl = 'http://' + self.getSiteDomain() + self.section + 'user.php?action=login'
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Member Account" not in d : #Member Account
|
||||
logging.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
@@ -127,7 +128,7 @@ class TheMasqueNetAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -152,7 +153,7 @@ class TheMasqueNetAdapter(BaseSiteAdapter):
|
||||
# correct stupid & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL 2nd try: "+url)
|
||||
logger.debug("URL 2nd try: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -186,7 +187,7 @@ class TheMasqueNetAdapter(BaseSiteAdapter):
|
||||
# Find the chapters:
|
||||
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")):
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host + self.story.getMetadata('section') + chapter['href']+addurl))
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host + self.section + chapter['href']+addurl))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
@@ -258,7 +259,7 @@ class TheMasqueNetAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2012 Fanficdownloader team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import time
|
||||
import logging
|
||||
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 ThePetulantPoetessComAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class ThePetulantPoetessComAdapter(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') +'&i=1')
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','tpp')
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%Y/%m/%d"
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'www.thepetulantpoetess.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+$"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
if 'You must be a member to read this story.' in data \
|
||||
or "The Ministry of Magic does not have a record of that password. " in data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def performLogin(self, url):
|
||||
params = {}
|
||||
|
||||
if self.password:
|
||||
params['penname'] = self.username
|
||||
params['password'] = self.password
|
||||
else:
|
||||
params['penname'] = self.getConfig("username")
|
||||
params['password'] = self.getConfig("password")
|
||||
params['cookiecheck'] = '1'
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/user.php?action=login'
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "My Account Page" not in d : #Member Account
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
# 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 self.needToLoginCheck(data):
|
||||
# need to log in for this one.
|
||||
self.performLogin(url)
|
||||
data = self._fetchUrl(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.
|
||||
|
||||
# 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:
|
||||
chapters=soup.find('select', {'name' : 'sid'})
|
||||
if chapters == None:
|
||||
self.chapterUrls.append((self.story.getMetadata('title'),url))
|
||||
else:
|
||||
for chapter in chapters.findAll('option', value=re.compile(r"viewstory.php\?sid=\d+&i=1")):
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['value']))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
# make sure that the story id is from the first chapter
|
||||
self.story.setMetadata('storyId',self.chapterUrls[0][1].split('=')[1].split('&')[0])
|
||||
|
||||
#locate the story on author's page
|
||||
index = 1
|
||||
found = 0
|
||||
while found == 0:
|
||||
asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')+"&page="+str(index)))
|
||||
|
||||
for info in asoup.findAll('td', {'class' : 'highlightcolor1'}):
|
||||
a = info.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
|
||||
if a != None:
|
||||
self.story.setMetadata('title',a.text)
|
||||
found = 1
|
||||
break
|
||||
index=index+1
|
||||
|
||||
# extract metadata
|
||||
b=info.find('b')
|
||||
b.find('a').extract()
|
||||
self.story.setMetadata('rating', b.text.split('[')[1].split(']')[0])
|
||||
|
||||
info = info.findNext('td', {'colspan' : '2'})
|
||||
for label in info.findAll('b'):
|
||||
value = label.nextSibling
|
||||
|
||||
if "Category" in label.text:
|
||||
for cat in info.findAll('a'):
|
||||
self.story.addToList('category',cat.string)
|
||||
|
||||
if "Characters" in label.text:
|
||||
for char in value.split(', '):
|
||||
self.story.addToList('characters',char)
|
||||
|
||||
if "Genres" in label.text:
|
||||
for genre in value.split(', '):
|
||||
if "General" not in genre:
|
||||
self.story.addToList('genre',genre)
|
||||
|
||||
if "Warnings" in label.text:
|
||||
for warning in value.split(', '):
|
||||
if "none" not in warning.lower():
|
||||
self.story.addToList('warnings',warning)
|
||||
|
||||
|
||||
info = info.findNext('td', {'class' : 'tblborder'})
|
||||
info.find('b').extract()
|
||||
self.setDescription(url,info)
|
||||
|
||||
info = info.findNext('td', {'class' : 'highlightcolor2'})
|
||||
for label in info.findAll('b'):
|
||||
value = label.nextSibling
|
||||
|
||||
if "Completed" in label.text:
|
||||
if 'Yes' in value:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
if 'Published' in label.text:
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
if 'Updated' in label.text:
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
if 'Word Count' in label.text:
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
|
||||
# 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.findAll('table')[2].findAll('td')[1]
|
||||
for a in div.findAll('div'):
|
||||
a.extract()
|
||||
div.name='div'
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -51,7 +52,7 @@ class TheQuidditchPitchOrgAdapter(BaseSiteAdapter): # XXX
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
# XXX Most sites don't have the part. Replace all to remove it usually.
|
||||
@@ -60,11 +61,6 @@ class TheQuidditchPitchOrgAdapter(BaseSiteAdapter): # XXX
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','tqdpch') # XXX
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Harry Potter") # XXX
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%m/%d/%Y" # XXX
|
||||
@@ -106,13 +102,13 @@ class TheQuidditchPitchOrgAdapter(BaseSiteAdapter): # XXX
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/user.php?action=login'
|
||||
logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Member Account" not in d : #Member Account
|
||||
logging.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
@@ -134,7 +130,7 @@ class TheQuidditchPitchOrgAdapter(BaseSiteAdapter): # XXX
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -277,7 +273,7 @@ class TheQuidditchPitchOrgAdapter(BaseSiteAdapter): # XXX
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
|
||||
soup = bs.BeautifulStoneSoup(self._fetchUrl(url),
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib
|
||||
import urllib2
|
||||
@@ -43,7 +44,7 @@ class TheWritersCoffeeShopComSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/library/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -81,13 +82,13 @@ class TheWritersCoffeeShopComSiteAdapter(BaseSiteAdapter):
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/library/user.php?action=login'
|
||||
logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Member Account" not in d : #Member Account
|
||||
logging.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
@@ -102,7 +103,7 @@ class TheWritersCoffeeShopComSiteAdapter(BaseSiteAdapter):
|
||||
addurl=""
|
||||
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -235,7 +236,7 @@ class TheWritersCoffeeShopComSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
data = self._fetchUrl(url)
|
||||
# problems with some stories, but only in calibre. I suspect
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
import time
|
||||
@@ -40,7 +41,7 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
|
||||
m = re.match(self.getSiteURLPattern(),url)
|
||||
if m:
|
||||
self.story.setMetadata('storyId',m.group('id'))
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
# normalized story URL.
|
||||
self._setURL("http://"+self.getSiteDomain()\
|
||||
+"/Story-"+self.story.getMetadata('storyId'))
|
||||
@@ -81,7 +82,7 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
|
||||
return
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/login.php'
|
||||
logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['urealname']))
|
||||
|
||||
## need to pull empty login page first to get ctkn and
|
||||
@@ -98,7 +99,7 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Stories Published" not in d : #Member Account
|
||||
logging.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
@@ -110,7 +111,7 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
|
||||
# metadata and chapter list
|
||||
|
||||
url=self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
# tth won't send you future updates if you aren't 'caught up'
|
||||
# on the story. Login isn't required for F21, but logging in will
|
||||
@@ -137,7 +138,7 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
|
||||
form = soup.find('form', {'id':'sitemaxratingform'})
|
||||
params={'ctkn':form.find('input', {'name':'ctkn'})['value'],
|
||||
'sitemaxrating':'5'}
|
||||
logging.info("Attempting to get rating cookie for %s" % url)
|
||||
logger.info("Attempting to get rating cookie for %s" % url)
|
||||
data = self._postUrl("http://"+self.getSiteDomain()+'/setmaxrating.php',params)
|
||||
# refetch story page.
|
||||
data = self._fetchUrl(url)
|
||||
@@ -158,7 +159,7 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
|
||||
try:
|
||||
# going to pull part of the meta data from author list page.
|
||||
infourl = 'http://'+self.host+ainfo['href']
|
||||
logging.debug("**StoryInfo** URL: "+infourl)
|
||||
logger.debug("**StoryInfo** URL: "+infourl)
|
||||
infodata = self._fetchUrl(infourl)
|
||||
infosoup = bs.BeautifulSoup(infodata)
|
||||
|
||||
@@ -175,14 +176,14 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
try:
|
||||
# going to pull part of the meta data from *primary* author list page.
|
||||
logging.debug("**AUTHOR** URL: "+authorurl)
|
||||
logger.debug("**AUTHOR** URL: "+authorurl)
|
||||
authordata = self._fetchUrl(authorurl)
|
||||
descurl=authorurl
|
||||
authorsoup = bs.BeautifulSoup(authordata)
|
||||
# author can have several pages, scan until we find it.
|
||||
while( not authorsoup.find('a', href=re.compile(r"^/Story-"+self.story.getMetadata('storyId'))) ):
|
||||
nextpage = 'http://'+self.host+authorsoup.find('a', {'class':'arrowf'})['href']
|
||||
logging.debug("**AUTHOR** nextpage URL: "+nextpage)
|
||||
logger.debug("**AUTHOR** nextpage URL: "+nextpage)
|
||||
authordata = self._fetchUrl(nextpage)
|
||||
descurl=nextpage
|
||||
authorsoup = bs.BeautifulSoup(authordata)
|
||||
@@ -259,7 +260,7 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
|
||||
def getChapterText(self, url):
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(url))
|
||||
|
||||
div = soup.find('div', {'id' : 'storyinnerbody'})
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,7 +48,7 @@ class TwilightArchivesComAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL. http://www.twilightarchives.com/read/9353
|
||||
self._setURL('http://' + self.getSiteDomain() + '/read/'+self.story.getMetadata('storyId'))
|
||||
@@ -55,11 +56,6 @@ class TwilightArchivesComAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','twa')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Twilight")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%d %b %y"
|
||||
@@ -82,7 +78,7 @@ class TwilightArchivesComAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -177,7 +173,7 @@ class TwilightArchivesComAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib
|
||||
import urllib2
|
||||
@@ -37,13 +38,12 @@ class TwilightedNetSiteAdapter(BaseSiteAdapter):
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
self.story.addToList("category","Twilight")
|
||||
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -84,13 +84,13 @@ class TwilightedNetSiteAdapter(BaseSiteAdapter):
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/user.php?action=login'
|
||||
logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Member Account" not in d : #Member Account
|
||||
logging.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
@@ -100,7 +100,7 @@ class TwilightedNetSiteAdapter(BaseSiteAdapter):
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
url = self.url+'&index=1'
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -226,7 +226,7 @@ class TwilightedNetSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
data = self._fetchUrl(url)
|
||||
# problems with some stories, but only in calibre. I suspect
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib
|
||||
import urllib2
|
||||
@@ -37,14 +38,13 @@ class TwiwriteNetSiteAdapter(BaseSiteAdapter):
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
self.story.addToList("category","Twilight")
|
||||
self.is_adult = False
|
||||
self.username = "NoneGiven" # if left empty, twiwrite.net doesn't return any message at all.
|
||||
self.password = ""
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -85,13 +85,13 @@ class TwiwriteNetSiteAdapter(BaseSiteAdapter):
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/user.php?action=login'
|
||||
logging.info("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
logger.info("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Member Account" not in d : #Member Account
|
||||
logging.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
@@ -110,7 +110,7 @@ class TwiwriteNetSiteAdapter(BaseSiteAdapter):
|
||||
addurl=""
|
||||
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -139,12 +139,14 @@ class TwiwriteNetSiteAdapter(BaseSiteAdapter):
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = bs.BeautifulSoup(data)
|
||||
|
||||
pagetitlediv = soup.find('div',id='pagetitle')
|
||||
|
||||
## Title
|
||||
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
|
||||
a = pagetitlediv.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+"))
|
||||
a = pagetitlediv.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)
|
||||
@@ -253,7 +255,7 @@ class TwiwriteNetSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
data = self._fetchUrl(url)
|
||||
# problems with some stories, but only in calibre. I suspect
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -45,7 +46,7 @@ class WalkingThePlankOrgAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/archive/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -53,11 +54,6 @@ class WalkingThePlankOrgAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','wtp')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Harry Potter")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%b %d, %Y"
|
||||
@@ -89,7 +85,7 @@ class WalkingThePlankOrgAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -220,7 +216,7 @@ class WalkingThePlankOrgAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -56,7 +57,7 @@ class WhoficComSiteAdapter(BaseSiteAdapter):
|
||||
# - get chapter list, if not one-shot.
|
||||
|
||||
url = self.url+'&chapter=1'
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
try:
|
||||
@@ -69,7 +70,7 @@ class WhoficComSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
# pull title(title) and author from the HTML title.
|
||||
title = soup.find('title').string
|
||||
logging.debug('Title: %s' % title)
|
||||
logger.debug('Title: %s' % title)
|
||||
title = title.split('::')[1].strip()
|
||||
self.story.setMetadata('title',title.split(' by ')[0].strip())
|
||||
self.story.setMetadata('author',title.split(' by ')[1].strip())
|
||||
@@ -109,7 +110,7 @@ class WhoficComSiteAdapter(BaseSiteAdapter):
|
||||
# <i>Published:</i> 2010.08.15 - <i>Updated:</i> 2010.08.16 - <i>Chapters:</i> 4 - <i>Completed:</i> Yes - <i>Word Count:</i> 4890 </font>
|
||||
# </td></tr></table>
|
||||
|
||||
logging.debug("Author URL: "+self.story.getMetadata('authorUrl'))
|
||||
logger.debug("Author URL: "+self.story.getMetadata('authorUrl'))
|
||||
soup = bs.BeautifulStoneSoup(self._fetchUrl(self.story.getMetadata('authorUrl')),
|
||||
selfClosingTags=('br')) # normalize <br> tags to <br />
|
||||
# find this story in the list, parse it's metadata based on
|
||||
@@ -212,7 +213,7 @@ class WhoficComSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -47,7 +48,7 @@ class WizardTalesNetAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -92,13 +93,13 @@ class WizardTalesNetAdapter(BaseSiteAdapter):
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/user.php?action=login'
|
||||
logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Member Account" not in d : #Member Account
|
||||
logging.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
@@ -120,7 +121,7 @@ class WizardTalesNetAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -145,7 +146,7 @@ class WizardTalesNetAdapter(BaseSiteAdapter):
|
||||
# correct stupid & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL 2nd try: "+url)
|
||||
logger.debug("URL 2nd try: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -286,7 +287,7 @@ class WizardTalesNetAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2012 Fanficdownloader team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import time
|
||||
import logging
|
||||
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 WolverineAndRogueComAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class WolverineAndRogueComAdapter(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() + '/wrfa/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','wrfa')
|
||||
|
||||
# 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.wolverineandrogue.com'
|
||||
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://"+self.getSiteDomain()+"/wrfa/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain()+"/wrfa/viewstory.php?sid=")+r"\d+$"
|
||||
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'
|
||||
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 "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
|
||||
pt = soup.find('div', {'id' : 'pagetitle'})
|
||||
a = pt.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
|
||||
self.story.setMetadata('title',a.text)
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
a = pt.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+'/wrfa/'+a['href'])
|
||||
self.story.setMetadata('author',a.string)
|
||||
|
||||
rating=pt.text.split('(')[1].split(')')[0]
|
||||
self.story.setMetadata('rating', rating)
|
||||
|
||||
# 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+'/wrfa/'+chapter['href']))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
# eFiction sites don't help us out a lot with their meta data
|
||||
# formating, so it's a little ugly.
|
||||
|
||||
# <span class="label">Rated:</span> NC-17<br /> etc
|
||||
content=soup.find('div',{'class' : 'content'})
|
||||
labels = soup.findAll('span',{'class':'label'})
|
||||
|
||||
value = labels[0].previousSibling
|
||||
svalue = ""
|
||||
while value != None:
|
||||
val = value
|
||||
value = value.previousSibling
|
||||
while "Categories" not in str(val):
|
||||
svalue += str(val)
|
||||
val = val.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
|
||||
|
||||
for labelspan in labels:
|
||||
value = labelspan.nextSibling
|
||||
label = labelspan.string
|
||||
|
||||
if 'Rated' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value.split(' -')[0])
|
||||
|
||||
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 'Complete' in label:
|
||||
if 'Yes' in value:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
if 'Published' in label:
|
||||
self.story.setMetadata('datePublished', makeDate(value.split(' -')[0], self.dateformat))
|
||||
|
||||
if 'Updated' in label:
|
||||
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+'/wrfa/'+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)
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -48,7 +49,7 @@ class WraithBaitComAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -56,11 +57,6 @@ class WraithBaitComAdapter(BaseSiteAdapter):
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','wb')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Stargate")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%d %b %Y"
|
||||
@@ -91,7 +87,7 @@ class WraithBaitComAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -214,7 +210,7 @@ class WraithBaitComAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(url))
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
@@ -41,7 +42,7 @@ class YourFanfictionComAdapter(BaseSiteAdapter):
|
||||
# targeted as us and offered to 'whitelist our IP'. Clearly,
|
||||
# that wouldn't work, but it does let me do this in good
|
||||
# conscience:
|
||||
self.opener.addheaders = [('User-agent', 'FFDL/1.5')]
|
||||
self.opener.addheaders = [('User-agent', 'FFDL/1.6')]
|
||||
|
||||
self.decode = ["Windows-1252",
|
||||
"utf8"] # 1252 is a superset of iso-8859-1.
|
||||
@@ -54,7 +55,7 @@ class YourFanfictionComAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
@@ -92,7 +93,7 @@ class YourFanfictionComAdapter(BaseSiteAdapter):
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL: "+url)
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -115,7 +116,7 @@ class YourFanfictionComAdapter(BaseSiteAdapter):
|
||||
# viewstory.php?sid=1654&ageconsent=ok&warning=5
|
||||
#print data
|
||||
#m = re.search(r"'viewstory.php\?sid=1882(&warning=4)'",data)
|
||||
m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data)
|
||||
m = re.search(r"'viewstory.php\?sid=\d+((&ageconsent=ok)?&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
|
||||
@@ -123,9 +124,10 @@ class YourFanfictionComAdapter(BaseSiteAdapter):
|
||||
# link and reload data.
|
||||
addurl = m.group(1)
|
||||
# correct stupid & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
# explicitly put ageconsent because google appengine regexp doesn't include it for some reason.
|
||||
addurl = addurl.replace("&","&")+'&ageconsent=ok'
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.debug("URL 2nd try: "+url)
|
||||
logger.debug("URL 2nd try: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
@@ -140,9 +142,17 @@ class YourFanfictionComAdapter(BaseSiteAdapter):
|
||||
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.")
|
||||
|
||||
# because for some reason, this works while simple 'print data' errors on ascii conversion.
|
||||
# loopdata = data
|
||||
# chklen=5000
|
||||
# while len(loopdata) > 0:
|
||||
# if len(loopdata) < 5000:
|
||||
# chklen = len(loopdata)
|
||||
# logger.info("loopdata: %s" % loopdata[:chklen])
|
||||
# loopdata = loopdata[chklen:]
|
||||
|
||||
# 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.
|
||||
|
||||
@@ -182,9 +192,11 @@ class YourFanfictionComAdapter(BaseSiteAdapter):
|
||||
if 'Summary' in label:
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
while value and not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
value = value.nextSibling
|
||||
# sometimes poorly formated desc (<p> w/o </p>) leads
|
||||
# to all labels being included.
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
@@ -245,7 +257,6 @@ class YourFanfictionComAdapter(BaseSiteAdapter):
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+'))
|
||||
i=1
|
||||
for a in storyas:
|
||||
print("series a['href']:%s"%a['href'])
|
||||
# skip 'report this' and 'TOC' links
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
@@ -260,7 +271,7 @@ class YourFanfictionComAdapter(BaseSiteAdapter):
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % 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.
|
||||
|
||||
@@ -27,6 +27,8 @@ from functools import partial
|
||||
from .. import BeautifulSoup as bs
|
||||
from ..htmlcleanup import stripHTML
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from google.appengine.api import apiproxy_stub_map
|
||||
def urlfetch_timeout_hook(service, call, request, response):
|
||||
@@ -38,10 +40,10 @@ try:
|
||||
|
||||
apiproxy_stub_map.apiproxy.GetPreCallHooks().Append(
|
||||
'urlfetch_timeout_hook', urlfetch_timeout_hook, 'urlfetch')
|
||||
logging.info("Hook to make default deadline 10.0 installed.")
|
||||
logger.info("Hook to make default deadline 10.0 installed.")
|
||||
except:
|
||||
pass
|
||||
#logging.info("Hook to make default deadline 10.0 NOT installed--not using appengine")
|
||||
#logger.info("Hook to make default deadline 10.0 NOT installed--not using appengine")
|
||||
|
||||
from ..story import Story
|
||||
from ..gziphttp import GZipProcessor
|
||||
@@ -67,10 +69,8 @@ class BaseSiteAdapter(Configurable):
|
||||
def validateURL(self):
|
||||
return re.match(self.getSiteURLPattern(), self.url)
|
||||
|
||||
def __init__(self, config, url):
|
||||
self.config = config
|
||||
Configurable.__init__(self, config)
|
||||
self.setSectionOrder(self.getConfigSection())
|
||||
def __init__(self, configuration, url):
|
||||
Configurable.__init__(self, configuration)
|
||||
|
||||
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
@@ -79,7 +79,7 @@ class BaseSiteAdapter(Configurable):
|
||||
self.opener = u2.build_opener(u2.HTTPCookieProcessor(),GZipProcessor())
|
||||
self.storyDone = False
|
||||
self.metadataDone = False
|
||||
self.story = Story()
|
||||
self.story = Story(configuration)
|
||||
self.story.setMetadata('site',self.getConfigSection())
|
||||
self.story.setMetadata('dateCreated',datetime.datetime.now())
|
||||
self.chapterUrls = [] # tuples of (chapter title,chapter url)
|
||||
@@ -89,6 +89,7 @@ class BaseSiteAdapter(Configurable):
|
||||
self.oldimgs = None
|
||||
self.oldcover = None # (data of existing cover html, data of existing cover image)
|
||||
self.calibrebookmark = None
|
||||
self.logfile = None
|
||||
## order of preference for decoding.
|
||||
self.decode = ["utf8",
|
||||
"Windows-1252"] # 1252 is a superset of
|
||||
@@ -126,7 +127,7 @@ class BaseSiteAdapter(Configurable):
|
||||
#print code
|
||||
if code == "auto":
|
||||
if not chardet:
|
||||
logging.info("chardet not available, skipping 'auto' encoding")
|
||||
logger.info("chardet not available, skipping 'auto' encoding")
|
||||
continue
|
||||
detected = chardet.detect(data)
|
||||
#print detected
|
||||
@@ -134,12 +135,11 @@ class BaseSiteAdapter(Configurable):
|
||||
code=detected['encoding']
|
||||
else:
|
||||
continue
|
||||
logging.debug("try code:"+code)
|
||||
return data.decode(code)
|
||||
except:
|
||||
logging.info("code failed:"+code)
|
||||
logger.debug("code failed:"+code)
|
||||
pass
|
||||
logging.info("Could not decode story, tried:%s Stripping non-ASCII."%decode)
|
||||
logger.info("Could not decode story, tried:%s Stripping non-ASCII."%decode)
|
||||
return "".join([x for x in data if ord(x) < 128])
|
||||
|
||||
# Assumes application/x-www-form-urlencoded. parameters, headers are dict()s
|
||||
@@ -176,10 +176,10 @@ class BaseSiteAdapter(Configurable):
|
||||
return self._decode(self._fetchUrlRaw(url,parameters))
|
||||
except Exception, e:
|
||||
excpt=e
|
||||
logging.warn("Caught an exception reading URL: %s Exception %s."%(unicode(url),unicode(e)))
|
||||
logger.warn("Caught an exception reading URL: %s Exception %s."%(unicode(url),unicode(e)))
|
||||
|
||||
logging.error("Giving up on %s" %url)
|
||||
logging.exception(excpt)
|
||||
logger.error("Giving up on %s" %url)
|
||||
logger.exception(excpt)
|
||||
raise(excpt)
|
||||
|
||||
# Limit chapters to download. Input starts at 1, list starts at 0
|
||||
@@ -214,8 +214,7 @@ class BaseSiteAdapter(Configurable):
|
||||
if self.getConfig('include_images') and \
|
||||
not self.story.cover and \
|
||||
self.getConfig('default_cover_image'):
|
||||
self.story.addImgUrl(self,
|
||||
None,
|
||||
self.story.addImgUrl(None,
|
||||
#self.getConfig('default_cover_image'),
|
||||
self.story.formatFileName(self.getConfig('default_cover_image'),
|
||||
self.getConfig('allow_unsafe_filename')),
|
||||
@@ -229,6 +228,8 @@ class BaseSiteAdapter(Configurable):
|
||||
# cheesy way to carry calibre bookmark file forward across update.
|
||||
if self.calibrebookmark:
|
||||
self.story.calibrebookmark = self.calibrebookmark
|
||||
if self.logfile:
|
||||
self.story.logfile = self.logfile
|
||||
|
||||
return self.story
|
||||
|
||||
@@ -286,13 +287,17 @@ class BaseSiteAdapter(Configurable):
|
||||
def setDescription(self,url,svalue):
|
||||
#print("\n\nsvalue:\n%s\n"%svalue)
|
||||
if self.getConfig('keep_summary_html'):
|
||||
if isinstance(svalue,str) or isinstance(svalue,unicode):
|
||||
if isinstance(svalue,basestring):
|
||||
svalue = bs.BeautifulSoup(svalue)
|
||||
self.story.setMetadata('description',self.utf8FromSoup(url,svalue))
|
||||
else:
|
||||
self.story.setMetadata('description',stripHTML(svalue))
|
||||
#print("\n\ndescription:\n"+self.story.getMetadata('description')+"\n\n")
|
||||
|
||||
def setCoverImage(self,storyurl,imgurl):
|
||||
if self.getConfig('include_images'):
|
||||
self.story.addImgUrl(storyurl,imgurl,self._fetchUrlRaw,cover=True)
|
||||
|
||||
# This gives us a unicode object, not just a string containing bytes.
|
||||
# (I gave soup a unicode string, you'd think it could give it back...)
|
||||
# Now also does a bunch of other common processing for us.
|
||||
@@ -300,7 +305,7 @@ class BaseSiteAdapter(Configurable):
|
||||
if not fetch:
|
||||
fetch=self._fetchUrlRaw
|
||||
|
||||
acceptable_attributes = ['href','name']
|
||||
acceptable_attributes = ['href','name','class','id']
|
||||
#print("include_images:"+self.getConfig('include_images'))
|
||||
if self.getConfig('include_images'):
|
||||
acceptable_attributes.extend(('src','alt','longdesc'))
|
||||
@@ -308,7 +313,7 @@ class BaseSiteAdapter(Configurable):
|
||||
# some pre-existing epubs have img tags that had src stripped off.
|
||||
if img.has_key('src'):
|
||||
img['longdesc']=img['src']
|
||||
img['src']=self.story.addImgUrl(self,url,img['src'],fetch,
|
||||
img['src']=self.story.addImgUrl(url,img['src'],fetch,
|
||||
coverexclusion=self.getConfig('cover_exclusion_regexp'))
|
||||
|
||||
for attr in soup._getAttrMap().keys():
|
||||
@@ -352,7 +357,6 @@ class BaseSiteAdapter(Configurable):
|
||||
|
||||
def cachedfetch(realfetch,cache,url):
|
||||
if url in cache:
|
||||
print("cache hit")
|
||||
return cache[url]
|
||||
else:
|
||||
return realfetch(url)
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
#
|
||||
# Author: Pau Sanchez (contact@pausanchez.com)
|
||||
# Version: v1.0
|
||||
# Last Modified: 2010/09/15
|
||||
#
|
||||
# For the latest version check out:
|
||||
# http://www.codigomanso.com/en/projects
|
||||
#
|
||||
# My blog:
|
||||
# http://www.codigomanso.com/en/ - English Version
|
||||
# http://www.codigomanso.com/es/ - Spanish Version
|
||||
#
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import urllib
|
||||
|
||||
class bbcode2html:
|
||||
'''
|
||||
This class gets a parsed BBCode and transforms it to valid HTML
|
||||
|
||||
Useful functions of this class:
|
||||
html
|
||||
convertToHTML
|
||||
|
||||
Example:
|
||||
> parser = bbcodeparser ()
|
||||
> parser.parse ('[b]bold[/b]')
|
||||
> bbcode2html (parser).html()
|
||||
<b>bold</b>
|
||||
|
||||
# This is faster for huge strings but changes the parser object internally
|
||||
> bbcode2html (parser).html(doDeepCopy = False)
|
||||
<b>bold</b>
|
||||
'''
|
||||
def __init__ (self, parser):
|
||||
self._parser = parser
|
||||
return
|
||||
|
||||
def html (self, allowClassAttr = False, doDeepCopy = True, parser = None):
|
||||
'''
|
||||
Convert current parsed code to HTML
|
||||
|
||||
Example:
|
||||
code = bbcodeparser ('[b]bold[/b]')
|
||||
code.html() -> '<b>bold</b>'
|
||||
'''
|
||||
if parser is None:
|
||||
parser = self._parser
|
||||
|
||||
tokens = parser
|
||||
if instanceof (parser, bbcodeparser):
|
||||
tokens = parser.getTokens()
|
||||
|
||||
return bbcode2html.convertToHTML (tokens, allowClassAttr = allowClassAttr, doDeepCopy = doDeepCopy)
|
||||
|
||||
@staticmethod
|
||||
def htmlString (string):
|
||||
toReplace = {
|
||||
u'<' : '<',
|
||||
u'>' : '>',
|
||||
u'"' : """,
|
||||
u'&' : "&"
|
||||
}
|
||||
for entity in toReplace:
|
||||
string = string.replace(entity, toReplace[entity])
|
||||
return string
|
||||
|
||||
@staticmethod
|
||||
def getValidTags ():
|
||||
simpleTags = ['b', 'u', 'i', 'sup', 'sub', 'ul', 'ol', 'li', 'table', 'tr', 'th', 'td', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']
|
||||
validTags = {
|
||||
'p' : { 'color' : 'color', 'size' : 'size', 'font' : 'font' },
|
||||
'color' : { 'color' : 'color' },
|
||||
'size' : { 'size' : 'size' },
|
||||
'font' : { 'font' : 'font' },
|
||||
'img' : { 'alt' : 'alt', 'title' : 'title', 'width' : 'width' , 'height' : 'height', 'img' : 'img'},
|
||||
'url' : { 'href' : 'href', 'url' : 'href', 'link' : 'href', 'title' : 'title' },
|
||||
's' : { },
|
||||
'code' : { },
|
||||
'quote' : { },
|
||||
'list' : { 'list' : 'type' },
|
||||
'email' : { 'email': 'href'},
|
||||
'google' : { 'google': 'google'},
|
||||
'wikipedia' : { 'wikipedia' : 'wikipedia', 'language' : 'language', 'lang' : 'lang'}
|
||||
}
|
||||
|
||||
for tag in simpleTags:
|
||||
validTags[tag] = { }
|
||||
return validTags
|
||||
|
||||
@staticmethod
|
||||
def convertToHTML (tokens, allowClassAttr = False, validTags = None, doDeepCopy = True):
|
||||
'''
|
||||
Convert internally parsed BBCode to XHTML
|
||||
|
||||
@doDeepCopy
|
||||
True: it does a deep copy of tokens so this list will remain unchanged
|
||||
False: tokens will be modified internally, but the output will be produced like 5x faster
|
||||
it's a good idea to use False only when this is the last operation
|
||||
'''
|
||||
# do a deep copy
|
||||
if doDeepCopy:
|
||||
import copy
|
||||
tokens = copy.deepcopy (tokens)
|
||||
|
||||
# filter invalid tags and attributes
|
||||
if validTags is None:
|
||||
validTags = bbcode2html.getValidTags()
|
||||
|
||||
bbcode2html._filterInvalidTagsAndAttributes (tokens, validTags, allowClassAttr)
|
||||
|
||||
# Start to convert
|
||||
index = 0
|
||||
tokenLength = len (tokens)
|
||||
|
||||
# use a list for the output (an order of magnitude faster than using string concatenation)
|
||||
htmlList = []
|
||||
lastListOpener = []
|
||||
|
||||
while index < tokenLength:
|
||||
|
||||
if isinstance (tokens [index], basestring):
|
||||
htmlList.append (bbcode2html.htmlString (tokens [index]))
|
||||
index += 1
|
||||
continue
|
||||
|
||||
token = tokens[index]
|
||||
tag = token['tag'] # opening or closing simple tag. e.g: 'b', '/b', '/u', ...
|
||||
tagName = (tag[1:] if tag[0] == '/' else tag)
|
||||
tagOpener = (u'/' if tag[0] == '/' else u'')
|
||||
tokenArgs = (token['args'] if 'args' in token else {})
|
||||
|
||||
# opening or closing simple tag COLOR / SIZE
|
||||
if (tagName in ['p', 'color', 'size', 'font']):
|
||||
style = ''
|
||||
style += ((u' color: ' + tokenArgs['color'] + u';') if ('color' in tokenArgs) else '')
|
||||
style += ((u' font-size: ' + tokenArgs['size'] + u'pt;') if ('size' in tokenArgs) else '')
|
||||
style += ((u' font-family: ' + tokenArgs['font'] + u';') if ('font' in tokenArgs) else '')
|
||||
style = style.strip()
|
||||
|
||||
pArgs = {}
|
||||
if style != '':
|
||||
pArgs ['style'] = style
|
||||
|
||||
if 'class' in tokenArgs:
|
||||
pArgs ['class'] = tokenArgs['class']
|
||||
|
||||
if ('args' not in token) and (tagName != 'p'):
|
||||
if (tagOpener == '/'): # if closing tag, close it
|
||||
htmlList.append (u'</span>')
|
||||
index += 1
|
||||
continue
|
||||
|
||||
if tagName != 'p':
|
||||
tag = tagOpener + u'span'
|
||||
|
||||
htmlList.append (bbcode2html.xml (tag, pArgs))
|
||||
|
||||
# IMG tag
|
||||
elif tag == 'img' and (index+2 < tokenLength):
|
||||
if 'img' in tokenArgs:
|
||||
# has the form of <width>x<height> ?
|
||||
sizeMatch = re.match (u'^\s*(\d+)[xX](\d+)\s*$', tokenArgs['img'])
|
||||
if sizeMatch is not None:
|
||||
tokenArgs['width'] = sizeMatch.group(1)
|
||||
tokenArgs['height'] = sizeMatch.group(2)
|
||||
# then assume is the alternative text
|
||||
else:
|
||||
tokenArgs['alt'] = tokenArgs['img']
|
||||
del tokenArgs['img']
|
||||
|
||||
# add the source of the image
|
||||
tokenArgs ['src'] = tokens[index+1]
|
||||
|
||||
# [img]http://www.whatever.com/pic.jpg[/img]
|
||||
htmlList.append (
|
||||
bbcode2html.xml ('img', tokenArgs, soloTag=True)
|
||||
)
|
||||
index += 2 # skip next token and closing tag
|
||||
|
||||
# URL tag
|
||||
elif tag == 'url':
|
||||
if ('args' not in token) and (index+2 < tokenLength):
|
||||
# [url]http://www.google.com[/url]
|
||||
htmlList.append (bbcode2html.xml ('a', { 'href' : tokens[index+1] }))
|
||||
else:
|
||||
# [url=http://www.google.com]Google[/url]
|
||||
# [url link=http://www.google.com title="This is Google"]Google[/url]
|
||||
htmlList.append (bbcode2html.xml ('a', tokenArgs))
|
||||
|
||||
# URL closing tag (sometimes needed)
|
||||
elif (tag == '/url') or (tag == '/email'):
|
||||
htmlList.append (u'</a>')
|
||||
|
||||
# Email tag
|
||||
elif tag == 'email':
|
||||
if ('args' not in token) and (index+2 < tokenLength):
|
||||
# [email]asdf@asdf.com]
|
||||
htmlList.append (bbcode2html.xml ('a', { 'href' : u'mailto:' + tokens[index+1].strip() }))
|
||||
else:
|
||||
# [email=asdf@asfd.com]john smith[/email]
|
||||
if 'href' in tokenArgs:
|
||||
tokenArgs['href'] = u'mailto:' + tokenArgs['href']
|
||||
htmlList.append (bbcode2html.xml ('a', tokenArgs))
|
||||
|
||||
elif tagName == 'list':
|
||||
if tagOpener == '/':
|
||||
htmlList.append (bbcode2html.xml (u'/' + lastListOpener.pop()))
|
||||
else:
|
||||
if ('type' not in tokenArgs):
|
||||
htmlList.append (bbcode2html.xml (tagOpener + u'ul', tokenArgs))
|
||||
lastListOpener.append ('ul')
|
||||
else:
|
||||
htmlList.append (bbcode2html.xml (tagOpener + u'ol', tokenArgs))
|
||||
lastListOpener.append ('ol')
|
||||
|
||||
elif tagName == '*':
|
||||
htmlList.append (bbcode2html.xml (tagOpener + u'li', tokenArgs))
|
||||
|
||||
elif (tagName == 's'):
|
||||
tokenArgs['style'] = 'text-decoration: line-through;'
|
||||
htmlList.append (bbcode2html.xml (tagOpener + u'span', tokenArgs))
|
||||
|
||||
elif (tagName == 'code'):
|
||||
htmlList.append (bbcode2html.xml (tagOpener + u'pre', tokenArgs))
|
||||
|
||||
elif (tagName == 'quote'):
|
||||
htmlList.append (bbcode2html.xml (tagOpener + u'blockquote', tokenArgs))
|
||||
|
||||
elif (tagName == 'google'):
|
||||
htmlList.append (
|
||||
bbcode2html.xml (
|
||||
tagOpener + u'a',
|
||||
{'href' : 'http://www.google.com/search?q=' + urllib.quote_plus (tokens[index+1])},
|
||||
tokens[index+1]
|
||||
)
|
||||
)
|
||||
index += 2
|
||||
|
||||
elif (tagName == 'wikipedia'):
|
||||
subdomain = 'www'
|
||||
for arg in ['lang', 'language', 'wikipedia']:
|
||||
if arg in tokenArgs:
|
||||
subdomain = tokenArgs[arg]
|
||||
|
||||
htmlList.append (
|
||||
bbcode2html.xml (
|
||||
tagOpener + u'a',
|
||||
{'href' : 'http://' + subdomain + '.wikipedia.org/wiki/' + tokens[index+1].replace (' ', '_')},
|
||||
tokens[index+1]
|
||||
)
|
||||
)
|
||||
index += 2
|
||||
|
||||
elif (tagName in validTags):
|
||||
htmlList.append (
|
||||
bbcode2html.xml (tag, tokenArgs)
|
||||
)
|
||||
|
||||
else:
|
||||
# ignore this tag
|
||||
pass
|
||||
|
||||
index += 1
|
||||
|
||||
return ''.join (htmlList)
|
||||
|
||||
@staticmethod
|
||||
def _filterInvalidTagsAndAttributes (tokens, validTags, allowClassAttr):
|
||||
'''
|
||||
Helper function to filter out invalid attributes from the tokens list
|
||||
'''
|
||||
# add 'class' attribute as valid (mapping 'class' itself)
|
||||
if allowClassAttr:
|
||||
for attr in validTags:
|
||||
validTags[attr]['class'] = 'class'
|
||||
|
||||
# remove invalid attributes from tokens
|
||||
for tindex in range(0, len(tokens)):
|
||||
if isinstance (tokens[tindex], dict) and ('args' in tokens[tindex]) and (tokens[tindex]['tag'] in validTags):
|
||||
validList = validTags[tokens[tindex]['tag']]
|
||||
|
||||
filteredArgs = {}
|
||||
for arg in tokens[tindex]['args']:
|
||||
if arg in validList:
|
||||
# rename the argument
|
||||
filteredArgs[validList[arg]] = tokens[tindex]['args'][arg]
|
||||
else:
|
||||
pass # do not include this arg in the filteredArgs
|
||||
|
||||
tokens[tindex]['args'] = filteredArgs
|
||||
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def xml (tag, attrs = {}, text = None, soloTag = False):
|
||||
'''
|
||||
Helper function to produce valid XML output
|
||||
'''
|
||||
xml = u'<' + tag.lower()
|
||||
|
||||
# make sure we sort attributes alphabetically (for deterministic output)
|
||||
# Faster but non-deterministic:
|
||||
# for (key, value) in attrs.iteritems():
|
||||
# xml += u' ' + key + u'="' + value + u'"'
|
||||
for key in sorted (attrs.keys()):
|
||||
xml += u' ' + key + u'="' + attrs[key] + u'"'
|
||||
|
||||
# close tag
|
||||
if text is None:
|
||||
if soloTag:
|
||||
xml += u' />'
|
||||
else:
|
||||
xml += u'>'
|
||||
else:
|
||||
xml += u'>' + text + u'</' + tag.lower() + '>'
|
||||
|
||||
return xml
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/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]"
|
||||
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
#!/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
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
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]
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
#
|
||||
# Author: Pau Sanchez (contact@pausanchez.com)
|
||||
# Version: v1.0
|
||||
# Last Modified: 2010/09/15
|
||||
#
|
||||
# For the latest version check out:
|
||||
# http://www.codigomanso.com/en/projects
|
||||
#
|
||||
# My blog:
|
||||
# http://www.codigomanso.com/en/ - English Version
|
||||
# http://www.codigomanso.com/es/ - Spanish Version
|
||||
#
|
||||
|
||||
from bbcodeparser import bbcodeparser
|
||||
from bbcodebuilder import bbcodebuilder
|
||||
|
||||
import random
|
||||
import unittest
|
||||
|
||||
class BBCodeTests(unittest.TestCase):
|
||||
def setUp (self):
|
||||
self.bbcode = bbcodeparser()
|
||||
return
|
||||
|
||||
def testConstructor (self):
|
||||
self.assertEqual (bbcodeparser ('whatever').html(), 'whatever')
|
||||
self.assertEqual (bbcodeparser ('[b]bold[/b]').html(), '<b>bold</b>')
|
||||
self.assertEqual (str (bbcodeparser ('[b]bold[/b]')), '[b]bold[/b]')
|
||||
return
|
||||
|
||||
def testBold (self):
|
||||
self.assertEqual (self.bbcode.parse ('whatever').html(), 'whatever')
|
||||
self.assertEqual (self.bbcode.parse ('[b]bold[/b]').html(), '<b>bold</b>')
|
||||
self.assertEqual (self.bbcode.parse ('[B]bold[/b]').html(), '<b>bold</b>')
|
||||
self.assertEqual (self.bbcode.parse ('this is [B]bold[/B]').html(), 'this is <b>bold</b>')
|
||||
return
|
||||
|
||||
def testItalic (self):
|
||||
self.assertEqual (self.bbcode.parse ('[i]italic[/i]').html(), '<i>italic</i>')
|
||||
return
|
||||
|
||||
def testUnderline (self):
|
||||
self.assertEqual (self.bbcode.parse ('[u]italic[/u]').html(), '<u>italic</u>')
|
||||
return
|
||||
|
||||
def testURLs (self):
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[url]http://www.google.com[/url]').html(),
|
||||
'<a href="http://www.google.com">http://www.google.com</a>'
|
||||
)
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[url=http://www.google.com]Google[/url]').html(),
|
||||
'<a href="http://www.google.com">Google</a>'
|
||||
)
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[url="http://www.google.com"]Google[/url]').html(),
|
||||
'<a href="http://www.google.com">Google</a>'
|
||||
)
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[url="http://www.google.com" title="Search Engine"]Google[/url]').html(),
|
||||
'<a href="http://www.google.com" title="Search Engine">Google</a>'
|
||||
)
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[url link="http://www.google.com"]Google[/url]').html(),
|
||||
'<a href="http://www.google.com">Google</a>'
|
||||
)
|
||||
return
|
||||
|
||||
def testPTag (self):
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[p color=#0000ff]blue[/p]').html(),
|
||||
u'<p style="color: #0000ff;">blue</p>'
|
||||
)
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[p size=12]12pt font[/p]').html(),
|
||||
u'<p style="font-size: 12pt;">12pt font</p>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[p font=arial]arial[/p]').html(),
|
||||
u'<p style="font-family: arial;">arial</p>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[p font=arial color=blue size=14]blue 14pt arial').html(),
|
||||
u'<p style="color: blue; font-size: 14pt; font-family: arial;">blue 14pt arial</p>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[p class=whatever]text[/p]').html(),
|
||||
u'<p>text</p>'
|
||||
)
|
||||
return
|
||||
|
||||
def testColorTag (self):
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[color=#0000ff]blue[/color]').html(),
|
||||
u'<span style="color: #0000ff;">blue</span>'
|
||||
)
|
||||
return
|
||||
|
||||
def testSizeTag (self):
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[size=12]12pt font[/size]').html(),
|
||||
u'<span style="font-size: 12pt;">12pt font</span>'
|
||||
)
|
||||
return
|
||||
|
||||
def testEmail(self):
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[email]asdf@asdf.com[/email]').html(),
|
||||
u'<a href="mailto:asdf@asdf.com">asdf@asdf.com</a>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[email=john@smith.com]John Smith[/email]').html(),
|
||||
u'<a href="mailto:john@smith.com">John Smith</a>'
|
||||
)
|
||||
return
|
||||
|
||||
def testImgTag (self):
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[img]http://www.codigomanso.com/image.jpg[/img]').html(),
|
||||
u'<img src="http://www.codigomanso.com/image.jpg" />'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[img="This is the ALT of the image"]http://www.codigomanso.com/image.jpg[/img]').html(),
|
||||
u'<img alt="This is the ALT of the image" src="http://www.codigomanso.com/image.jpg" />'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[img=320x200]http://www.codigomanso.com/image.jpg[/img]').html(),
|
||||
u'<img height="200" src="http://www.codigomanso.com/image.jpg" width="320" />'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[img=320x200 title="Image Test"]http://www.codigomanso.com/image.jpg[/img]').html(),
|
||||
u'<img height="200" src="http://www.codigomanso.com/image.jpg" title="Image Test" width="320" />'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[img="whatever" width=320 height="212" title="Image Test"]http://www.codigomanso.com/image.jpg[/img]').html(),
|
||||
u'<img alt="whatever" height="212" src="http://www.codigomanso.com/image.jpg" title="Image Test" width="320" />'
|
||||
)
|
||||
return
|
||||
|
||||
def testGoogleURL (self):
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[google]asdf[/google]').html(),
|
||||
u'<a href="http://www.google.com/search?q=asdf">asdf</a>'
|
||||
)
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[google]Tom Hanks[/google]').html(),
|
||||
u'<a href="http://www.google.com/search?q=Tom+Hanks">Tom Hanks</a>'
|
||||
)
|
||||
return
|
||||
|
||||
def testWikipediaURL (self):
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[wikipedia]Tom Hanks[/wikipedia]').html(),
|
||||
u'<a href="http://www.wikipedia.org/wiki/Tom_Hanks">Tom Hanks</a>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[wikipedia language=en]Tom Hanks[/wikipedia]').html(),
|
||||
u'<a href="http://en.wikipedia.org/wiki/Tom_Hanks">Tom Hanks</a>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[wikipedia lang=es]Tom Hanks[/wikipedia]').html(),
|
||||
u'<a href="http://es.wikipedia.org/wiki/Tom_Hanks">Tom Hanks</a>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[wikipedia=es]Tom Hanks[/wikipedia]').html(),
|
||||
u'<a href="http://es.wikipedia.org/wiki/Tom_Hanks">Tom Hanks</a>'
|
||||
)
|
||||
return
|
||||
|
||||
def testListTags (self):
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[ul][li]item 1[/li][li]item 2[/li][/ul]').html(),
|
||||
u'<ul><li>item 1</li><li>item 2</li></ul>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[ol][li]item 1[/li][li]item 2[/li][/ol]').html(),
|
||||
u'<ol><li>item 1</li><li>item 2</li></ol>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[list][li]item 1[/li][li]item 2[/li][/list]').html(),
|
||||
u'<ul><li>item 1</li><li>item 2</li></ul>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[list][*]item 1[*]item 2[/list]').html(),
|
||||
u'<ul><li>item 1</li><li>item 2</li></ul>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[list=1][li]item 1[/li][li]item 2[/li][/list]').html(),
|
||||
u'<ol type="1"><li>item 1</li><li>item 2</li></ol>'
|
||||
)
|
||||
return
|
||||
|
||||
def testInvalidCode (self):
|
||||
self.assertEqual (self.bbcode.parse ('[invalid]valid text[/invalid]').html(), 'valid text')
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[b]bold and [i]italics[/b]').html(),
|
||||
'<b>bold and <i>italics</i></b>'
|
||||
)
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[/b]invalid[/b][/p]').html(),
|
||||
'invalid'
|
||||
)
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[p][b]bold').html(),
|
||||
'<p><b>bold</b></p>'
|
||||
)
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[p][b]a <b>').html(),
|
||||
'<p><b>a <b></b></p>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[ol][li]item 1[li]item 2[/li][/ol]').html(),
|
||||
u'<ol><li>item 1</li><li>item 2</li></ol>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[b]\[b\] stands for [b]bold[/b]').html(),
|
||||
u'<b>[b] stands for </b><b>bold</b>'
|
||||
)
|
||||
return
|
||||
|
||||
def testEscapedBrackets (self):
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('\[b\]not bold\[/b\]').html(),
|
||||
u'[b]not bold[/b]'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('[b]\[b\] stands for bold[/b]').html(),
|
||||
u'<b>[b] stands for bold</b>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('\[b\][b]stands for bold[/b]').html(),
|
||||
u'[b]<b>stands for bold</b>'
|
||||
)
|
||||
|
||||
self.assertEqual (
|
||||
self.bbcode.parse ('\[b\][b]stands for bold[/b] just like <b> in HTML').html(),
|
||||
u'[b]<b>stands for bold</b> just like <b> in HTML'
|
||||
)
|
||||
|
||||
def testBigExample (self):
|
||||
inputText = """check this out
|
||||
|
||||
[h1 class=circle]heading[/h1]
|
||||
|
||||
[p size=14 color=blue font="verdana, Times New Roman"]This is [b] bold [/b] and this [i]italic[/i] and this is [color=red]red[/color] and this is [color="red"]also red[/color].
|
||||
[/p]
|
||||
|
||||
fix [b][i]bold [font=verdana][size=12]and[/size][/font] italic[/b]
|
||||
[img]http://www.codigomanso.com/b.jpg[/img]
|
||||
[url]http://www.codigomanso.com/[/url]
|
||||
[url=http://www.codigomanso.com/]Codigo Manso[/url]
|
||||
[uRl link=http://www.codigomanso.com title="Codigo Manso Blog"]Codigo Manso[/url]
|
||||
|
||||
[ul]
|
||||
[Li]item 1[/Li]
|
||||
[li]item 2[/LI]
|
||||
[/UL]
|
||||
|
||||
[list=1 ]
|
||||
[*]item 1
|
||||
[*]item 2
|
||||
[/list]
|
||||
|
||||
[table class="big"]
|
||||
[tr]
|
||||
[th]big[/th]
|
||||
[/tr]
|
||||
[/table]
|
||||
[invalid class="extra"]whatever[/invalid]"""
|
||||
|
||||
out = self.bbcode.parse (inputText).html(allowClassAttr = True)
|
||||
self.assertEquals (out, '''check this out
|
||||
|
||||
<h1 class="circle">heading</h1>
|
||||
|
||||
<p style="color: blue; font-size: 14pt; font-family: verdana, Times New Roman;">This is <b> bold </b> and this <i>italic</i> and this is <span style="color: red;">red</span> and this is <span style="color: red;">also red</span>.
|
||||
</p>
|
||||
|
||||
fix <b><i>bold <span style="font-family: verdana;"><span style="font-size: 12pt;">and</span></span> italic</i></b>
|
||||
<img src="http://www.codigomanso.com/b.jpg" />
|
||||
<a href="http://www.codigomanso.com/">http://www.codigomanso.com/</a>
|
||||
<a href="http://www.codigomanso.com/">Codigo Manso</a>
|
||||
<a href="http://www.codigomanso.com" title="Codigo Manso Blog">Codigo Manso</a>
|
||||
|
||||
<ul>
|
||||
<li>item 1</li>
|
||||
<li>item 2</li>
|
||||
</ul>
|
||||
|
||||
<ol type="1">
|
||||
<li>item 1
|
||||
</li><li>item 2
|
||||
</li></ol>
|
||||
|
||||
<table class="big">
|
||||
<tr>
|
||||
<th>big</th>
|
||||
</tr>
|
||||
</table>
|
||||
whatever''')
|
||||
|
||||
|
||||
def testBBCodeDumper (self):
|
||||
self.assertEquals (
|
||||
self.bbcode.parse ('[b]bold[/b]').bbcode(),
|
||||
'[b]bold[/b]'
|
||||
)
|
||||
|
||||
self.assertEquals (
|
||||
self.bbcode.parse ('[color=red]text in red[/color]').bbcode(),
|
||||
'[color=red]text in red[/color]'
|
||||
)
|
||||
self.assertEquals (
|
||||
self.bbcode.parse ('[p][color=red]text in red').bbcode(),
|
||||
'[p][color=red]text in red[/color][/p]'
|
||||
)
|
||||
|
||||
self.assertEquals (
|
||||
self.bbcode.parse ('This [b][i]code[/b] will be fixed[/invalid]').bbcode(),
|
||||
'This [b][i]code[/i][/b] will be fixed'
|
||||
)
|
||||
|
||||
self.assertEquals (
|
||||
self.bbcode.parse ('\[[url]http://www.codigomanso.com/en[/url]\]').bbcode(),
|
||||
"\[[url]http://www.codigomanso.com/en[/url]\]"
|
||||
)
|
||||
|
||||
def performanceTest(self):
|
||||
'''
|
||||
This test checks the performance of parse and html operations
|
||||
|
||||
To run this test type:
|
||||
> python test.py BBCodeTests.performanceTest
|
||||
'''
|
||||
inputText = """check this out
|
||||
|
||||
[h1 class=circle]heading[/h1]
|
||||
|
||||
[p size=14 color=blue font="verdana, Times New Roman"]This is [b] bold [/b] and this [i]italic[/i] and this is [color=red]red[/color] and this is [color="red"]also red[/color].
|
||||
[/p]
|
||||
|
||||
fix [b][i]bold [font=verdana][size=12]and[/size][/font] italic[/b]
|
||||
[img]http://www.codigomanso.com/b.jpg[/img]
|
||||
[url]http://www.codigomanso.com/[/url]
|
||||
[url=http://www.codigomanso.com/]Codigo Manso[/url]
|
||||
[uRl link=http://www.codigomanso.com title="Codigo Manso Blog"]Codigo Manso[/url]
|
||||
|
||||
[ul]
|
||||
[Li]item 1[/Li]
|
||||
[li]item 2[/LI]
|
||||
[/UL]
|
||||
|
||||
[list=1 ]
|
||||
[*]item 1
|
||||
[*]item 2
|
||||
[/list]
|
||||
|
||||
[table class="big"]
|
||||
[tr]
|
||||
[th]big[/th]
|
||||
[/tr]
|
||||
[/table]
|
||||
[invalid class="extra"]whatever[/invalid]"""
|
||||
|
||||
import time
|
||||
start = time.time()
|
||||
|
||||
for i in range(0, 12):
|
||||
inputText += inputText
|
||||
|
||||
print "len(inputText) = %.2f MB (took %.2f seconds)" % (len(inputText)/(1024.0*1024.0), time.time() - start)
|
||||
|
||||
bbcode = bbcodeparser()
|
||||
start = time.time()
|
||||
bbcode.parse (inputText)
|
||||
total = (time.time() - start)
|
||||
print "time (bbcode.parse()) = %f" % total
|
||||
print " >> %.2f chars/second" % (len(inputText) / total)
|
||||
|
||||
start = time.time()
|
||||
bbcode.html(doDeepCopy = False)
|
||||
total = (time.time() - start)
|
||||
print "time (bbcode.html()) = %f" % total
|
||||
print " >> %.2f chars/second" % (len(inputText) / total)
|
||||
return
|
||||
|
||||
def testCodeBuilder (self):
|
||||
bbcode = bbcodebuilder ()
|
||||
self.assertEquals (bbcode.b ('bold'), u'[b]bold[/b]')
|
||||
self.assertEquals (bbcode.color ('this goes in red', 'red'), u'[color=red]this goes in red[/color]')
|
||||
self.assertEquals (bbcode.url ('Google', 'http://www.google.com'), u'[url=http://www.google.com]Google[/url]')
|
||||
self.assertEquals (bbcode.alist('item 1', 'item 2'), u"[list=a]\n [*]item 1\n [*]item 2\n[/list]")
|
||||
return
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user