mirror of
https://github.com/wassname/FanFicFare.git
synced 2026-09-15 11:36:30 +08:00
Compare commits
181
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d52d2f2438 | ||
|
|
e47e4bf29a | ||
|
|
445d676d24 | ||
|
|
ef71577b73 | ||
|
|
951fd68ce6 | ||
|
|
e740166ba4 | ||
|
|
54616e9892 | ||
|
|
3fbfa5b56c | ||
|
|
a606db85d1 | ||
|
|
15abab181f | ||
|
|
d81b365aba | ||
|
|
6e897c78f1 | ||
|
|
52ccebf16e | ||
|
|
a0e9123c58 | ||
|
|
6e93ded2a3 | ||
|
|
d95b96b9c4 | ||
|
|
d14b100d7e | ||
|
|
66f9d4f7e0 | ||
|
|
cd342bb352 | ||
|
|
ae7ffcfc32 | ||
|
|
863ee5c44b | ||
|
|
8d094fc26e | ||
|
|
ee85c13e75 | ||
|
|
367bea316b | ||
|
|
e5168d1d98 | ||
|
|
ff4559a8ad | ||
|
|
3cfa3179c9 | ||
|
|
e3970a64de | ||
|
|
2320e118b3 | ||
|
|
53d76052a8 | ||
|
|
9471a74527 | ||
|
|
7ba9290c7d | ||
|
|
64f60b4540 | ||
|
|
2adbcdc23e | ||
|
|
d3ab5e2024 | ||
|
|
635170f664 | ||
|
|
5ac90d3cdb | ||
|
|
394b21ab0e | ||
|
|
df6599a9cc | ||
|
|
d380f8b05c | ||
|
|
cf1ecee8e9 | ||
|
|
5a85524629 | ||
|
|
65e6bce0bc | ||
|
|
12161a8224 | ||
|
|
19d181a90f | ||
|
|
159d33f287 | ||
|
|
bbd806ab95 | ||
|
|
a4f82bf841 | ||
|
|
389b658135 | ||
|
|
7bcd4143e5 | ||
|
|
e9f010a162 | ||
|
|
babfc35f7b | ||
|
|
cefcb9ab96 | ||
|
|
7a763a8516 | ||
|
|
a191521649 | ||
|
|
e108c2d828 | ||
|
|
7534c03a37 | ||
|
|
bf2e71e17f | ||
|
|
110960169a | ||
|
|
fb9d128687 | ||
|
|
e99c3d6ea6 | ||
|
|
1172322446 | ||
|
|
2f4ce5c40e | ||
|
|
73c7ffeff6 | ||
|
|
0a78a0c044 | ||
|
|
dc5adb7f4a | ||
|
|
4abfbdf462 | ||
|
|
d1b73b9a6a | ||
|
|
e477a9870d | ||
|
|
2dbbb0f13d | ||
|
|
3d1d3f4e26 | ||
|
|
7a5d77975a | ||
|
|
c3911a279b | ||
|
|
8cf6f210e6 | ||
|
|
539426b41d | ||
|
|
78efbb3e1e | ||
|
|
4878837805 | ||
|
|
8347f4490e | ||
|
|
360d37746d | ||
|
|
8af36f298e | ||
|
|
831370134b | ||
|
|
de37c4aa1d | ||
|
|
2cb139147a | ||
|
|
9ce6117688 | ||
|
|
2e38ef1122 | ||
|
|
4c4576f331 | ||
|
|
9aa75905c6 | ||
|
|
76823dccfb | ||
|
|
0d0778fea5 | ||
|
|
5823d335a4 | ||
|
|
db3878668b | ||
|
|
02289c0af1 | ||
|
|
7a840043f0 | ||
|
|
0c3ccb4e7c | ||
|
|
e8904ec061 | ||
|
|
a589cf4280 | ||
|
|
cf11959970 | ||
|
|
49777c299e | ||
|
|
4acffb88f6 | ||
|
|
6e38557454 | ||
|
|
f24c363d3b | ||
|
|
e7ea699bc9 | ||
|
|
9de65d94f3 | ||
|
|
da7498d202 | ||
|
|
05ec7bce2b | ||
|
|
241c4d8d52 | ||
|
|
ccf4c8cc4e | ||
|
|
7498a9aa93 | ||
|
|
25c63c3a47 | ||
|
|
c5e5a9bb84 | ||
|
|
9bc36c3652 | ||
|
|
0736c35be0 | ||
|
|
cd4f2c2717 | ||
|
|
e6bb8c557b | ||
|
|
f4da7dc1bd | ||
|
|
eba312e777 | ||
|
|
0cd615d950 | ||
|
|
4b120bb2d3 | ||
|
|
c9bcf9175e | ||
|
|
c76facd40c | ||
|
|
f1d52834d1 | ||
|
|
e8ac7f8a89 | ||
|
|
3e0f92d8ce | ||
|
|
2682d0fe36 | ||
|
|
125e29091f | ||
|
|
d0c73d5444 | ||
|
|
bd8e54edcf | ||
|
|
d680a86f0c | ||
|
|
f075ae582d | ||
|
|
928ebb9751 | ||
|
|
ce262be162 | ||
|
|
6faa6850af | ||
|
|
f392c6dd77 | ||
|
|
af0dff28b4 | ||
|
|
02a75a821f | ||
|
|
c06028b498 | ||
|
|
de4b95af9b | ||
|
|
5366355d96 | ||
|
|
264853d768 | ||
|
|
d82b399738 | ||
|
|
59446d23dc | ||
|
|
6fcbdd8a8d | ||
|
|
1348525d25 | ||
|
|
40bac62c5e | ||
|
|
362f15f9fa | ||
|
|
b833041dc4 | ||
|
|
fa67220b86 | ||
|
|
4f412eb89f | ||
|
|
6a4aa4340e | ||
|
|
dc785b911e | ||
|
|
d403f916a9 | ||
|
|
29fe1a6e24 | ||
|
|
131a08c0dc | ||
|
|
fbd26c16e0 | ||
|
|
34ebba40d0 | ||
|
|
7ce8436208 | ||
|
|
b247e4fc7b | ||
|
|
bf509bb6ee | ||
|
|
8b4f1953fe | ||
|
|
cf7e913e60 | ||
|
|
402366a3e5 | ||
|
|
bc0ef37fe6 | ||
|
|
3b77a6b1c1 | ||
|
|
f603762261 | ||
|
|
094f864252 | ||
|
|
34e67ebb26 | ||
|
|
7dae1140a7 | ||
|
|
c4b1810887 | ||
|
|
e9d3ef2b9a | ||
|
|
04895411b1 | ||
|
|
e8706b19fd | ||
|
|
dc86c90b9e | ||
|
|
b90399d674 | ||
|
|
7c13b5aed9 | ||
|
|
8d87564158 | ||
|
|
1663f13950 | ||
|
|
10aedfbce6 | ||
|
|
d5dd328a05 | ||
|
|
d08bb7ac21 | ||
|
|
6687601074 | ||
|
|
4847a2b1b4 |
@@ -1,6 +1,6 @@
|
||||
# ffd-retief-hrd fanfictiondownloader
|
||||
application: fanfictiondownloader
|
||||
version: 4-4-94
|
||||
version: 2-0-03
|
||||
runtime: python27
|
||||
api_version: 1
|
||||
threadsafe: true
|
||||
|
||||
@@ -42,7 +42,7 @@ class FanFictionDownLoaderBase(InterfaceActionBase):
|
||||
description = _('UI plugin to download FanFiction stories from various sites.')
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
author = 'Jim Miller'
|
||||
version = (1, 8, 13)
|
||||
version = (2, 0, 3)
|
||||
minimum_calibre_version = (1, 13, 0)
|
||||
|
||||
#: This field defines the GUI plugin class that contains all the code
|
||||
|
||||
@@ -8,12 +8,19 @@ __copyright__ = '2011, Grant Drake <grant.drake@gmail.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4.Qt import (Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout,
|
||||
QTableWidgetItem, QFont, QLineEdit, QComboBox,
|
||||
QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime,
|
||||
QTextEdit,
|
||||
QListWidget, QAbstractItemView)
|
||||
try:
|
||||
from PyQt5 import QtWidgets as QtGui
|
||||
from PyQt5.Qt import (Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout,
|
||||
QTableWidgetItem, QFont, QLineEdit, QComboBox,
|
||||
QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime,
|
||||
QTextEdit, QListWidget, QAbstractItemView)
|
||||
except ImportError as e:
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4.Qt import (Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout,
|
||||
QTableWidgetItem, QFont, QLineEdit, QComboBox,
|
||||
QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime,
|
||||
QTextEdit, QListWidget, QAbstractItemView)
|
||||
|
||||
from calibre.constants import iswindows
|
||||
from calibre.gui2 import gprefs, error_dialog, UNDEFINED_QDATETIME, info_dialog
|
||||
from calibre.gui2.actions import menu_action_unique_name
|
||||
|
||||
+48
-21
@@ -4,7 +4,7 @@ from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2012, Jim Miller'
|
||||
__copyright__ = '2014, Jim Miller'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import logging
|
||||
@@ -13,10 +13,31 @@ logger = logging.getLogger(__name__)
|
||||
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,
|
||||
QDialogButtonBox, QGroupBox )
|
||||
try:
|
||||
from PyQt5.Qt import (QDialog, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QLineEdit, QFont, QWidget, QTextEdit, QComboBox,
|
||||
QCheckBox, QPushButton, QTabWidget, QScrollArea,
|
||||
QDialogButtonBox, QGroupBox )
|
||||
except ImportError as e:
|
||||
from PyQt4.Qt import (QDialog, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QLineEdit, QFont, QWidget, QTextEdit, QComboBox,
|
||||
QCheckBox, QPushButton, QTabWidget, QScrollArea,
|
||||
QDialogButtonBox, QGroupBox )
|
||||
try:
|
||||
from calibre.gui2 import QVariant
|
||||
del QVariant
|
||||
except ImportError:
|
||||
is_qt4 = False
|
||||
convert_qvariant = lambda x: x
|
||||
else:
|
||||
is_qt4 = True
|
||||
def convert_qvariant(x):
|
||||
vt = x.type()
|
||||
if vt == x.String:
|
||||
return unicode(x.toString())
|
||||
if vt == x.List:
|
||||
return [convert_qvariant(i) for i in x.toList()]
|
||||
return x.toPyObject()
|
||||
|
||||
from calibre.gui2.ui import get_gui
|
||||
from calibre.gui2 import dynamic, info_dialog
|
||||
@@ -60,7 +81,7 @@ from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.adapters \
|
||||
from calibre_plugins.fanfictiondownloader_plugin.common_utils \
|
||||
import ( KeyboardConfigDialog, PrefsViewerDialog )
|
||||
|
||||
from calibre.gui2.complete import MultiCompleteLineEdit
|
||||
from calibre.gui2.complete2 import EditWithComplete #MultiCompleteLineEdit
|
||||
|
||||
class RejectURLList:
|
||||
def __init__(self,prefs):
|
||||
@@ -220,6 +241,7 @@ class ConfigWidget(QWidget):
|
||||
prefs['checkforurlchange'] = self.basic_tab.checkforurlchange.isChecked()
|
||||
prefs['injectseries'] = self.basic_tab.injectseries.isChecked()
|
||||
prefs['smarten_punctuation'] = self.basic_tab.smarten_punctuation.isChecked()
|
||||
prefs['reject_always'] = self.basic_tab.reject_always.isChecked()
|
||||
|
||||
if self.readinglist_tab:
|
||||
# lists
|
||||
@@ -243,7 +265,7 @@ class ConfigWidget(QWidget):
|
||||
prefs['gcnewonly'] = self.generatecover_tab.gcnewonly.isChecked()
|
||||
gc_site_settings = {}
|
||||
for (site,combo) in self.generatecover_tab.gc_dropdowns.iteritems():
|
||||
val = unicode(combo.itemData(combo.currentIndex()).toString())
|
||||
val = unicode(convert_qvariant(combo.itemData(combo.currentIndex())))
|
||||
if val != 'none':
|
||||
gc_site_settings[site] = val
|
||||
#print("gc_site_settings[%s]:%s"%(site,gc_site_settings[site]))
|
||||
@@ -275,12 +297,12 @@ class ConfigWidget(QWidget):
|
||||
|
||||
# Custom Columns tab
|
||||
# error column
|
||||
prefs['errorcol'] = unicode(self.cust_columns_tab.errorcol.itemData(self.cust_columns_tab.errorcol.currentIndex()).toString())
|
||||
prefs['errorcol'] = unicode(convert_qvariant(self.cust_columns_tab.errorcol.itemData(self.cust_columns_tab.errorcol.currentIndex())))
|
||||
|
||||
# cust cols tab
|
||||
colsmap = {}
|
||||
for (col,combo) in self.cust_columns_tab.custcol_dropdowns.iteritems():
|
||||
val = unicode(combo.itemData(combo.currentIndex()).toString())
|
||||
val = unicode(convert_qvariant(combo.itemData(combo.currentIndex())))
|
||||
if val != 'none':
|
||||
colsmap[col] = val
|
||||
#print("colsmap[%s]:%s"%(col,colsmap[col]))
|
||||
@@ -482,6 +504,11 @@ class BasicTab(QWidget):
|
||||
self.reject_reasons.clicked.connect(self.show_reject_reasons)
|
||||
self.l.addWidget(self.reject_reasons)
|
||||
|
||||
self.reject_always = QCheckBox(_('Reject Without Confirmation?'),self)
|
||||
self.reject_always.setToolTip(_("Always reject URLs on the Reject List without stopping and asking."))
|
||||
self.reject_always.setChecked(prefs['reject_always'])
|
||||
self.l.addWidget(self.reject_always)
|
||||
|
||||
topl.addWidget(defs_gb)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
@@ -649,7 +676,7 @@ class ReadingListTab(QWidget):
|
||||
label = QLabel(_('"Send to Device" Reading Lists'))
|
||||
label.setToolTip(_("When enabled, new/updated stories will be automatically added to these lists."))
|
||||
horz.addWidget(label)
|
||||
self.send_lists_box = MultiCompleteLineEdit(self)
|
||||
self.send_lists_box = EditWithComplete(self)
|
||||
self.send_lists_box.setToolTip(_("When enabled, new/updated stories will be automatically added to these lists."))
|
||||
self.send_lists_box.update_items_cache(reading_lists)
|
||||
self.send_lists_box.setText(prefs['send_lists'])
|
||||
@@ -665,7 +692,7 @@ class ReadingListTab(QWidget):
|
||||
label = QLabel(_('"To Read" Reading Lists'))
|
||||
label.setToolTip(_("When enabled, new/updated stories will be automatically added to these lists."))
|
||||
horz.addWidget(label)
|
||||
self.read_lists_box = MultiCompleteLineEdit(self)
|
||||
self.read_lists_box = EditWithComplete(self)
|
||||
self.read_lists_box.setToolTip(_("When enabled, new/updated stories will be automatically added to these lists."))
|
||||
self.read_lists_box.update_items_cache(reading_lists)
|
||||
self.read_lists_box.setText(prefs['read_lists'])
|
||||
@@ -727,17 +754,17 @@ class GenerateCoverTab(QWidget):
|
||||
horz.addWidget(label)
|
||||
dropdown = QComboBox(self)
|
||||
dropdown.setToolTip(s)
|
||||
dropdown.addItem('',QVariant('none'))
|
||||
dropdown.addItem('','none')
|
||||
for setting in gc_settings:
|
||||
dropdown.addItem(setting,QVariant(setting))
|
||||
dropdown.addItem(setting,setting)
|
||||
if site == _("Default"):
|
||||
self.gc_dropdowns["Default"] = dropdown
|
||||
if 'Default' in prefs['gc_site_settings']:
|
||||
dropdown.setCurrentIndex(dropdown.findData(QVariant(prefs['gc_site_settings']['Default'])))
|
||||
dropdown.setCurrentIndex(dropdown.findData(prefs['gc_site_settings']['Default']))
|
||||
else:
|
||||
self.gc_dropdowns[site] = dropdown
|
||||
if site in prefs['gc_site_settings']:
|
||||
dropdown.setCurrentIndex(dropdown.findData(QVariant(prefs['gc_site_settings'][site])))
|
||||
dropdown.setCurrentIndex(dropdown.findData(prefs['gc_site_settings'][site]))
|
||||
|
||||
horz.addWidget(dropdown)
|
||||
self.sl.addLayout(horz)
|
||||
@@ -966,12 +993,12 @@ class CustomColumnsTab(QWidget):
|
||||
label.setToolTip(_("Update this %s column(%s) with...")%(key,column['datatype']))
|
||||
horz.addWidget(label)
|
||||
dropdown = QComboBox(self)
|
||||
dropdown.addItem('',QVariant('none'))
|
||||
dropdown.addItem('','none')
|
||||
for md in permitted_values[column['datatype']]:
|
||||
dropdown.addItem(titleLabels[md],QVariant(md))
|
||||
dropdown.addItem(titleLabels[md],md)
|
||||
self.custcol_dropdowns[key] = dropdown
|
||||
if key in prefs['custom_cols']:
|
||||
dropdown.setCurrentIndex(dropdown.findData(QVariant(prefs['custom_cols'][key])))
|
||||
dropdown.setCurrentIndex(dropdown.findData(prefs['custom_cols'][key]))
|
||||
if column['datatype'] == 'enumeration':
|
||||
dropdown.setToolTip(_("Metadata values valid for this type of column.")+"\n"+_("Values that aren't valid for this enumeration column will be ignored."))
|
||||
else:
|
||||
@@ -1007,11 +1034,11 @@ class CustomColumnsTab(QWidget):
|
||||
horz.addWidget(label)
|
||||
self.errorcol = QComboBox(self)
|
||||
self.errorcol.setToolTip(tooltip)
|
||||
self.errorcol.addItem('',QVariant('none'))
|
||||
self.errorcol.addItem('','none')
|
||||
for key, column in custom_columns.iteritems():
|
||||
if column['datatype'] in ('text','comments'):
|
||||
self.errorcol.addItem(column['name'],QVariant(key))
|
||||
self.errorcol.setCurrentIndex(self.errorcol.findData(QVariant(prefs['errorcol'])))
|
||||
self.errorcol.addItem(column['name'],key)
|
||||
self.errorcol.setCurrentIndex(self.errorcol.findData(prefs['errorcol']))
|
||||
horz.addWidget(self.errorcol)
|
||||
self.l.addLayout(horz)
|
||||
|
||||
|
||||
+41
-40
@@ -19,12 +19,36 @@ logger = logging.getLogger(__name__)
|
||||
import urllib
|
||||
import email
|
||||
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4.Qt import (QDialog, QTableWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
|
||||
QPushButton, QString, QLabel, QCheckBox, QIcon, QLineEdit,
|
||||
QComboBox, QVariant, QProgressDialog, QTimer, QDialogButtonBox,
|
||||
QPixmap, Qt, QAbstractItemView, SIGNAL, QTextEdit, pyqtSignal,
|
||||
QGroupBox, QFrame)
|
||||
try:
|
||||
from PyQt5 import QtWidgets as QtGui
|
||||
from PyQt5.Qt import (QDialog, QTableWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
|
||||
QPushButton, QLabel, QCheckBox, QIcon, QLineEdit,
|
||||
QComboBox, QProgressDialog, QTimer, QDialogButtonBox,
|
||||
QPixmap, Qt, QAbstractItemView, QTextEdit, pyqtSignal,
|
||||
QGroupBox, QFrame)
|
||||
except ImportError as e:
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4.Qt import (QDialog, QTableWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
|
||||
QPushButton, QLabel, QCheckBox, QIcon, QLineEdit,
|
||||
QComboBox, QProgressDialog, QTimer, QDialogButtonBox,
|
||||
QPixmap, Qt, QAbstractItemView, QTextEdit, pyqtSignal,
|
||||
QGroupBox, QFrame)
|
||||
|
||||
try:
|
||||
from calibre.gui2 import QVariant
|
||||
del QVariant
|
||||
except ImportError:
|
||||
is_qt4 = False
|
||||
convert_qvariant = lambda x: x
|
||||
else:
|
||||
is_qt4 = True
|
||||
def convert_qvariant(x):
|
||||
vt = x.type()
|
||||
if vt == x.String:
|
||||
return unicode(x.toString())
|
||||
if vt == x.List:
|
||||
return [convert_qvariant(i) for i in x.toList()]
|
||||
return x.toPyObject()
|
||||
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
from calibre.gui2.complete2 import EditWithComplete
|
||||
@@ -146,19 +170,6 @@ class RejectUrlEntry:
|
||||
|
||||
return retval
|
||||
|
||||
# This is a more than slightly kludgey way to get
|
||||
# EditWithComplete to *not* alpha-order the reasons, but leave
|
||||
# them in the order entered. If
|
||||
# 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()
|
||||
|
||||
class NotGoingToDownload(Exception):
|
||||
def __init__(self,error,icon='dialog_error.png'):
|
||||
self.error=error
|
||||
@@ -196,9 +207,9 @@ class DroppableQTextEdit(QTextEdit):
|
||||
urllist.extend(get_urls_from_text(part.get_payload(decode=True)))
|
||||
else:
|
||||
urllist.extend(get_urls_from_text("%s"%msg))
|
||||
|
||||
if urllist:
|
||||
self.append("\n".join(urllist))
|
||||
return None
|
||||
return QTextEdit.dropEvent(self,event)
|
||||
|
||||
def canInsertFromMimeData(self, source):
|
||||
@@ -559,7 +570,7 @@ class LoopProgressDialog(QProgressDialog):
|
||||
status_prefix=_("Fetched metadata for")):
|
||||
QProgressDialog.__init__(self,
|
||||
init_label,
|
||||
QString(), 0, len(book_list), gui)
|
||||
_('Cancel'), 0, len(book_list), gui)
|
||||
self.setWindowTitle(win_title)
|
||||
self.setMinimumWidth(500)
|
||||
self.book_list = book_list
|
||||
@@ -829,11 +840,11 @@ class StoryListTableWidget(QTableWidget):
|
||||
icon = get_icon(book['icon'])
|
||||
|
||||
status_cell = IconWidgetItem(None,icon,val)
|
||||
status_cell.setData(Qt.UserRole, QVariant(val))
|
||||
status_cell.setData(Qt.UserRole, val)
|
||||
self.setItem(row, 0, status_cell)
|
||||
|
||||
title_cell = ReadOnlyTableWidgetItem(book['title'])
|
||||
title_cell.setData(Qt.UserRole, QVariant(row))
|
||||
title_cell.setData(Qt.UserRole, row)
|
||||
self.setItem(row, 1, title_cell)
|
||||
|
||||
self.setItem(row, 2, AuthorTableWidgetItem(", ".join(book['author']), ", ".join(book['author_sort'])))
|
||||
@@ -848,7 +859,7 @@ class StoryListTableWidget(QTableWidget):
|
||||
books = []
|
||||
#print("=========================\nbooks:%s"%self.books)
|
||||
for row in range(self.rowCount()):
|
||||
rnum = self.item(row, 1).data(Qt.UserRole).toPyObject()
|
||||
rnum = convert_qvariant(self.item(row, 1).data(Qt.UserRole))
|
||||
book = self.books[rnum]
|
||||
books.append(book)
|
||||
return books
|
||||
@@ -914,15 +925,12 @@ class RejectListTableWidget(QTableWidget):
|
||||
def populate_table_row(self, row, rej):
|
||||
|
||||
url_cell = ReadOnlyTableWidgetItem(rej.url)
|
||||
url_cell.setData(Qt.UserRole, QVariant(rej.book_id))
|
||||
url_cell.setData(Qt.UserRole, rej.book_id)
|
||||
self.setItem(row, 0, url_cell)
|
||||
self.setItem(row, 1, ReadOnlyTableWidgetItem(rej.title))
|
||||
self.setItem(row, 2, ReadOnlyTableWidgetItem(rej.auth))
|
||||
|
||||
note_cell = EditWithComplete(self)
|
||||
note_cell.lineEdit().mcompleter.model().set_items = \
|
||||
partial(complete_model_set_items_kludge,
|
||||
note_cell.lineEdit().mcompleter.model())
|
||||
note_cell = EditWithComplete(self,sort_func=lambda x:1)
|
||||
|
||||
items = [rej.note]+self.rejectreasons
|
||||
note_cell.update_items_cache(items)
|
||||
@@ -992,10 +1000,7 @@ class RejectListDialog(SizePersistedDialog):
|
||||
button_layout.addItem(spacerItem1)
|
||||
|
||||
if show_all_reasons:
|
||||
self.reason_edit = EditWithComplete(self)
|
||||
self.reason_edit.lineEdit().mcompleter.model().set_items = \
|
||||
partial(complete_model_set_items_kludge,
|
||||
self.reason_edit.lineEdit().mcompleter.model())
|
||||
self.reason_edit = EditWithComplete(self,sort_func=lambda x:1)
|
||||
|
||||
items = ['']+rejectreasons
|
||||
self.reason_edit.update_items_cache(items)
|
||||
@@ -1037,7 +1042,7 @@ class RejectListDialog(SizePersistedDialog):
|
||||
rejectrows = []
|
||||
for row in range(self.rejects_table.rowCount()):
|
||||
url = unicode(self.rejects_table.item(row, 0).text()).strip()
|
||||
book_id = self.rejects_table.item(row, 0).data(Qt.UserRole).toPyObject()
|
||||
book_id =convert_qvariant(self.rejects_table.item(row, 0).data(Qt.UserRole))
|
||||
title = unicode(self.rejects_table.item(row, 1).text()).strip()
|
||||
auth = unicode(self.rejects_table.item(row, 2).text()).strip()
|
||||
note = unicode(self.rejects_table.cellWidget(row, 3).currentText()).strip()
|
||||
@@ -1047,7 +1052,7 @@ class RejectListDialog(SizePersistedDialog):
|
||||
def get_reject_list_ids(self):
|
||||
rejectrows = []
|
||||
for row in range(self.rejects_table.rowCount()):
|
||||
book_id = self.rejects_table.item(row, 0).data(Qt.UserRole).toPyObject()
|
||||
book_id = convert_qvariant(self.rejects_table.item(row, 0).data(Qt.UserRole))
|
||||
if book_id:
|
||||
rejectrows.append(book_id)
|
||||
return rejectrows
|
||||
@@ -1089,11 +1094,7 @@ class EditTextDialog(QDialog):
|
||||
self.textedit.setToolTip(tooltip)
|
||||
|
||||
if rejectreasons or reasonslabel:
|
||||
self.reason_edit = EditWithComplete(self)
|
||||
|
||||
self.reason_edit.lineEdit().mcompleter.model().set_items = \
|
||||
partial(complete_model_set_items_kludge,
|
||||
self.reason_edit.lineEdit().mcompleter.model())
|
||||
self.reason_edit = EditWithComplete(self,sort_func=lambda x:1)
|
||||
|
||||
items = ['']+rejectreasons
|
||||
self.reason_edit.update_items_cache(items)
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2012, Jim Miller'
|
||||
__copyright__ = '2014, Jim Miller'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import logging
|
||||
@@ -19,10 +19,12 @@ import urllib
|
||||
import email
|
||||
import traceback
|
||||
|
||||
from PyQt4.Qt import (QApplication, QMenu, QToolButton, QTimer)
|
||||
|
||||
from PyQt4.Qt import QPixmap, Qt
|
||||
from PyQt4.QtCore import QBuffer
|
||||
try:
|
||||
from PyQt5.Qt import (QApplication, QMenu, QTimer)
|
||||
from PyQt5.QtCore import QBuffer
|
||||
except ImportError as e:
|
||||
from PyQt4.Qt import (QApplication, QMenu, QTimer)
|
||||
from PyQt4.QtCore import QBuffer
|
||||
|
||||
from calibre.constants import numeric_version as calibre_version
|
||||
|
||||
@@ -32,6 +34,7 @@ from calibre.ebooks.metadata.meta import get_metadata
|
||||
from calibre.gui2 import error_dialog, warning_dialog, question_dialog, info_dialog
|
||||
from calibre.gui2.dialogs.message_box import ViewLog
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
from calibre.utils.config import prefs as calibre_prefs
|
||||
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
|
||||
@@ -206,7 +209,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
#print("text/plain:%s"%event.mimeData().data(mimetype))
|
||||
urllist.extend(get_urls_from_text(event.mimeData().data(mimetype)))
|
||||
|
||||
#print("urllist:%s\ndropped_ids:%s"%(urllist,dropped_ids))
|
||||
# print("urllist:%s\ndropped_ids:%s"%(urllist,dropped_ids))
|
||||
if urllist or dropped_ids:
|
||||
QTimer.singleShot(1, partial(self.do_drop,
|
||||
dropped_ids=dropped_ids,
|
||||
@@ -384,6 +387,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
|
||||
def get_urls_from_page_menu(self):
|
||||
|
||||
urltxt = ""
|
||||
if prefs['urlsfromclip']:
|
||||
try:
|
||||
urltxt = self.get_urls_clip(storyurls=False)[0]
|
||||
@@ -711,6 +715,11 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
# No need to do anything with perfs here, but we could.
|
||||
prefs
|
||||
|
||||
def make_id_searchstr(self,url):
|
||||
# older idents can be uri vs url and have | instead of : after
|
||||
# http, plus many sites are now switching to https.
|
||||
return 'identifiers:"~ur(i|l):~^%s$"'%re.sub(r'https?\\\:','https?(\:|\|)',re.escape(url))
|
||||
|
||||
def prep_downloads(self, options, books, merge=False, extrapayload=None):
|
||||
'''Fetch metadata for stories from servers, launch BG job when done.'''
|
||||
|
||||
@@ -763,7 +772,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
if not merge: # skip reject list when merging.
|
||||
if rejecturllist.check(url):
|
||||
rejnote = rejecturllist.get_full_note(url)
|
||||
if question_dialog(self.gui, _('Reject URL?'),'''
|
||||
if prefs['reject_always'] or question_dialog(self.gui, _('Reject URL?'),'''
|
||||
<h3>%s</h3>
|
||||
<p>%s</p>
|
||||
<p>"<b>%s</b>"</p>
|
||||
@@ -856,7 +865,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
series = story.getMetadata('series')
|
||||
if not merge and series and prefs['checkforseriesurlid']:
|
||||
# try to find *series anthology* by *seriesUrl* identifier url or uri first.
|
||||
searchstr = 'identifiers:"~ur(i|l):~^%s$"'%re.sub(r'https?\:','https?(\:|\|)',re.escape(story.getMetadata('seriesUrl')))
|
||||
searchstr = self.make_id_searchstr(story.getMetadata('seriesUrl'))
|
||||
identicalbooks = db.search_getting_ids(searchstr, None)
|
||||
# print("searchstr:%s"%searchstr)
|
||||
# print("identicalbooks:%s"%identicalbooks)
|
||||
@@ -888,7 +897,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
|
||||
# all_metadata duplicates some data, but also includes extra_entries, etc.
|
||||
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")
|
||||
@@ -933,20 +942,13 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
logger.debug("from URL(%s)"%url)
|
||||
|
||||
# try to find by identifier url or uri first.
|
||||
searchstr = 'identifiers:"~ur(i|l):~^%s$"'%re.sub(r'https?\:','https?(\:|\|)',url)
|
||||
searchstr = self.make_id_searchstr(url)
|
||||
identicalbooks = db.search_getting_ids(searchstr, None)
|
||||
# print("searchstr:%s"%searchstr)
|
||||
# print("identicalbooks:%s"%identicalbooks)
|
||||
if len(identicalbooks) < 1:
|
||||
# find dups
|
||||
authlist = story.getList("author", removeallentities=True)
|
||||
if len(authlist) > 100 and calibre_version < (0, 8, 61):
|
||||
## should be fixed from 0.8.61 on. In the
|
||||
## meantime, if it matches the title *and* first
|
||||
## 100 authors, I'm prepared to assume it's a
|
||||
## match.
|
||||
logger.debug("reduce author list to 100 only when calibre < 0.8.61")
|
||||
authlist = authlist[:100]
|
||||
mi = MetaInformation(story.getMetadata("title", removeallentities=True),
|
||||
authlist)
|
||||
identicalbooks = db.find_identical_books(mi)
|
||||
@@ -976,7 +978,8 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
if book_id and mi: # book_id and mi only set if matched by title/author.
|
||||
liburl = self.get_story_url(db,book_id)
|
||||
if book['url'] != liburl and prefs['checkforurlchange'] and \
|
||||
not (book['url'].replace('https','http') == liburl and 'fanfiction.net' in liburl):
|
||||
not (book['url'].replace('https','http') == liburl): # several sites have been changing to
|
||||
# https now. Don't flag when that's the only change.
|
||||
# special case for ffnet urls change to https.
|
||||
if not question_dialog(self.gui, _('Change Story URL?'),'''
|
||||
<h3>%s</h3>
|
||||
@@ -1456,8 +1459,18 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
fmts = db.formats(book['calibre_id'], index_is_id=True).split(',')
|
||||
for fmt in fmts:
|
||||
if fmt != formmapping[options['fileform']]:
|
||||
logger.debug("remove f:"+fmt)
|
||||
logger.debug("deleteotherforms remove f:"+fmt)
|
||||
db.remove_format(book['calibre_id'], fmt, index_is_id=True)#, notify=False
|
||||
elif prefs['autoconvert']:
|
||||
## 'Convert Book'.auto_convert_auto_add doesn't convert if
|
||||
## the format is already there.
|
||||
fmt = calibre_prefs['output_format']
|
||||
# delete if there, but not if the format we just made.
|
||||
if fmt != formmapping[options['fileform']] and \
|
||||
db.has_format(book_id,fmt,index_is_id=True):
|
||||
logger.debug("autoconvert remove f:"+fmt)
|
||||
db.remove_format(book['calibre_id'], fmt, index_is_id=True)#, notify=False
|
||||
|
||||
|
||||
return book_id
|
||||
|
||||
@@ -1612,7 +1625,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
pass
|
||||
|
||||
if val:
|
||||
vallist = [val]
|
||||
vallist.append(val)
|
||||
|
||||
db.set_custom(book_id, ", ".join(vallist), label=label, commit=False)
|
||||
|
||||
@@ -1621,25 +1634,18 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
if 'authorUrl' in book['all_metadata']:
|
||||
authurls = book['all_metadata']['authorUrl'].split(", ")
|
||||
authorlist = [ a.replace('&',';') for a in book['author'] ]
|
||||
if hasattr(db, 'new_api'): # new_api starts in calibre 1.0.0
|
||||
authorids = db.new_api.get_item_ids('authors',authorlist)
|
||||
authordata = db.new_api.author_data(authorids.values())
|
||||
# print("\n\nauthorids:%s"%authorids)
|
||||
# print("authordata:%s"%authordata)
|
||||
authorids = db.new_api.get_item_ids('authors',authorlist)
|
||||
authordata = db.new_api.author_data(authorids.values())
|
||||
# print("\n\nauthorids:%s"%authorids)
|
||||
# print("authordata:%s"%authordata)
|
||||
|
||||
author_id_to_link_map = dict()
|
||||
for i, author in enumerate(authorlist):
|
||||
author_id_to_link_map[authorids[author]] = authurls[i]
|
||||
author_id_to_link_map = dict()
|
||||
for i, author in enumerate(authorlist):
|
||||
author_id_to_link_map[authorids[author]] = authurls[i]
|
||||
|
||||
# print("author_id_to_link_map:%s\n\n"%author_id_to_link_map)
|
||||
db.new_api.set_link_for_authors(author_id_to_link_map)
|
||||
else:
|
||||
# keep for pre-calibre 1.0.0
|
||||
for i, auth in enumerate(authorlist):
|
||||
#print("===Update author url for %s to %s"%(auth,authurls[i]))
|
||||
autid=db.get_author_id(auth)
|
||||
db.set_link_field_for_author(autid, unicode(authurls[i]),
|
||||
commit=False, notify=False)
|
||||
# print("author_id_to_link_map:%s\n\n"%author_id_to_link_map)
|
||||
db.new_api.set_link_for_authors(author_id_to_link_map)
|
||||
|
||||
db.commit()
|
||||
|
||||
if 'Generate Cover' in self.gui.iactions and (book['added'] or not prefs['gcnewonly']):
|
||||
@@ -1688,7 +1694,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
gc_plugin.generate_cover_for_book(realmi,saved_setting_name=setting_name)
|
||||
|
||||
if prefs['gc_polish_cover'] and \
|
||||
options['fileform'] == "epub" and calibre_version >= (0, 9, 39):
|
||||
options['fileform'] == "epub":
|
||||
# set cover inside epub from calibre's polish feature
|
||||
from calibre.ebooks.oeb.polish.main import polish, ALL_OPTS
|
||||
from calibre.utils.logging import Log
|
||||
@@ -1799,6 +1805,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
book['url'] = ''
|
||||
book['site'] = ''
|
||||
book['added'] = False
|
||||
book['pubdate'] = None
|
||||
return book
|
||||
|
||||
def convert_urls_to_books(self, urls):
|
||||
@@ -1964,9 +1971,9 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
|
||||
# fill from first of each if not already present:
|
||||
for k in ('pubdate', 'timestamp', 'updatedate'):
|
||||
if k not in b: # not in this book? Skip it.
|
||||
if k not in b or not b[k]: # not in this book? Skip it.
|
||||
continue
|
||||
if k not in book: # first is good enough for publisher.
|
||||
if k not in book or not book[k]: # first is good enough for publisher.
|
||||
book[k]=b[k]
|
||||
|
||||
# Do these even on first to get the all_metadata settings.
|
||||
@@ -1987,11 +1994,12 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
for (k,v) in b['all_metadata'].iteritems():
|
||||
#print("merge_meta_books v:%s k:%s"%(v,k))
|
||||
if k in ('numChapters','numWords'):
|
||||
if k not in book['all_metadata']:
|
||||
book['all_metadata'][k] = b['all_metadata'][k]
|
||||
else:
|
||||
# lot of work for a simple add.
|
||||
book['all_metadata'][k] = unicode(int(book['all_metadata'][k].replace(',',''))+int(b['all_metadata'][k].replace(',','')))
|
||||
if k in b['all_metadata'] and b['all_metadata'][k]:
|
||||
if k not in book['all_metadata']:
|
||||
book['all_metadata'][k] = b['all_metadata'][k]
|
||||
else:
|
||||
# lot of work for a simple add.
|
||||
book['all_metadata'][k] = unicode(int(book['all_metadata'][k].replace(',',''))+int(b['all_metadata'][k].replace(',','')))
|
||||
elif k in ('dateUpdated','datePublished','dateCreated',
|
||||
'series','status','title'):
|
||||
pass # handled above, below or skip these for now, not going to do anything with them.
|
||||
@@ -2006,14 +2014,20 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
# cust cols can convert back to numbers and
|
||||
# add.
|
||||
book['anthology_meta_list'][k]=True
|
||||
|
||||
|
||||
print("book['url']:%s"%book['url'])
|
||||
configuration = get_ffdl_config(book['url'],fileform)
|
||||
if existingbook:
|
||||
book['title'] = deftitle = existingbook['title']
|
||||
book['comments'] = existingbook['comments']
|
||||
else:
|
||||
book['title'] = deftitle = book_list[0]['title']
|
||||
book['comments'] = _("Anthology containing:")+"\n" + \
|
||||
"\n".join([ _("%s by %s")%(b['title'],', '.join(b['author'])) for b in book_list ])
|
||||
if len(book['author']) > 1:
|
||||
book['comments'] = _("Anthology containing:")+"\n" + \
|
||||
"\n".join([ _("%s by %s")%(b['title'],', '.join(b['author'])) for b in book_list ])
|
||||
else:
|
||||
book['comments'] = _("Anthology containing:")+"\n" + \
|
||||
"\n".join([ b['title'] for b in book_list ])
|
||||
# book['all_metadata']['description']
|
||||
|
||||
# if all same series, use series for name. But only if all and not previous named
|
||||
@@ -2025,7 +2039,6 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
book['title'] = deftitle
|
||||
break
|
||||
|
||||
configuration = get_ffdl_config(book['url'],fileform)
|
||||
logger.debug("anthology_title_pattern:%s"%configuration.getConfig('anthology_title_pattern'))
|
||||
if configuration.getConfig('anthology_title_pattern'):
|
||||
tmplt = Template(configuration.getConfig('anthology_title_pattern'))
|
||||
@@ -2041,7 +2054,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
for v in ['Completed','In-Progress']:
|
||||
if v in book['tags']:
|
||||
book['tags'].remove(v)
|
||||
book['tags'].append('Anthology')
|
||||
book['tags'].extend(configuration.getConfigList('anthology_tags'))
|
||||
book['all_metadata']['anthology'] = "true"
|
||||
|
||||
return book
|
||||
|
||||
+7
-10
@@ -4,7 +4,7 @@ from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2012, Jim Miller'
|
||||
__copyright__ = '2014, Jim Miller'
|
||||
__copyright__ = '2011, Grant Drake <grant.drake@gmail.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
@@ -93,16 +93,13 @@ def do_download_worker(book_list, options,
|
||||
logger.info('Logfile for book ID %s (%s)'%(book_id, job._book['title']))
|
||||
logger.info(job.details)
|
||||
|
||||
|
||||
|
||||
if count >= total:
|
||||
# All done! Output some lists for convenience of some users.
|
||||
logger.info("Successfully downloaded:")
|
||||
for book in book_list:
|
||||
if book['good']:
|
||||
logger.info("%s %s"%(book['title'],book['url']))
|
||||
logger.info("\nUnsuccessful:")
|
||||
for book in book_list:
|
||||
if not book['good']:
|
||||
logger.info("%s %s"%(book['title'],book['url']))
|
||||
logger.info("\nSuccessful:\n%s\n"%("\n".join([book['url'] for book in
|
||||
filter(lambda x: x['good'], book_list) ] ) ) )
|
||||
logger.info("\nUnsuccessful:\n%s\n"%("\n".join([book['url'] for book in
|
||||
filter(lambda x: not x['good'], book_list) ] ) ) )
|
||||
break
|
||||
|
||||
server.close()
|
||||
|
||||
@@ -25,6 +25,7 @@ default_prefs['rejecturls'] = ''
|
||||
default_prefs['rejectreasons'] = '''Sucked
|
||||
Boring
|
||||
Dup from another site'''
|
||||
default_prefs['reject_always'] = False
|
||||
|
||||
default_prefs['updatemeta'] = True
|
||||
default_prefs['updatecover'] = False
|
||||
|
||||
+516
-782
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+503
-765
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+363
-355
File diff suppressed because it is too large
Load Diff
+237
-41
@@ -182,6 +182,11 @@ extratags: FanFiction
|
||||
## useful if pulling large numbers of stories or if the site is slow.
|
||||
#slow_down_sleep_time:0.5
|
||||
|
||||
## How long to wait for each HTTP connection to finish. Longer times
|
||||
## are better for sites that are slow to respond. Shorter times
|
||||
## prevent excessive wait when your network or the site is down.
|
||||
connect_timeout:60.0
|
||||
|
||||
## For use only with stand-alone CLI version--run a command on the
|
||||
## generated file after it's produced. All of the titlepage_entries
|
||||
## values are available, plus output_filename.
|
||||
@@ -214,7 +219,32 @@ extratags: FanFiction
|
||||
# .*-Centered=>
|
||||
# characters=>Sam W\.=>Sam Witwicky&&category=>Transformers
|
||||
# characters=>Sam W\.=>Sam Winchester&&category=>Supernatural
|
||||
|
||||
|
||||
## Include/Exclude metadata
|
||||
##
|
||||
## You can use the include/exclude metadata features to either limit
|
||||
## the values of particular metadata lists to specific values or to
|
||||
## exclude specific values. Further, you can conditionally apply each
|
||||
## line depending on other metadata, use exact strings or regular
|
||||
## expressions(regex) to match values, and negate matches.
|
||||
##
|
||||
## The settings are:
|
||||
## include_metadata_pre
|
||||
## exclude_metadata_pre
|
||||
## include_metadata_post
|
||||
## exclude_metadata_post
|
||||
##
|
||||
## The form of each line is:
|
||||
## metakey[,metakey]==exactvalue
|
||||
## metakey[,metakey]=~regex
|
||||
## metakey[,metakey]==exactvalue&&conditionalkey==exactcondvalue
|
||||
## metakey[,metakey]=~regex&&conditionalkey==exactcondvalue
|
||||
## metakey[,metakey]==exactvalue&&conditionalkey=~condregex
|
||||
##
|
||||
## This is fairly complicated, so it's documented on its own wiki
|
||||
## page:
|
||||
## https://code.google.com/p/fanficdownloader/wiki/InExcludeMetadataFeature
|
||||
|
||||
## Some readers don't show horizontal rule (<hr />) tags correctly.
|
||||
## This replaces them all with a centered '* * *'. (Note centering
|
||||
## doesn't work on some devices either.)
|
||||
@@ -568,6 +598,12 @@ include_in_freefromtags:freeformtags
|
||||
## adds to include_subject_tags instead of replacing it.
|
||||
#extra_subject_tags:fandoms,freeformtags,ao3categories
|
||||
|
||||
## AO3 chapters can include several different types of notes. We've
|
||||
## traditional included them all in the chapter text, but this allows
|
||||
## you to customize which you include. Copy this parameter to your
|
||||
## personal.ini and list the ones you don't want.
|
||||
#exclude_notes:authorheadnotes,chaptersummary,chapterheadnotes,chapterfootnotes,authorfootnotes
|
||||
|
||||
[ashwinder.sycophanthex.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -590,6 +626,41 @@ extracategories:The Sentinel
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[bdsm-geschichten.net]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## This site offers no index page so we can either guess the chapter URLs
|
||||
## by dec/incrementing numbers ('guess') or walk all the chapters in the metadata
|
||||
## parsing state ('parse'). Since guessing can lead to errors for non-standard
|
||||
## story URLs, the default is to parse
|
||||
#find_chapters:guess
|
||||
|
||||
[bloodshedverse.com]
|
||||
## website encoding(s) In theory, each website reports the character
|
||||
## encoding they use for each page. In practice, some sites report it
|
||||
## incorrectly. Each adapter has a default list, usually "utf8,
|
||||
## Windows-1252" or "Windows-1252, utf8", but this will let you
|
||||
## explicitly set the encoding and order if you need to. The special
|
||||
## value 'auto' will call chardet and use the encoding it reports if
|
||||
## it has +90% confidence. 'auto' is not reliable.
|
||||
website_encodings:Windows-1252,ISO-8859-1,auto
|
||||
|
||||
## Extra metadata that this adapter knows about. See [dramione.org]
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:warnings,reviews
|
||||
reviews_label:Reviews
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracharacters:Spike,Buffy
|
||||
extracategories:Buffy the Vampire Slayer
|
||||
|
||||
## Strips links found in the story text
|
||||
## Specific to bloodshedverse.com
|
||||
strip_text_links:true
|
||||
|
||||
[bloodties-fans.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -731,6 +802,38 @@ extracategories:Harry Potter
|
||||
## cover image. This lets you exclude them.
|
||||
cover_exclusion_regexp:/images/.*?ribbon.gif
|
||||
|
||||
[fanfiction.csodaidok.hu]
|
||||
## website encoding(s) In theory, each website reports the character
|
||||
## encoding they use for each page. In practice, some sites report it
|
||||
## incorrectly. Each adapter has a default list, usually "utf8,
|
||||
## Windows-1252" or "Windows-1252, utf8", but this will let you
|
||||
## explicitly set the encoding and order if you need to. The special
|
||||
## value 'auto' will call chardet and use the encoding it reports if
|
||||
## it has +90% confidence. 'auto' is not reliable.
|
||||
website_encodings:ISO-8859-2,auto
|
||||
|
||||
## Extra metadata that this adapter knows about. See [dramione.org]
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:reviews,challenge
|
||||
reviews_label:Reviews
|
||||
challenge_label:Challenge
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
|
||||
[fanfic.hu]
|
||||
## website encoding(s) In theory, each website reports the character
|
||||
## encoding they use for each page. In practice, some sites report it
|
||||
## incorrectly. Each adapter has a default list, usually "utf8,
|
||||
## Windows-1252" or "Windows-1252, utf8", but this will let you
|
||||
## explicitly set the encoding and order if you need to. The special
|
||||
## value 'auto' will call chardet and use the encoding it reports if
|
||||
## it has +90% confidence. 'auto' is not reliable.
|
||||
website_encodings:ISO-8859-1,auto
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
|
||||
[fanfiction.mugglenet.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
@@ -781,6 +884,53 @@ extraships:Harry Potter/Hermione Granger
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
[ficwad.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
[fictionmania.tv]
|
||||
## website encoding(s) In theory, each website reports the character
|
||||
## encoding they use for each page. In practice, some sites report it
|
||||
## incorrectly. Each adapter has a default list, usually "utf8,
|
||||
## Windows-1252" or "Windows-1252, utf8", but this will let you
|
||||
## explicitly set the encoding and order if you need to. The special
|
||||
## value 'auto' will call chardet and use the encoding it reports if
|
||||
## it has +90% confidence. 'auto' is not reliable.
|
||||
website_encodings:ISO-8859-1,auto
|
||||
|
||||
## items to include in the log page Empty metadata entries, or those
|
||||
## that haven't changed since the last update, will *not* appear, even
|
||||
## if in the list. You can include extra text or HTML that will be
|
||||
## included as-is in each log entry. Eg: logpage_entries: ...,<br />,
|
||||
## summary,<br />,...
|
||||
## Don't include numChapters since all stories are a single "chapter", there's
|
||||
## no way to reliably find the next chapter
|
||||
logpage_entries: dateCreated,datePublished,dateUpdated,numChapters,numWords,status,series,title,author,description,category,genre,rating,warnings
|
||||
|
||||
## items to include in the title page
|
||||
## Empty metadata entries will *not* appear, even if in the list.
|
||||
## You can include extra text or HTML that will be included as-is in
|
||||
## the title page. Eg: titlepage_entries: ...,<br />,summary,<br />,...
|
||||
## All current formats already include title and author.
|
||||
## Don't include numChapters since all stories are a single "chapter", there's
|
||||
## no way to reliably find the next chapter
|
||||
titlepage_entries: seriesHTML,category,genre,language,characters,ships,status,datePublished,dateUpdated,dateCreated,rating,warnings,numWords,site,description
|
||||
|
||||
## Extra metadata that this adapter knows about. See [dramione.org]
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:fileName,fileSize,oldName,newName,keyWords,mainCharactersAge,readings
|
||||
|
||||
## Turns all space characters into " " HTML entities to forcefully preserve
|
||||
## formatting with spaces. Enabling this will blow up the filesize quite a bit
|
||||
## and is probably not a good idea, unless you absolutely need the story
|
||||
## formatting.
|
||||
## Specific to fictionmania.tv
|
||||
non_breaking_spaces:false
|
||||
|
||||
[fictionpad.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -806,36 +956,6 @@ dislikes_label:Dislikes
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
[storiesonline.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
## Clear FanFiction from defaults, site is original fiction.
|
||||
extratags:
|
||||
|
||||
extra_valid_entries:size,universe,universeUrl,universeHTML,codes,notice
|
||||
#extra_titlepage_entries:size,universeHTML,codes,notice
|
||||
|
||||
size_label:Size
|
||||
universe_label:Universe
|
||||
universeUrl_label:Universe URL
|
||||
universeHTML_label:Universe
|
||||
codes_label:Codes
|
||||
notice_label:Notice
|
||||
|
||||
## Assume entryUrl, apply to "<a class='%slink' href='%s'>%s</a>" to
|
||||
## make entryHTML.
|
||||
make_linkhtml_entries:universe
|
||||
|
||||
## storiesonline.net stories can be in a series or a universe, but not
|
||||
## both. By default, universe will be populated in 'series' with
|
||||
## index=0
|
||||
universe_as_series: true
|
||||
|
||||
[grangerenchanted.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -888,6 +1008,11 @@ extracategories:Star Trek
|
||||
extracharacters:Kirk,Spock
|
||||
extraships:Kirk/Spock
|
||||
|
||||
[literotica.com]
|
||||
extra_valid_entries:eroticatags
|
||||
eroticatags_label:Erotica Tags
|
||||
extra_titlepage_entries: eroticatags
|
||||
|
||||
[lumos.sycophanthex.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
@@ -937,6 +1062,17 @@ extracategories:NCIS
|
||||
extracategories:Buffy: The Vampire Slayer
|
||||
extracharacters:Willow
|
||||
|
||||
[nocturnal-light.net]
|
||||
## Extra metadata that this adapter knows about. See [dramione.org]
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:readings,reviews
|
||||
readings_label:Readings
|
||||
reviews_label:Reviews
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracharacters:Spike,Buffy
|
||||
extracategories:Buffy the Vampire Slayer
|
||||
|
||||
[occlumency.sycophanthex.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -1027,10 +1163,50 @@ extracategories:Harry Potter
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[spikeluver.com]
|
||||
## Extra metadata that this adapter knows about. See [dramione.org]
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:warnings,reviews
|
||||
reviews_label:Reviews
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracharacters:Spike,Buffy
|
||||
extracategories:Buffy the Vampire Slayer
|
||||
|
||||
[stargate-atlantis.org]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Stargate: Atlantis
|
||||
|
||||
[storiesonline.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
## Clear FanFiction from defaults, site is original fiction.
|
||||
extratags:
|
||||
|
||||
extra_valid_entries:size,universe,universeUrl,universeHTML,codes,notice
|
||||
#extra_titlepage_entries:size,universeHTML,codes,notice
|
||||
|
||||
size_label:Size
|
||||
universe_label:Universe
|
||||
universeUrl_label:Universe URL
|
||||
universeHTML_label:Universe
|
||||
codes_label:Codes
|
||||
notice_label:Notice
|
||||
|
||||
## Assume entryUrl, apply to "<a class='%slink' href='%s'>%s</a>" to
|
||||
## make entryHTML.
|
||||
make_linkhtml_entries:universe
|
||||
|
||||
## storiesonline.net stories can be in a series or a universe, but not
|
||||
## both. By default, universe will be populated in 'series' with
|
||||
## index=0
|
||||
universe_as_series: true
|
||||
|
||||
[svufiction.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -1066,6 +1242,12 @@ extraships:Harry Potter/Draco Malfoy
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Criminal Minds
|
||||
|
||||
[themaplebookshelf.com]
|
||||
## Some sites also require the user to confirm they are adult for
|
||||
## adult content. In commandline version, this should go in your
|
||||
## personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[themasque.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -1104,6 +1286,10 @@ extracategories:Harry Potter
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Stargate: SG-1
|
||||
|
||||
[tolkienfanfiction.com]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Lord of the Rings
|
||||
|
||||
[trekiverse.org]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -1125,6 +1311,13 @@ awards_label:Awards
|
||||
|
||||
cover_exclusion_regexp:art/.*Awards.jpg
|
||||
|
||||
[voracity2.e-fic.com]
|
||||
## Extra metadata that this adapter knows about. See [dramione.org]
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:reviews,readings
|
||||
reviews_label:Reviews
|
||||
readings_label:Readings
|
||||
|
||||
[www.adastrafanfic.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
@@ -1212,8 +1405,11 @@ type_label:Type of Couple
|
||||
[www.fanfiction.net]
|
||||
user_agent:
|
||||
## fanfiction.net's 'cover' images are really just tiny thumbnails.
|
||||
## Change this to false to use them anyway.
|
||||
never_make_cover: true
|
||||
## Set this to true to never use them.
|
||||
#never_make_cover: false
|
||||
|
||||
## fanfiction.net shows the user's
|
||||
cover_exclusion_regexp:/imageu/
|
||||
|
||||
## fanfiction.net is blocking people more aggressively. If you
|
||||
## download fewer stories less often you can likely get by with
|
||||
@@ -1277,14 +1473,6 @@ extratags:
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:reviews,favs,follows
|
||||
|
||||
[ficwad.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
[www.fimfiction.net]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
@@ -1303,6 +1491,11 @@ extra_valid_entries:reviews,favs,follows
|
||||
## when updating to enforce accurate chapters.
|
||||
#do_update_hook:false
|
||||
|
||||
## fimfiction.net is reported to misinterprete some BBCode with
|
||||
## blockquotes incorrectly. This fixes those instances and defaults
|
||||
## to on, but can be switched off if it is found to cause problems.
|
||||
fix_fimf_blockquotes:true
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:My Little Pony: Friendship is Magic
|
||||
|
||||
@@ -1640,6 +1833,9 @@ extracharacters:Wolverine,Rogue
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Stargate: Atlantis
|
||||
|
||||
extra_valid_entries:reviews
|
||||
reviews_label:Reviews
|
||||
|
||||
[overrides]
|
||||
## It may sometimes be useful to override all of the specific format,
|
||||
## site and site:format sections in your private configuration. For
|
||||
|
||||
+20
-21
@@ -23,6 +23,7 @@ import getpass
|
||||
import string
|
||||
import ConfigParser
|
||||
from subprocess import call
|
||||
import pprint
|
||||
|
||||
import logging
|
||||
if sys.version_info >= (2, 7):
|
||||
@@ -226,9 +227,9 @@ def main(argv,
|
||||
except:
|
||||
options.update = False
|
||||
pass
|
||||
|
||||
## Check for include_images and absence of PIL, give warning.
|
||||
if adapter.getConfig('include_images'):
|
||||
|
||||
## Check for include_images without no_image_processing. In absence of PIL, give warning.
|
||||
if adapter.getConfig('include_images') and not adapter.getConfig('no_image_processing'):
|
||||
try:
|
||||
from calibre.utils.magick import Image
|
||||
logging.debug("Using calibre.utils.magick")
|
||||
@@ -270,31 +271,29 @@ def main(argv,
|
||||
elif chaptercount == 0:
|
||||
print "%s doesn't contain any recognizable chapters, probably from a different source. Not updating." % (output_filename)
|
||||
else:
|
||||
if not options.metaonly:
|
||||
# update now handled by pre-populating the old
|
||||
# images and chapters in the adapter rather than
|
||||
# merging epubs.
|
||||
(url,
|
||||
chaptercount,
|
||||
adapter.oldchapters,
|
||||
adapter.oldimgs,
|
||||
adapter.oldcover,
|
||||
adapter.calibrebookmark,
|
||||
adapter.logfile) = get_update_data(output_filename)
|
||||
|
||||
# update now handled by pre-populating the old
|
||||
# images and chapters in the adapter rather than
|
||||
# merging epubs.
|
||||
(url,
|
||||
chaptercount,
|
||||
adapter.oldchapters,
|
||||
adapter.oldimgs,
|
||||
adapter.oldcover,
|
||||
adapter.calibrebookmark,
|
||||
adapter.logfile) = get_update_data(output_filename)
|
||||
print "Do update - epub(%d) vs url(%d)" % (chaptercount, urlchaptercount)
|
||||
|
||||
print "Do update - epub(%d) vs url(%d)" % (chaptercount, urlchaptercount)
|
||||
if not (options.update and chaptercount == urlchaptercount) \
|
||||
and adapter.getConfig("do_update_hook"):
|
||||
chaptercount = adapter.hookForUpdates(chaptercount)
|
||||
|
||||
if not (options.update and chaptercount == urlchaptercount) \
|
||||
and adapter.getConfig("do_update_hook"):
|
||||
chaptercount = adapter.hookForUpdates(chaptercount)
|
||||
|
||||
writeStory(configuration,adapter,"epub")
|
||||
writeStory(configuration,adapter,"epub")
|
||||
|
||||
else:
|
||||
# regular download
|
||||
if options.metaonly:
|
||||
print adapter.getStoryMetadataOnly()
|
||||
pprint.pprint(adapter.getStoryMetadataOnly().getAllMetadata())
|
||||
|
||||
output_filename=writeStory(configuration,adapter,options.format,options.metaonly)
|
||||
|
||||
|
||||
Binary file not shown.
@@ -123,11 +123,21 @@ import adapter_fictionpadcom
|
||||
import adapter_storiesonlinenet
|
||||
import adapter_trekiverseorg
|
||||
import adapter_literotica
|
||||
import adapter_voracity2eficcom
|
||||
import adapter_spikeluvercom
|
||||
import adapter_bloodshedversecom
|
||||
import adapter_nocturnallightnet
|
||||
import adapter_fanfichu
|
||||
import adapter_fanfictioncsodaidokhu
|
||||
import adapter_fictionmaniatv
|
||||
import adapter_bdsmgeschichten
|
||||
import adapter_tolkienfanfiction
|
||||
import adapter_themaplebookshelf
|
||||
|
||||
## 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 = []
|
||||
__domain_map = {}
|
||||
@@ -195,18 +205,20 @@ 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)^[htp]+(s?)[:/]+",r"http\1://",url.strip())
|
||||
if fixedurl.startswith("//"):
|
||||
fixedurl = "http:%s"%url
|
||||
if not fixedurl.startswith("http"):
|
||||
fixedurl = "http://%s"%url
|
||||
## remove any trailing '#' locations.
|
||||
fixedurl = re.sub(r"#.*$","",fixedurl)
|
||||
|
||||
|
||||
parsedUrl = up.urlparse(fixedurl)
|
||||
domain = parsedUrl.netloc.lower()
|
||||
if( domain != parsedUrl.netloc ):
|
||||
@@ -217,17 +229,17 @@ def getClassFor(url):
|
||||
domain = domain.replace("www.","")
|
||||
#logger.debug("trying site:without www: "+domain)
|
||||
cls = getClassFromList(domain)
|
||||
fixedurl = fixedurl.replace("http://www.","http://")
|
||||
fixedurl = re.sub(r"^http(s?)://www\.",r"http\1://",fixedurl)
|
||||
if not cls:
|
||||
#logger.debug("trying site:www."+domain)
|
||||
cls = getClassFromList("www."+domain)
|
||||
fixedurl = fixedurl.replace("http://","http://www.")
|
||||
fixedurl = re.sub(r"^http(s?)://",r"http\1://www.",fixedurl)
|
||||
|
||||
if cls:
|
||||
fixedurl = cls.stripURLParameters(fixedurl)
|
||||
|
||||
|
||||
return (cls,fixedurl)
|
||||
|
||||
|
||||
def getClassFromList(domain):
|
||||
try:
|
||||
return __domain_map[domain]
|
||||
|
||||
@@ -84,7 +84,7 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
def getSiteURLPattern(self):
|
||||
# http://archiveofourown.org/collections/Smallville_Slash_Archive/works/159770
|
||||
# Discard leading zeros from story ID numbers--AO3 doesn't use them in it's own chapter URLs.
|
||||
return re.escape("http://")+re.escape(self.getSiteDomain())+r"(/collections/[^/]+)?/works/0*(?P<id>\d+)"
|
||||
return r"https?://"+re.escape(self.getSiteDomain())+r"(/collections/[^/]+)?/works/0*(?P<id>\d+)"
|
||||
|
||||
## Login
|
||||
def needToLoginCheck(self, data):
|
||||
@@ -145,10 +145,13 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.meta)
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
|
||||
if "Sorry, we couldn't find the work you were looking for." in data:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
|
||||
if self.needToLoginCheck(data):
|
||||
# need to log in for this one.
|
||||
self.performLogin(url,data)
|
||||
@@ -162,19 +165,19 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
## Title
|
||||
a = soup.find('a', href=re.compile(r"^/works/\d+$"))
|
||||
a = soup.find('a', href=re.compile(r"/works/\d+$"))
|
||||
self.story.setMetadata('title',stripHTML(a))
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
alist = soup.findAll('a', href=re.compile(r"^/users/\w+/pseuds/\w+"))
|
||||
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','http://archiveofourown.org/')
|
||||
self.story.setMetadata('authorId','0')
|
||||
else:
|
||||
for a in alist:
|
||||
self.story.addToList('authorId',a['href'].split('/')[2])
|
||||
self.story.addToList('authorUrl','http://'+self.host+a['href'])
|
||||
self.story.addToList('authorId',a['href'].split('/')[-1])
|
||||
self.story.addToList('authorUrl',a['href'])
|
||||
self.story.addToList('author',a.text)
|
||||
|
||||
newestChapter = None
|
||||
@@ -321,25 +324,31 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
chapter=bs.BeautifulSoup('<div class="story"></div>').find('div')
|
||||
data = self._fetchUrl(url)
|
||||
soup = bs.BeautifulSoup(data,selfClosingTags=('br','hr'))
|
||||
|
||||
headnotes = soup.find('div', {'class' : "preface group"}).find('div', {'class' : "notes module"})
|
||||
if headnotes != None:
|
||||
headnotes = headnotes.find('blockquote', {'class' : "userstuff"})
|
||||
|
||||
exclude_notes=self.getConfigList('exclude_notes')
|
||||
|
||||
if 'authorheadnotes' not in exclude_notes:
|
||||
headnotes = soup.find('div', {'class' : "preface group"}).find('div', {'class' : "notes module"})
|
||||
if headnotes != None:
|
||||
chapter.append("<b>Author's Note:</b>")
|
||||
chapter.append(headnotes)
|
||||
headnotes = headnotes.find('blockquote', {'class' : "userstuff"})
|
||||
if headnotes != None:
|
||||
chapter.append("<b>Author's Note:</b>")
|
||||
chapter.append(headnotes)
|
||||
|
||||
chapsumm = soup.find('div', {'id' : "summary"})
|
||||
if chapsumm != None:
|
||||
chapsumm = chapsumm.find('blockquote')
|
||||
chapter.append("<b>Summary for the Chapter:</b>")
|
||||
chapter.append(chapsumm)
|
||||
chapnotes = soup.find('div', {'id' : "notes"})
|
||||
if chapnotes != None:
|
||||
chapnotes = chapnotes.find('blockquote')
|
||||
if 'chaptersummary' not in exclude_notes:
|
||||
chapsumm = soup.find('div', {'id' : "summary"})
|
||||
if chapsumm != None:
|
||||
chapsumm = chapsumm.find('blockquote')
|
||||
chapter.append("<b>Summary for the Chapter:</b>")
|
||||
chapter.append(chapsumm)
|
||||
|
||||
if 'chapterheadnotes' not in exclude_notes:
|
||||
chapnotes = soup.find('div', {'id' : "notes"})
|
||||
if chapnotes != None:
|
||||
chapter.append("<b>Notes for the Chapter:</b>")
|
||||
chapter.append(chapnotes)
|
||||
chapnotes = chapnotes.find('blockquote')
|
||||
if chapnotes != None:
|
||||
chapter.append("<b>Notes for the Chapter:</b>")
|
||||
chapter.append(chapnotes)
|
||||
|
||||
text = soup.find('div', {'class' : "userstuff module"})
|
||||
chtext = text.find('h3', {'class' : "landmark heading"})
|
||||
@@ -347,17 +356,19 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
chtext.extract()
|
||||
chapter.append(text)
|
||||
|
||||
chapfoot = soup.find('div', {'class' : "end notes module", 'role' : "complementary"})
|
||||
if chapfoot != None:
|
||||
chapfoot = chapfoot.find('blockquote')
|
||||
chapter.append("<b>Notes for the Chapter:</b>")
|
||||
chapter.append(chapfoot)
|
||||
if 'chapterfootnotes' not in exclude_notes:
|
||||
chapfoot = soup.find('div', {'class' : "end notes module", 'role' : "complementary"})
|
||||
if chapfoot != None:
|
||||
chapfoot = chapfoot.find('blockquote')
|
||||
chapter.append("<b>Notes for the Chapter:</b>")
|
||||
chapter.append(chapfoot)
|
||||
|
||||
footnotes = soup.find('div', {'id' : "work_endnotes"})
|
||||
if footnotes != None:
|
||||
footnotes = footnotes.find('blockquote')
|
||||
chapter.append("<b>Author's Note:</b>")
|
||||
chapter.append(footnotes)
|
||||
if 'authorfootnotes' not in exclude_notes:
|
||||
footnotes = soup.find('div', {'id' : "work_endnotes"})
|
||||
if footnotes != None:
|
||||
footnotes = footnotes.find('blockquote')
|
||||
chapter.append("<b>Author's Note:</b>")
|
||||
chapter.append(footnotes)
|
||||
|
||||
if None == soup:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2014 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
|
||||
import urlparse
|
||||
import time
|
||||
|
||||
from .. import BeautifulSoup as bs
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
def _translate_date_german_english(date):
|
||||
fullmon = {"Januar":"01",
|
||||
"Februar":"02",
|
||||
u"März":"03",
|
||||
"April":"04",
|
||||
"Mai":"05",
|
||||
"Juni":"06",
|
||||
"Juli":"07",
|
||||
"August":"08",
|
||||
"September":"09",
|
||||
"Oktober":"10",
|
||||
"November":"11",
|
||||
"Dezember":"12"}
|
||||
for (name,num) in fullmon.items():
|
||||
date = date.replace(name,num)
|
||||
return date
|
||||
|
||||
_REGEX_TRAILING_DIGIT = re.compile("(\d+)$")
|
||||
_REGEX_DASH_TO_END = re.compile("-[^-]+$")
|
||||
_REGEX_CHAPTER_TITLE = re.compile(ur"""
|
||||
\s*
|
||||
[\u2013-]?
|
||||
\s*
|
||||
([\dIVX-]+)?
|
||||
\.?
|
||||
\s*
|
||||
[\[\(]?
|
||||
\s*
|
||||
(Teil|Kapitel|Tag)?
|
||||
\s*
|
||||
([\dIVX-]+)?
|
||||
\s*
|
||||
[\]\)]?
|
||||
\s*
|
||||
$
|
||||
""", re.VERBOSE)
|
||||
_INITIAL_STEP = 5
|
||||
|
||||
class BdsmGeschichtenAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.decode = ["utf8", "Windows-1252"]
|
||||
|
||||
self.story.setMetadata('siteabbrev','bdsmgesch')
|
||||
|
||||
# Replace possible chapter numbering
|
||||
chapterMatch = _REGEX_TRAILING_DIGIT.search(url)
|
||||
if chapterMatch is None:
|
||||
self.maxChapter = 1
|
||||
else:
|
||||
self.maxChapter = int(chapterMatch.group(1))
|
||||
# url = re.sub(_REGEX_TRAILING_DIGIT, "1", url)
|
||||
|
||||
# set storyId
|
||||
self.story.setMetadata('storyId', re.compile(self.getSiteURLPattern()).match(url).group('storyId'))
|
||||
|
||||
# normalize URL
|
||||
self._setURL('http://%s/%s' % (self.getSiteDomain(), self.story.getMetadata('storyId')))
|
||||
|
||||
self.dateformat = '%d. %m %Y - %H:%M'
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return 'bdsm-geschichten.net'
|
||||
|
||||
@classmethod
|
||||
def getAcceptDomains(cls):
|
||||
return ['www.bdsm-geschichten.net', 'www.bdsm-geschichten.net']
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://www.bdsm-geschichten.net/title-of-story-1 http://bdsm-geschichten.net/title-of-story-1"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return r"http://(www\.)?bdsm-geschichten.net/(?P<storyId>[a-zA-Z0-9_-]+)"
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
if not (self.is_adult or self.getConfig("is_adult")):
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
try:
|
||||
data1 = self._fetchUrl(self.url)
|
||||
soup = bs.BeautifulSoup(data1)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
#strip comments from soup
|
||||
[comment.extract() for comment in soup.findAll(text=lambda text:isinstance(text, bs.Comment))]
|
||||
|
||||
# Cache the soups so we won't have to redownload in getChapterText later
|
||||
self.soupsCache = {}
|
||||
self.soupsCache[self.url] = soup
|
||||
|
||||
# author
|
||||
authorDiv = soup.find("div", "author-pane-line author-name")
|
||||
authorId = authorDiv.string.strip()
|
||||
self.story.setMetadata('authorId', authorId)
|
||||
self.story.setMetadata('author', authorId)
|
||||
# TODO not really true need to be loggedin for this to work or fetch userid
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/'+authorId)
|
||||
|
||||
# TODO better metadata
|
||||
date = soup.find("div", {"class": "submitted"}).string.strip()
|
||||
date = re.sub(" —.*", "", date)
|
||||
date = _translate_date_german_english(date)
|
||||
self.story.setMetadata('datePublished', makeDate(date, self.dateformat))
|
||||
title1 = soup.find("h1", {'class': 'title'}).string
|
||||
|
||||
|
||||
for tagLink in soup.find("ul", "taxonomy").findAll("a"):
|
||||
self.story.addToList('category', tagLink.string)
|
||||
|
||||
## Retrieve chapter soups
|
||||
if self.getConfig('find_chapters') == 'guess':
|
||||
self.chapterUrls = []
|
||||
self._find_chapters_by_guessing(title1)
|
||||
else:
|
||||
self._find_chapters_by_parsing(soup)
|
||||
|
||||
firstChapterUrl = self.chapterUrls[0][1]
|
||||
if firstChapterUrl in self.soupsCache:
|
||||
firstChapterSoup = self.soupsCache[firstChapterUrl]
|
||||
h1 = firstChapterSoup.find("h1").text
|
||||
else:
|
||||
h1 = soup.find("h1").text
|
||||
|
||||
h1 = re.sub(_REGEX_CHAPTER_TITLE, "", h1)
|
||||
self.story.setMetadata('title', h1)
|
||||
self.story.setMetadata('numChapters', len(self.chapterUrls))
|
||||
return
|
||||
|
||||
def _find_chapters_by_parsing(self, soup):
|
||||
|
||||
# store original soup
|
||||
origSoup = soup
|
||||
|
||||
#
|
||||
# find first chapter
|
||||
#
|
||||
firstLink = None
|
||||
firstLinkDiv = soup.find("div", "field-field-erster-teil")
|
||||
if firstLinkDiv is not None:
|
||||
firstLink = "http://%s%s" % (self.getSiteDomain(), firstLinkDiv.findNext("a")['href'])
|
||||
logger.debug("Found first chapter right away <%s>" % firstLink)
|
||||
try:
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(firstLink))
|
||||
self.soupsCache[firstLink] = soup
|
||||
self.chapterUrls.insert(0, (soup.find("h1").text, firstLink))
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise exceptions.StoryDoesNotExist(firstLink)
|
||||
else:
|
||||
logger.debug("DIDN'T find first chapter right away")
|
||||
# parse previous Link until first
|
||||
while True:
|
||||
prevLink = None
|
||||
prevLinkDiv = soup.find("div", "field-field-vorheriger-teil")
|
||||
if prevLinkDiv is not None:
|
||||
prevLink = prevLinkDiv.find("a")
|
||||
if prevLink is None:
|
||||
prevLink = soup.find("a", text=re.compile("<<<")) # <<<
|
||||
if prevLink is None:
|
||||
logger.debug("Couldn't find prev part")
|
||||
break
|
||||
else:
|
||||
logger.debug("Previous Chapter <%s>" % prevLink)
|
||||
if type(prevLink) != bs.Tag or prevLink.name != "a":
|
||||
prevLink = prevLink.findParent("a")
|
||||
if prevLink is None or '#' in prevLink['href']:
|
||||
logger.debug("Couldn't find prev part (false positive) <%s>" % prevLink)
|
||||
break
|
||||
prevLink = prevLink['href']
|
||||
try:
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(prevLink))
|
||||
self.soupsCache[prevLink] = soup
|
||||
prevTtitle = soup.find("h1", {'class': 'title'}).string
|
||||
self.chapterUrls.insert(0, (prevTtitle, prevLink))
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(nextLink)
|
||||
else:
|
||||
raise e
|
||||
firstLink = prevLink
|
||||
|
||||
# if first chapter couldn't be determined, assume the URL originally
|
||||
# passed is the first chapter
|
||||
if firstLink is None:
|
||||
logger.debug("Couldn't set first chapter")
|
||||
firstLink = self.url
|
||||
self.chapterUrls.insert(0, (soup.find("h1").text, firstLink))
|
||||
|
||||
# set first URL
|
||||
logger.debug("Set first link: %s" % firstLink)
|
||||
self._setURL(firstLink)
|
||||
self.story.setMetadata('storyId', re.compile(self.getSiteURLPattern()).match(firstLink).group('storyId'))
|
||||
|
||||
#
|
||||
# Parse next chapters
|
||||
#
|
||||
while True:
|
||||
nextLink = None
|
||||
nextLinkDiv = soup.find("div", "field-field-naechster-teil")
|
||||
if nextLinkDiv is not None:
|
||||
nextLink = nextLinkDiv.find("a")
|
||||
if nextLink is None:
|
||||
nextLink = soup.find("a", text=re.compile(">>>"))
|
||||
if nextLink is None:
|
||||
nextLink = soup.find("a", text=re.compile("Fortsetzung"))
|
||||
|
||||
if nextLink is None:
|
||||
logger.debug("Couldn't find next part")
|
||||
break
|
||||
else:
|
||||
if type(nextLink) != bs.Tag or nextLink.name != "a":
|
||||
nextLink = nextLink.findParent("a")
|
||||
if nextLink is None or '#' in nextLink['href']:
|
||||
logger.debug("Couldn't find next part (false positive) <%s>" % nextLink)
|
||||
break
|
||||
nextLink = nextLink['href']
|
||||
|
||||
if not nextLink.startswith('http:'):
|
||||
nextLink = 'http://' + self.getSiteDomain() + nextLink
|
||||
|
||||
for loadedChapter in self.chapterUrls:
|
||||
if loadedChapter[0] == nextLink:
|
||||
logger.debug("ERROR: Repeating chapter <%s> Try to fix it" % nextLink)
|
||||
nextLinkMatch = _REGEX_TRAILING_DIGIT.match(nextLink)
|
||||
if nextLinkMatch is not None:
|
||||
curChap = nextLinkMatch.group(1)
|
||||
nextLink = re.sub(_REGEX_TRAILING_DIGIT, str(int(curChap) + 1), nextLink)
|
||||
else:
|
||||
break
|
||||
try:
|
||||
data = self._fetchUrl(nextLink)
|
||||
soup = bs.BeautifulSoup(data)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(nextLink)
|
||||
else:
|
||||
raise e
|
||||
title2 = soup.find("h1", {'class': 'title'}).string
|
||||
self.chapterUrls.append((title2, nextLink))
|
||||
logger.debug("Grabbing next chapter URL " + nextLink)
|
||||
self.soupsCache[nextLink] = soup
|
||||
# [comment.extract() for comment in soup.findAll(text=lambda text:isinstance(text, bs.Comment))]
|
||||
logger.debug("Chapters: %s" % self.chapterUrls)
|
||||
|
||||
|
||||
def _find_chapters_by_guessing(self, title1):
|
||||
step = _INITIAL_STEP
|
||||
curMax = self.maxChapter + step
|
||||
lastHit = True
|
||||
while True:
|
||||
nextChapterUrl = re.sub(_REGEX_TRAILING_DIGIT, str(curMax), self.url)
|
||||
if nextChapterUrl == self.url:
|
||||
logger.debug("Unable to guess next chapter because URL doesn't end in numbers")
|
||||
break;
|
||||
try:
|
||||
logger.debug("Trying chapter URL " + nextChapterUrl)
|
||||
data = self._fetchUrl(nextChapterUrl)
|
||||
hit = True
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
hit = False
|
||||
else:
|
||||
raise e
|
||||
if hit:
|
||||
logger.debug("Found chapter URL " + nextChapterUrl)
|
||||
self.maxChapter = curMax
|
||||
self.soupsCache[nextChapterUrl] = bs.BeautifulSoup(data)
|
||||
if not lastHit:
|
||||
break
|
||||
lastHit = curMax
|
||||
curMax += step
|
||||
else:
|
||||
lastHit = False
|
||||
curMax -= 1
|
||||
logger.debug(curMax)
|
||||
|
||||
for i in xrange(1, self.maxChapter):
|
||||
nextChapterUrl = re.sub(_REGEX_TRAILING_DIGIT, str(i), self.url)
|
||||
nextChapterTitle = re.sub("1", str(i), title1)
|
||||
self.chapterUrls.append((nextChapterTitle, nextChapterUrl))
|
||||
|
||||
def getChapterText(self, url):
|
||||
|
||||
if url in self.soupsCache:
|
||||
logger.debug('Getting chapter <%s> from cache' % url)
|
||||
soup = self.soupsCache[url]
|
||||
else:
|
||||
logger.debug('Downloading chapter <%s>' % url)
|
||||
data1 = self._fetchUrl(url)
|
||||
soup = bs.BeautifulSoup(data1)
|
||||
#strip comments from soup
|
||||
[comment.extract() for comment in soup.findAll(text=lambda text:isinstance(text, bs.Comment))]
|
||||
|
||||
# get story text
|
||||
storyDiv1 = bs.Tag(soup, "div")
|
||||
for para in soup.find("div", "full-node").find('div', 'content').findAll("p"):
|
||||
storyDiv1.append(para)
|
||||
storyDiv1.append('<br />')
|
||||
storytext = self.utf8FromSoup(url,storyDiv1)
|
||||
|
||||
return storytext
|
||||
|
||||
|
||||
def getClass():
|
||||
return BdsmGeschichtenAdapter
|
||||
@@ -0,0 +1,193 @@
|
||||
from datetime import timedelta
|
||||
import re
|
||||
import urllib2
|
||||
import urlparse
|
||||
|
||||
from .. import BeautifulSoup
|
||||
from ..htmlcleanup import stripHTML
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
from .. import exceptions
|
||||
|
||||
|
||||
def getClass():
|
||||
return BloodshedverseComAdapter
|
||||
|
||||
|
||||
def _get_query_data(url):
|
||||
components = urlparse.urlparse(url)
|
||||
query_data = urlparse.parse_qs(components.query)
|
||||
return dict((key, data[0]) for key, data in query_data.items())
|
||||
|
||||
|
||||
class BloodshedverseComAdapter(BaseSiteAdapter):
|
||||
SITE_ABBREVIATION = 'bvc'
|
||||
SITE_DOMAIN = 'bloodshedverse.com'
|
||||
|
||||
BASE_URL = 'http://' + SITE_DOMAIN + '/'
|
||||
READ_URL_TEMPLATE = BASE_URL + 'stories.php?go=read&no=%s'
|
||||
|
||||
STARTED_DATETIME_FORMAT = '%m/%d/%Y'
|
||||
UPDATED_DATETIME_FORMAT = '%m/%d/%Y %I:%M'
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
query_data = urlparse.parse_qs(self.parsedUrl.query)
|
||||
story_no = query_data['no'][0]
|
||||
|
||||
self.story.setMetadata('storyId', story_no)
|
||||
self._setURL(self.READ_URL_TEMPLATE % story_no)
|
||||
self.story.setMetadata('siteabbrev', self.SITE_ABBREVIATION)
|
||||
|
||||
def _customized_fetch_url(self, url, exception=None, parameters=None):
|
||||
if exception:
|
||||
try:
|
||||
data = self._fetchUrl(url, parameters)
|
||||
except urllib2.HTTPError:
|
||||
raise exception(self.url)
|
||||
# Just let self._fetchUrl throw the exception, don't catch and
|
||||
# customize it.
|
||||
else:
|
||||
data = self._fetchUrl(url, parameters)
|
||||
|
||||
return BeautifulSoup.BeautifulSoup(data)
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return BloodshedverseComAdapter.SITE_DOMAIN
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return cls.READ_URL_TEMPLATE % 1234
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape(self.BASE_URL + 'stories.php?go=') + r'(read|chapters)\&no=\d+$'
|
||||
|
||||
# Override stripURLParameters so the "no" parameter won't get stripped
|
||||
@classmethod
|
||||
def stripURLParameters(cls, url):
|
||||
return url
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
soup = self._customized_fetch_url(self.url)
|
||||
|
||||
# Since no 404 error code we have to raise the exception ourselves.
|
||||
# A title that is just 'by' indicates that there is no author name
|
||||
# and no story title available.
|
||||
if stripHTML(soup.title) == 'by':
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
|
||||
for option in soup.find('select', {'name': 'chapter'}):
|
||||
title = stripHTML(option)
|
||||
url = self.READ_URL_TEMPLATE % option['value']
|
||||
self.chapterUrls.append((title, url))
|
||||
|
||||
# Get the URL to the author's page and find the correct story entry to
|
||||
# scrape the metadata
|
||||
author_url = urlparse.urljoin(self.url, soup.find('a', {'class': 'headline'})['href'])
|
||||
soup = self._customized_fetch_url(author_url)
|
||||
|
||||
story_no = self.story.getMetadata('storyId')
|
||||
# Ignore first list_box div, it only contains the author information
|
||||
for list_box in soup('div', {'class': 'list_box'})[1:]:
|
||||
url = list_box.find('a', {'class': 'fictitle'})['href']
|
||||
query_data = _get_query_data(url)
|
||||
|
||||
# Found the div containing the story's metadata; break the loop and
|
||||
# parse the element
|
||||
if query_data['no'] == story_no:
|
||||
break
|
||||
else:
|
||||
raise exceptions.FailedToDownload(self.url)
|
||||
|
||||
title_anchor = list_box.find('a', {'class': 'fictitle'})
|
||||
self.story.setMetadata('title', stripHTML(title_anchor))
|
||||
|
||||
author_anchor = title_anchor.findNextSibling('a')
|
||||
self.story.setMetadata('author', stripHTML(author_anchor))
|
||||
self.story.setMetadata('authorId', _get_query_data(author_anchor['href'])['who'])
|
||||
self.story.setMetadata('authorUrl', urlparse.urljoin(self.url, author_anchor['href']))
|
||||
|
||||
list_review = list_box.find('div', {'class': 'list_review'})
|
||||
reviews = stripHTML(list_review.a).split(' ', 1)[0]
|
||||
self.story.setMetadata('reviews', reviews)
|
||||
|
||||
summary_div = list_box.find('div', {'class': 'list_summary'})
|
||||
if not self.getConfig('keep_summary_html'):
|
||||
summary = ''.join(summary_div(text=True))
|
||||
else:
|
||||
summary = self.utf8FromSoup(author_url, summary_div)
|
||||
|
||||
self.story.setMetadata('description', summary)
|
||||
|
||||
# I'm assuming this to be the category, not sure what else it could be
|
||||
first_listinfo = list_box.find('div', {'class': 'list_info'})
|
||||
self.story.addToList('category', stripHTML(first_listinfo.a))
|
||||
|
||||
for list_info in first_listinfo.findNextSiblings('div', {'class': 'list_info'}):
|
||||
for b_tag in list_info('b'):
|
||||
key = b_tag.string.strip(': ')
|
||||
# Strip colons from the beginning, superfluous spaces and minus
|
||||
# characters from the end, and possibly trailing commas from
|
||||
# the warnings if only one is present
|
||||
value = b_tag.nextSibling.string.strip(': -,')
|
||||
|
||||
if key == 'Genre':
|
||||
for genre in value.split(', '):
|
||||
# Ignore the "none" genre
|
||||
if not genre == 'none':
|
||||
self.story.addToList('genre', genre)
|
||||
|
||||
elif key == 'Rating':
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
elif key == 'Complete':
|
||||
self.story.setMetadata('status', 'Completed' if value == 'Yes' else 'In-Progress')
|
||||
|
||||
elif key == 'Warning':
|
||||
for warning in value.split(', '):
|
||||
# The string here starts with ", " before the actual list
|
||||
# of values sometimes, so check for an empty warning
|
||||
# and ignore the "none" warning.
|
||||
if not warning or warning == 'none':
|
||||
continue
|
||||
|
||||
self.story.addToList('warnings', warning)
|
||||
|
||||
elif key == 'Chapters':
|
||||
self.story.setMetadata('numChapters', int(value))
|
||||
|
||||
elif key == 'Words':
|
||||
# Apparently only numChapters need to be an integer for
|
||||
# some strange reason. Remove possible ',' characters as to
|
||||
# not confuse the codebase down the line
|
||||
self.story.setMetadata('numWords', value.replace(',', ''))
|
||||
|
||||
elif key == 'Started':
|
||||
self.story.setMetadata('datePublished', makeDate(value, self.STARTED_DATETIME_FORMAT))
|
||||
|
||||
elif key == 'Updated':
|
||||
date_string, period = value.rsplit(' ', 1)
|
||||
date = makeDate(date_string, self.UPDATED_DATETIME_FORMAT)
|
||||
|
||||
# Rather ugly hack to work around Calibre's changing of
|
||||
# Python's locale setting, causing am/pm to not be properly
|
||||
# parsed by strptime() when using a non-english locale
|
||||
if period == 'pm':
|
||||
date += timedelta(hours=12)
|
||||
self.story.setMetadata('dateUpdated', date)
|
||||
|
||||
if self.story.getMetadata('rating') == 'NC-17' and not (self.is_adult or self.getConfig('is_adult')):
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
def getChapterText(self, url):
|
||||
soup = self._customized_fetch_url(url)
|
||||
storytext_div = soup.find('div', {'class': 'storytext'})
|
||||
|
||||
if self.getConfig('strip_text_links'):
|
||||
for anchor in storytext_div('a', {'class': 'FAtxtL'}):
|
||||
navigable_string = BeautifulSoup.NavigableString(anchor.string)
|
||||
anchor.replaceWith(navigable_string)
|
||||
|
||||
return self.utf8FromSoup(url, storytext_div)
|
||||
@@ -182,6 +182,11 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
|
||||
|
||||
# first a tag in pagetitle is title
|
||||
self.story.setMetadata('title',stripHTML(div.find('a')))
|
||||
div.find('a').extract()
|
||||
# only thing left in div(pagetitle) now should be 'by' and rating.
|
||||
rating = stripHTML(div)
|
||||
if '[' in rating:
|
||||
self.story.setMetadata('rating', rating[rating.index('[')+1:-1])
|
||||
|
||||
for chapa in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+
|
||||
self.story.getMetadata('storyId')+'&chapter=\d+')):
|
||||
@@ -234,31 +239,28 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
|
||||
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+'))
|
||||
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:
|
||||
for char in value.string.split(', '):
|
||||
if not 'None' in char:
|
||||
self.story.addToList('characters',char)
|
||||
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:
|
||||
for genre in value.string.split(', '):
|
||||
if not 'None' in genre:
|
||||
self.story.addToList('genre',genre)
|
||||
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:
|
||||
for warning in value.string.split(', '):
|
||||
if not 'None' in warning:
|
||||
self.story.addToList('warnings',warning)
|
||||
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:
|
||||
|
||||
@@ -74,7 +74,8 @@ class EFPFanFicNet(BaseSiteAdapter):
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
if 'Fai il login e leggi la storia!' in data:
|
||||
if( 'Fai il login e leggi la storia!' in data or
|
||||
'Questa storia presenta contenuti non adatti ai minori' in data ):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
# coding=utf-8
|
||||
|
||||
import re
|
||||
import urllib2
|
||||
import urlparse
|
||||
|
||||
from .. import BeautifulSoup
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
from .. import exceptions
|
||||
|
||||
|
||||
_SOURCE_CODE_ENCODING = 'utf-8'
|
||||
|
||||
|
||||
def getClass():
|
||||
return FanficHuAdapter
|
||||
|
||||
|
||||
def _get_query_data(url):
|
||||
components = urlparse.urlparse(url)
|
||||
query_data = urlparse.parse_qs(components.query)
|
||||
return dict((key, data[0]) for key, data in query_data.items())
|
||||
|
||||
|
||||
class FanficHuAdapter(BaseSiteAdapter):
|
||||
SITE_ABBREVIATION = 'ffh'
|
||||
SITE_DOMAIN = 'fanfic.hu'
|
||||
SITE_LANGUAGE = 'Hungarian'
|
||||
|
||||
BASE_URL = 'http://' + SITE_DOMAIN + '/merengo/'
|
||||
VIEW_STORY_URL_TEMPLATE = BASE_URL + 'viewstory.php?sid=%s'
|
||||
|
||||
DATE_FORMAT = '%m/%d/%Y'
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
query_data = urlparse.parse_qs(self.parsedUrl.query)
|
||||
story_id = query_data['sid'][0]
|
||||
|
||||
self.story.setMetadata('storyId', story_id)
|
||||
self._setURL(self.VIEW_STORY_URL_TEMPLATE % story_id)
|
||||
self.story.setMetadata('siteabbrev', self.SITE_ABBREVIATION)
|
||||
self.story.setMetadata('language', self.SITE_LANGUAGE)
|
||||
|
||||
def _customized_fetch_url(self, url, exception=None, parameters=None):
|
||||
if exception:
|
||||
try:
|
||||
data = self._fetchUrl(url, parameters)
|
||||
except urllib2.HTTPError:
|
||||
raise exception(self.url)
|
||||
# Just let self._fetchUrl throw the exception, don't catch and
|
||||
# customize it.
|
||||
else:
|
||||
data = self._fetchUrl(url, parameters)
|
||||
|
||||
return BeautifulSoup.BeautifulSoup(data)
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return FanficHuAdapter.SITE_DOMAIN
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return cls.VIEW_STORY_URL_TEMPLATE % 1234
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape(self.VIEW_STORY_URL_TEMPLATE[:-2]) + r'\d+$'
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
soup = self._customized_fetch_url(self.url + '&i=1')
|
||||
|
||||
if soup.title.string.encode(_SOURCE_CODE_ENCODING).strip(' :') == 'írta':
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
|
||||
chapter_options = soup.find('form', action='viewstory.php').select('option')
|
||||
# Remove redundant "Fejezetek" option
|
||||
chapter_options.pop(0)
|
||||
|
||||
# If there is still more than one entry remove chapter overview entry
|
||||
if len(chapter_options) > 1:
|
||||
chapter_options.pop(0)
|
||||
|
||||
for option in chapter_options:
|
||||
url = urlparse.urljoin(self.url, option['value'])
|
||||
self.chapterUrls.append((option.string, url))
|
||||
|
||||
author_url = urlparse.urljoin(self.BASE_URL, soup.find('a', href=lambda href: href and href.startswith('viewuser.php?uid='))['href'])
|
||||
soup = self._customized_fetch_url(author_url)
|
||||
|
||||
story_id = self.story.getMetadata('storyId')
|
||||
for table in soup('table', {'class': 'mainnav'}):
|
||||
title_anchor = table.find('span', {'class': 'storytitle'}).a
|
||||
href = title_anchor['href']
|
||||
if href.startswith('javascript:'):
|
||||
href = href.rsplit(' ', 1)[1].strip("'")
|
||||
query_data = _get_query_data(href)
|
||||
|
||||
if query_data['sid'] == story_id:
|
||||
break
|
||||
else:
|
||||
# This should never happen, the story must be found on the author's
|
||||
# page.
|
||||
raise exceptions.FailedToDownload(self.url)
|
||||
|
||||
self.story.setMetadata('title', title_anchor.string)
|
||||
|
||||
rows = table('tr')
|
||||
|
||||
anchors = rows[0].div('a')
|
||||
author_anchor = anchors[1]
|
||||
query_data = _get_query_data(author_anchor['href'])
|
||||
self.story.setMetadata('author', author_anchor.string)
|
||||
self.story.setMetadata('authorId', query_data['uid'])
|
||||
self.story.setMetadata('authorUrl', urlparse.urljoin(self.BASE_URL, author_anchor['href']))
|
||||
self.story.setMetadata('reviews', anchors[3].string)
|
||||
|
||||
if self.getConfig('keep_summary_html'):
|
||||
self.story.setMetadata('description', self.utf8FromSoup(author_url, rows[1].td))
|
||||
else:
|
||||
self.story.setMetadata('description', ''.join(rows[1].td(text=True)))
|
||||
|
||||
for row in rows[3:]:
|
||||
index = 0
|
||||
cells = row('td')
|
||||
|
||||
while index < len(cells):
|
||||
cell = cells[index]
|
||||
key = cell.b.string.encode(_SOURCE_CODE_ENCODING).strip(':')
|
||||
try:
|
||||
value = cells[index+1].string.encode(_SOURCE_CODE_ENCODING)
|
||||
except AttributeError:
|
||||
value = None
|
||||
|
||||
if key == 'Kategória':
|
||||
for anchor in cells[index+1]('a'):
|
||||
self.story.addToList('category', anchor.string)
|
||||
|
||||
elif key == 'Szereplõk':
|
||||
if cells[index+1].string:
|
||||
for name in cells[index+1].string.split(', '):
|
||||
self.story.addToList('character', name)
|
||||
|
||||
elif key == 'Korhatár':
|
||||
if value != 'nem korhatáros':
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
elif key == 'Figyelmeztetések':
|
||||
for b_tag in cells[index+1]('b'):
|
||||
self.story.addToList('warnings', b_tag.string)
|
||||
|
||||
elif key == 'Jellemzõk':
|
||||
for genre in cells[index+1].string.split(', '):
|
||||
self.story.addToList('genre', genre)
|
||||
|
||||
elif key == 'Fejezetek':
|
||||
self.story.setMetadata('numChapters', int(value))
|
||||
|
||||
elif key == 'Megjelenés':
|
||||
self.story.setMetadata('datePublished', makeDate(value, self.DATE_FORMAT))
|
||||
|
||||
elif key == 'Frissítés':
|
||||
self.story.setMetadata('dateUpdated', makeDate(value, self.DATE_FORMAT))
|
||||
|
||||
elif key == 'Szavak':
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
elif key == 'Befejezett':
|
||||
self.story.setMetadata('status', 'Completed' if value == 'Nem' else 'In-Progress')
|
||||
|
||||
index += 2
|
||||
|
||||
if self.story.getMetadata('rating') == '18':
|
||||
if not (self.is_adult or self.getConfig('is_adult')):
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
def getChapterText(self, url):
|
||||
soup = self._customized_fetch_url(url)
|
||||
story_cell = soup.find('form', action='viewstory.php').parent.parent
|
||||
|
||||
for div in story_cell('div'):
|
||||
div.extract()
|
||||
|
||||
return self.utf8FromSoup(url, story_cell)
|
||||
@@ -0,0 +1,218 @@
|
||||
# coding=utf-8
|
||||
|
||||
import re
|
||||
import urllib2
|
||||
import urlparse
|
||||
|
||||
from .. import BeautifulSoup
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
from .. import exceptions
|
||||
|
||||
|
||||
_SOURCE_CODE_ENCODING = 'utf-8'
|
||||
|
||||
|
||||
def getClass():
|
||||
return FanfictionCsodaidokHuAdapter
|
||||
|
||||
|
||||
def _get_query_data(url):
|
||||
components = urlparse.urlparse(url)
|
||||
query_data = urlparse.parse_qs(components.query)
|
||||
return dict((key, data[0]) for key, data in query_data.items())
|
||||
|
||||
|
||||
# yields Tag _and_ NavigableString siblings from the given tag. The
|
||||
# BeautifulSoup findNextSiblings() method for some reasons only returns either
|
||||
# NavigableStrings _or_ Tag objects, not both.
|
||||
def _yield_next_siblings(tag):
|
||||
sibling = tag.nextSibling
|
||||
while sibling:
|
||||
yield sibling
|
||||
sibling = sibling.nextSibling
|
||||
|
||||
|
||||
class FanfictionCsodaidokHuAdapter(BaseSiteAdapter):
|
||||
_SITE_DOMAIN = 'fanfiction.csodaidok.hu'
|
||||
_BASE_URL = 'http://' + _SITE_DOMAIN + '/'
|
||||
_VIEW_STORY_URL_TEMPLATE = _BASE_URL + 'viewstory.php?sid=%s'
|
||||
_VIEW_CHAPTER_URL_TEMPLATE = _VIEW_STORY_URL_TEMPLATE + '&chapter=%s'
|
||||
|
||||
_STORY_DOES_NOT_EXIST_PAGE_TITLE = 'Cím: Szerző:'
|
||||
_DATE_FORMAT = '%Y.%m.%d'
|
||||
_SITE_LANGUAGE = 'Hungarian'
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
query_data = urlparse.parse_qs(self.parsedUrl.query)
|
||||
story_id = query_data['sid'][0]
|
||||
|
||||
self.story.setMetadata('storyId', story_id)
|
||||
self._setURL(self._VIEW_STORY_URL_TEMPLATE % story_id)
|
||||
self.story.setMetadata('siteabbrev', self._SITE_DOMAIN)
|
||||
self.story.setMetadata('language', self._SITE_LANGUAGE)
|
||||
|
||||
def _customized_fetch_url(self, url, exception=None, parameters=None):
|
||||
if exception:
|
||||
try:
|
||||
data = self._fetchUrl(url, parameters)
|
||||
except urllib2.HTTPError:
|
||||
raise exception(self.url)
|
||||
# Just let self._fetchUrl throw the exception, don't catch and
|
||||
# customize it.
|
||||
else:
|
||||
data = self._fetchUrl(url, parameters)
|
||||
|
||||
return BeautifulSoup.BeautifulSoup(data)
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return FanfictionCsodaidokHuAdapter._SITE_DOMAIN
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return cls._VIEW_STORY_URL_TEMPLATE % 1234
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape(self._VIEW_STORY_URL_TEMPLATE[:-2]) + r'\d+$'
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
soup = self._customized_fetch_url(self.url + '&chapter=1')
|
||||
|
||||
element = soup.find('div', id='pagetitle')
|
||||
page_title = ''.join(element(text=True)).encode(_SOURCE_CODE_ENCODING)
|
||||
if page_title == self._STORY_DOES_NOT_EXIST_PAGE_TITLE:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
|
||||
author_url = urlparse.urljoin(self.url, element.a['href'])
|
||||
|
||||
story_id = self.story.getMetadata('storyId')
|
||||
element = soup.find('select', {'name': 'chapter'})
|
||||
if element:
|
||||
for option in element('option'):
|
||||
title = option.string
|
||||
url = self._VIEW_CHAPTER_URL_TEMPLATE % (story_id, option['value'])
|
||||
self.chapterUrls.append((title, url))
|
||||
|
||||
soup = self._customized_fetch_url(author_url)
|
||||
story_id = self.story.getMetadata('storyId')
|
||||
|
||||
for listbox_div in soup('div', {'class': lambda klass: klass and 'listbox' in klass}):
|
||||
a = listbox_div.div.a
|
||||
if not a['href'].startswith('viewstory.php?sid='):
|
||||
continue
|
||||
|
||||
query_data = _get_query_data(a['href'])
|
||||
if query_data['sid'] == story_id:
|
||||
break
|
||||
else:
|
||||
raise exceptions.FailedToDownload(self.url)
|
||||
|
||||
title = ''.join(a(text=True))
|
||||
self.story.setMetadata('title', title)
|
||||
if not self.chapterUrls:
|
||||
self.chapterUrls.append((title, self.url))
|
||||
|
||||
element = a.findNextSibling('a')
|
||||
self.story.setMetadata('author', element.string)
|
||||
query_data = _get_query_data(element['href'])
|
||||
self.story.setMetadata('authorId', query_data['uid'])
|
||||
self.story.setMetadata('authorUrl', author_url)
|
||||
|
||||
element = element.findNextSibling('span')
|
||||
rating = element.nextSibling.strip(' [')
|
||||
|
||||
if rating.encode(_SOURCE_CODE_ENCODING) != 'Korhatár nélkül':
|
||||
self.story.setMetadata('rating', rating)
|
||||
|
||||
if rating == '18':
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
element = element.findNextSiblings('a')[1]
|
||||
self.story.setMetadata('reviews', element.string)
|
||||
|
||||
sections = listbox_div('div', {'class': lambda klass: klass and klass in ['content', 'tail']})
|
||||
for section in sections:
|
||||
for element in section('span', {'class': 'classification'}):
|
||||
key = element.string.encode(_SOURCE_CODE_ENCODING).strip(' :')
|
||||
try:
|
||||
value = element.nextSibling.string.encode(_SOURCE_CODE_ENCODING).strip()
|
||||
except AttributeError:
|
||||
value = None
|
||||
|
||||
if key == 'Tartalom':
|
||||
contents = []
|
||||
keep_summary_html = self.getConfig('keep_summary_html')
|
||||
|
||||
for sibling in _yield_next_siblings(element):
|
||||
if isinstance(sibling, BeautifulSoup.Tag):
|
||||
if sibling.name == 'span' and sibling.get('class', None) == 'classification':
|
||||
break
|
||||
|
||||
if keep_summary_html:
|
||||
contents.append(self.utf8FromSoup(author_url, sibling))
|
||||
else:
|
||||
contents.append(''.join(sibling(text=True)))
|
||||
else:
|
||||
contents.append(sibling)
|
||||
self.story.setMetadata('description', ''.join(contents))
|
||||
|
||||
elif key == 'Kategória':
|
||||
for sibling in element.findNextSiblings(['a', 'span']):
|
||||
if sibling.name == 'span':
|
||||
break
|
||||
|
||||
self.story.addToList('category', sibling.string)
|
||||
|
||||
elif key == 'Szereplők':
|
||||
for name in value.split(', '):
|
||||
self.story.addToList('characters', name)
|
||||
|
||||
elif key == 'Műfaj':
|
||||
if value != 'Nincs':
|
||||
self.story.setMetadata('genre', value)
|
||||
|
||||
elif key == 'Figyelmeztetés':
|
||||
if value != 'Nincs':
|
||||
for warning in value.split(', '):
|
||||
self.story.addToList('warnings', warning)
|
||||
|
||||
elif key == 'Kihívás':
|
||||
if value != 'Nincs':
|
||||
self.story.setMetadata('challenge', value)
|
||||
|
||||
elif key == 'Sorozat':
|
||||
if value != 'Nincs':
|
||||
self.story.setMetadata('series', value)
|
||||
|
||||
elif key == 'Fejezetek':
|
||||
self.story.setMetadata('numChapters', int(value))
|
||||
|
||||
elif key == 'Befejezett':
|
||||
self.story.setMetadata('status', 'Completed' if value == 'Nem' else 'In-Progress')
|
||||
|
||||
elif key == 'Szavak száma':
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
elif key == 'Feltöltve':
|
||||
self.story.setMetadata('datePublished', makeDate(value, self._DATE_FORMAT))
|
||||
|
||||
elif key == 'Frissítve':
|
||||
self.story.setMetadata('dateUpdated', makeDate(value, self._DATE_FORMAT))
|
||||
|
||||
def getChapterText(self, url):
|
||||
soup = self._customized_fetch_url(url)
|
||||
contents = []
|
||||
|
||||
notes_div = soup.find('div', id='notes')
|
||||
if notes_div:
|
||||
contents.append(self.utf8FromSoup(url, notes_div))
|
||||
story_div = notes_div.findNextSibling('div')
|
||||
else:
|
||||
element = soup.find('div', {'class': 'jumpmenu'})
|
||||
story_div = element.findNextSibling('div')
|
||||
|
||||
contents.append(self.utf8FromSoup(url, story_div.span))
|
||||
return ''.join(contents)
|
||||
@@ -0,0 +1,178 @@
|
||||
import re
|
||||
import urllib2
|
||||
import urlparse
|
||||
|
||||
from .. import BeautifulSoup
|
||||
from ..BeautifulSoup import NavigableString
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
from .. import exceptions
|
||||
|
||||
|
||||
def getClass():
|
||||
return FictionManiaTVAdapter
|
||||
|
||||
|
||||
def _get_query_data(url):
|
||||
components = urlparse.urlparse(url)
|
||||
query_data = urlparse.parse_qs(components.query)
|
||||
return dict((key, data[0]) for key, data in query_data.items())
|
||||
|
||||
# yields Tag _and_ NavigableString siblings from the given tag. The
|
||||
# BeautifulSoup findNextSiblings() method for some reasons only returns either
|
||||
# NavigableStrings _or_ Tag objects, not both.
|
||||
def _yield_next_siblings(tag):
|
||||
sibling = tag.nextSibling
|
||||
while sibling:
|
||||
yield sibling
|
||||
sibling = sibling.nextSibling
|
||||
|
||||
|
||||
class FictionManiaTVAdapter(BaseSiteAdapter):
|
||||
SITE_ABBREVIATION = 'fmt'
|
||||
SITE_DOMAIN = 'fictionmania.tv'
|
||||
|
||||
BASE_URL = 'http://' + SITE_DOMAIN + '/stories/'
|
||||
READ_TEXT_STORY_URL_TEMPLATE = BASE_URL + 'readtextstory.html?storyID=%s'
|
||||
DETAILS_URL_TEMPLATE = BASE_URL + 'details.html?storyID=%s'
|
||||
|
||||
DATETIME_FORMAT = '%m/%d/%Y'
|
||||
ALTERNATIVE_DATETIME_FORMAT = '%m/%d/%y'
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
query_data = urlparse.parse_qs(self.parsedUrl.query)
|
||||
story_id = query_data['storyID'][0]
|
||||
|
||||
self.story.setMetadata('storyId', story_id)
|
||||
self._setURL(self.READ_TEXT_STORY_URL_TEMPLATE % story_id)
|
||||
self.story.setMetadata('siteabbrev', self.SITE_ABBREVIATION)
|
||||
|
||||
# Always single chapters, probably should use the Anthology feature to
|
||||
# merge chapters of a story
|
||||
self.story.setMetadata('numChapters', 1)
|
||||
|
||||
def _customized_fetch_url(self, url, exception=None, parameters=None):
|
||||
if exception:
|
||||
try:
|
||||
data = self._fetchUrl(url, parameters)
|
||||
except urllib2.HTTPError:
|
||||
raise exception(self.url)
|
||||
# Just let self._fetchUrl throw the exception, don't catch and
|
||||
# customize it.
|
||||
else:
|
||||
data = self._fetchUrl(url, parameters)
|
||||
|
||||
return BeautifulSoup.BeautifulSoup(data)
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return FictionManiaTVAdapter.SITE_DOMAIN
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return cls.READ_TEXT_STORY_URL_TEMPLATE % 1234
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape(self.BASE_URL) + '(readtextstory|details)\.html\?storyID=\d+$'
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
url = self.DETAILS_URL_TEMPLATE % self.story.getMetadata('storyId')
|
||||
soup = self._customized_fetch_url(url)
|
||||
|
||||
keep_summary_html = self.getConfig('keep_summary_html')
|
||||
for row in soup.find('table')('tr'):
|
||||
cells = row('td')
|
||||
key = cells[0].b.string.strip(':')
|
||||
try:
|
||||
value = cells[1].string
|
||||
except AttributeError:
|
||||
value = None
|
||||
|
||||
if key == 'Story Name-Title':
|
||||
self.story.setMetadata('title', value)
|
||||
self.chapterUrls.append((value, self.url))
|
||||
|
||||
elif key == 'File Name':
|
||||
self.story.setMetadata('fileName', value)
|
||||
|
||||
elif key == 'File Size':
|
||||
self.story.setMetadata('fileSize', value)
|
||||
|
||||
elif key == 'Author':
|
||||
element = cells[1].a
|
||||
self.story.setMetadata('author', element.string)
|
||||
query_data = _get_query_data(element['href'])
|
||||
self.story.setMetadata('authorId', query_data['word'])
|
||||
self.story.setMetadata('authorUrl', urlparse.urljoin(url, element['href']))
|
||||
|
||||
elif key == 'Date Added':
|
||||
try:
|
||||
date = makeDate(value, self.DATETIME_FORMAT)
|
||||
except ValueError:
|
||||
date = makeDate(value, self.ALTERNATIVE_DATETIME_FORMAT)
|
||||
self.story.setMetadata('datePublished', date)
|
||||
|
||||
elif key == 'Old Name':
|
||||
self.story.setMetadata('oldName', value)
|
||||
|
||||
elif key == 'New Name':
|
||||
self.story.setMetadata('newName', value)
|
||||
|
||||
elif key == 'Other Key Names':
|
||||
for name in value.split(', '):
|
||||
self.story.addToList('characters', name)
|
||||
|
||||
# I have no clue how the rating system works, if you are reading
|
||||
# transgender fanfiction, you are probably an adult.
|
||||
elif key == 'Rating':
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
elif key == 'Complete':
|
||||
self.story.setMetadata('status', 'Complete' if value == 'Complete' else 'In-Progress')
|
||||
|
||||
elif key == 'Categories':
|
||||
for element in cells[1]('a'):
|
||||
self.story.addToList('category', element.string)
|
||||
|
||||
elif key == 'Key Words':
|
||||
for element in cells[1]('a'):
|
||||
self.story.addToList('keyWords', element.string)
|
||||
|
||||
elif key == 'Main Characters Age':
|
||||
element = cells[1].a
|
||||
self.story.setMetadata('mainCharactersAge', element.string)
|
||||
|
||||
elif key == 'Synopsis':
|
||||
element = cells[1]
|
||||
|
||||
# Replace td with div to avoid possible strange formatting in
|
||||
# the ebook later on
|
||||
element.name = 'div'
|
||||
|
||||
if keep_summary_html:
|
||||
self.story.setMetadata('description', unicode(element))
|
||||
else:
|
||||
self.story.setMetadata('description', ''.join(element(text=True)))
|
||||
|
||||
elif key == 'Reads':
|
||||
self.story.setMetadata('readings', value)
|
||||
|
||||
def getChapterText(self, url):
|
||||
soup = self._customized_fetch_url(url)
|
||||
element = soup.find('pre')
|
||||
element.name = 'div'
|
||||
|
||||
# The story's content is contained in a <pre> tag, probably taken 1:1
|
||||
# from the source text file. A simple replacement of all newline
|
||||
# characters with a break line tag should take care of formatting.
|
||||
|
||||
# While wrapping in paragraphs would be possible, it's too much work,
|
||||
# I'd rather display the story 1:1 like it was found in the pre tag.
|
||||
content = unicode(element)
|
||||
content = content.replace('\n', '<br />')
|
||||
|
||||
if self.getConfig('non_breaking_spaces'):
|
||||
content = content.replace(' ', ' ')
|
||||
return content
|
||||
@@ -133,6 +133,7 @@ class FictionPadSiteAdapter(BaseSiteAdapter):
|
||||
author = tables['users'][0]
|
||||
story = tables['stories'][0]
|
||||
story_ver = tables['story_versions'][0]
|
||||
print("story:%s"%story)
|
||||
|
||||
self.story.setMetadata('authorId',author['id'])
|
||||
self.story.setMetadata('author',author['display_name'])
|
||||
@@ -151,7 +152,8 @@ class FictionPadSiteAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('comments',story['comments_count'])
|
||||
self.story.setMetadata('views',story['views_count'])
|
||||
self.story.setMetadata('likes',int(story['likes'])) # no idea why they floated these.
|
||||
self.story.setMetadata('dislikes',int(story['dislikes']))
|
||||
if 'dislikes' in story:
|
||||
self.story.setMetadata('dislikes',int(story['dislikes']))
|
||||
|
||||
if story_ver['is_complete']:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
|
||||
@@ -41,10 +41,10 @@ class FictionPressComSiteAdapter(FanFictionNetSiteAdapter):
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://www.fictionpress.com/s/1234/1/ http://www.fictionpress.com/s/1234/12/ http://www.fictionpress.com/s/1234/1/Story_Title http://m.fictionpress.com/s/1234/1/"
|
||||
return "https://www.fictionpress.com/s/1234/1/ https://www.fictionpress.com/s/1234/12/ http://www.fictionpress.com/s/1234/1/Story_Title http://m.fictionpress.com/s/1234/1/"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return r"http://(www|m)?\.fictionpress\.com/s/\d+(/\d+)?(/|/[a-zA-Z0-9_-]+)?/?$"
|
||||
return r"https?://(www|m)?\.fictionpress\.com/s/\d+(/\d+)?(/|/[a-zA-Z0-9_-]+)?/?$"
|
||||
|
||||
def getClass():
|
||||
return FictionPressComSiteAdapter
|
||||
|
||||
@@ -59,7 +59,7 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
return "http://www.fimfiction.net/story/1234/story-title-here http://www.fimfiction.net/story/1234/ http://www.fimfiction.com/story/1234/1/ http://mobile.fimfiction.net/story/1234/1/story-title-here/chapter-title-here"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return r"http://(www|mobile)\.fimfiction\.(net|com)/story/\d+/?.*"
|
||||
return r"https?://(www|mobile)\.fimfiction\.(net|com)/story/\d+/?.*"
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
@@ -85,7 +85,7 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
# Unfortunately, we still need to load the story index
|
||||
# page to parse the characters. And chapters, now, too.
|
||||
data = self._fetchUrl(self.url)
|
||||
data = self.do_fix_blockquotes(self._fetchUrl(self.url))
|
||||
soup = bs.BeautifulSoup(data)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
@@ -186,9 +186,9 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
self.setCoverImage(self.url,coverurl)
|
||||
|
||||
# fimf has started including extra stuff inside the description div.
|
||||
descdivstr = "%s"%soup.find("div", {"class":"description"})
|
||||
hrstr="<hr />"
|
||||
descdivstr = '<div class="description">'+descdivstr[descdivstr.index(hrstr)+len(hrstr):]
|
||||
descdivstr = u"%s"%soup.find("div", {"class":"description"})
|
||||
hrstr=u"<hr />"
|
||||
descdivstr = u'<div class="description">'+descdivstr[descdivstr.index(hrstr)+len(hrstr):]
|
||||
self.setDescription(self.url,descdivstr)
|
||||
|
||||
# Can't trust dates from API anymore I'm told.
|
||||
@@ -283,12 +283,21 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
print("Existing epub has %s chapters\nNewest chapter is %s. Discarding old chapters from there on."%(len(self.oldchapters), self.newestChapterNum+1))
|
||||
self.oldchapters = self.oldchapters[:self.newestChapterNum]
|
||||
return len(self.oldchapters)
|
||||
|
||||
|
||||
def do_fix_blockquotes(self,data):
|
||||
if self.getConfig('fix_fimf_blockquotes'):
|
||||
# <p class="double"><blockquote>
|
||||
# </blockquote></p>
|
||||
# include > in re groups so there's always something in the group.
|
||||
data = re.sub(r'<p([^>]*>\s*)<blockquote([^>]*>)',r'<blockquote\2<p\1',data)
|
||||
data = re.sub(r'</blockquote(>\s*)</p>',r'</p\1</blockquote>',data)
|
||||
return data
|
||||
|
||||
def getChapterText(self, url):
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(url),selfClosingTags=('br','hr')).find('div', {'class' : 'chapter_content'})
|
||||
|
||||
data = self.do_fix_blockquotes(self._fetchUrl(url))
|
||||
soup = bs.BeautifulSoup(data,selfClosingTags=('br','hr')).find('div', {'class' : 'chapter_content'})
|
||||
if soup == None:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
return self.utf8FromSoup(url,soup)
|
||||
|
||||
@@ -160,6 +160,11 @@ class HPFanficArchiveComAdapter(BaseSiteAdapter):
|
||||
for genre in genres:
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
if 'Pairing' in label:
|
||||
ships = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=4'))
|
||||
for ship in ships:
|
||||
self.story.addToList('ships',ship.string)
|
||||
|
||||
if 'Warnings' in label:
|
||||
warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX
|
||||
for warning in warnings:
|
||||
|
||||
@@ -39,161 +39,181 @@ class LiteroticaSiteAdapter(BaseSiteAdapter):
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
|
||||
|
||||
self.story.setMetadata('siteabbrev','litero')
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
|
||||
|
||||
self.origurl = url
|
||||
if "http://www.i." in self.origurl:
|
||||
## accept m(mobile)url, but use www.
|
||||
self.origurl = self.origurl.replace("http://www.i.","http://www.")
|
||||
# normalize to first chapter. Not sure if they ever have more than 2 digits.
|
||||
storyId = self.parsedUrl.path.split('/',)[2]
|
||||
# replace later chapters with first chapter but don't remove numbers
|
||||
# from the URL that disambiguate stories with the same title.
|
||||
storyId = re.sub("-ch-?\d\d", "", storyId)
|
||||
self.story.setMetadata('storyId', storyId)
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL("http://"+self.getSiteDomain()\
|
||||
+"/s/"+self.story.getMetadata('storyId'))
|
||||
## accept m(mobile)url, but use www.
|
||||
url = re.sub("^(www|german|spanish|french|dutch|italian|romanian|portuguese|other)\.i",
|
||||
"\1",
|
||||
url)
|
||||
|
||||
## strip ?page=...
|
||||
url = re.sub("\?page=.*$", "", url)
|
||||
|
||||
## set url
|
||||
self._setURL(url)
|
||||
|
||||
# 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
|
||||
def getSiteDomain():
|
||||
return 'www.literotica.com'
|
||||
return 'literotica.com'
|
||||
|
||||
@classmethod
|
||||
def getAcceptDomains(cls):
|
||||
return ['www.literotica.com', 'www.i.literotica.com']
|
||||
return ['www.literotica.com',
|
||||
'www.i.literotica.com',
|
||||
'german.literotica.com',
|
||||
'german.i.literotica.com',
|
||||
'spanish.literotica.com',
|
||||
'spanish.i.literotica.com',
|
||||
'french.literotica.com',
|
||||
'french.i.literotica.com',
|
||||
'dutch.literotica.com',
|
||||
'dutch.i.literotica.com',
|
||||
'italian.literotica.com',
|
||||
'italian.i.literotica.com',
|
||||
'romanian.literotica.com',
|
||||
'romanian.i.literotica.com',
|
||||
'portuguese.literotica.com',
|
||||
'portuguese.i.literotica.com',
|
||||
'other.literotica.com',
|
||||
'other.i.literotica.com']
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(self):
|
||||
#return "http://www.literotica.com/s/story-title http://www.literotica.com/stories/showstory.php?id=1234 http://www.i.literotica.com/stories/showstory.php?id=1234"
|
||||
return "http://www.literotica.com/s/story-title"
|
||||
return "http://www.literotica.com/s/story-title https://www.literotica.com/s/story-title http://portuguese.literotica.com/s/story-title http://german.literotica.com/s/story-title"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return r"http://www(\.i)?\.literotica\.com/s/([a-zA-Z0-9_-]+)"
|
||||
return r"https?://(www|german|spanish|french|dutch|italian|romanian|portuguese|other)(\.i)?\.literotica\.com/s/([a-zA-Z0-9_-]+)"
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
"""
|
||||
NOTE: Some stories can have versions,
|
||||
e.g. /my-story-ch-05-version-10
|
||||
NOTE: If two stories share the same title, a running index is added,
|
||||
e.g.: /my-story-ch-02-1
|
||||
Strategy:
|
||||
* Go to author's page, search for the current story link,
|
||||
* If it's in a tr.root-story => One-part story
|
||||
* , get metadata and be done
|
||||
* If it's in a tr.sl => Chapter in series
|
||||
* Search up from there until we find a tr.ser-ttl (this is the
|
||||
story)
|
||||
* Gather metadata
|
||||
* Search down from there for all tr.sl until the next
|
||||
tr.ser-ttl, foreach
|
||||
* Chapter link is there
|
||||
"""
|
||||
|
||||
if not (self.is_adult or self.getConfig("is_adult")):
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
url1 = self.origurl
|
||||
logger.debug("first page URL: "+url1)
|
||||
|
||||
logger.debug("Chapter/Story URL: <%s> " % self.url)
|
||||
try:
|
||||
data1 = self._fetchUrl(url1)
|
||||
data1 = self._fetchUrl(self.url)
|
||||
soup1 = bs.BeautifulSoup(data1)
|
||||
#strip comments from soup
|
||||
[comment.extract() for comment in soup1.findAll(text=lambda text:isinstance(text, bs.Comment))]
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(url1)
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
#strip comments from soup
|
||||
[comment.extract() for comment in soup1.findAll(text=lambda text:isinstance(text, bs.Comment))]
|
||||
|
||||
# author
|
||||
a = soup1.find("span", "b-story-user-y")
|
||||
self.story.setMetadata('authorId', urlparse.parse_qs(a.a['href'].split('?')[1])['uid'])
|
||||
self.story.setMetadata('authorUrl', a.a['href'])
|
||||
self.story.setMetadata('authorId', urlparse.parse_qs(a.a['href'].split('?')[1])['uid'][0])
|
||||
authorurl = a.a['href']
|
||||
if authorurl.startswith('//'):
|
||||
authorurl = self.parsedUrl.scheme+':'+authorurl
|
||||
self.story.setMetadata('authorUrl', authorurl)
|
||||
self.story.setMetadata('author', a.text)
|
||||
|
||||
# get the author page
|
||||
try:
|
||||
dataAuth = self._fetchUrl(a.a['href'])
|
||||
dataAuth = self._fetchUrl(authorurl)
|
||||
soupAuth = bs.BeautifulSoup(dataAuth)
|
||||
#strip comments from soup
|
||||
[comment.extract() for comment in soupAuth.findAll(text=lambda text:isinstance(text, bs.Comment))]
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(a.a['href'])
|
||||
raise exceptions.StoryDoesNotExist(authorurl)
|
||||
else:
|
||||
raise e
|
||||
|
||||
storyLink = soupAuth.find('a', href=url1)
|
||||
## Find link to url in author's page
|
||||
## site has started using //domain.name/asdf urls remove https?: from front
|
||||
storyLink = soupAuth.find('a', href=self.url[self.url.index(':')+1:])
|
||||
|
||||
if storyLink is not None:
|
||||
# pull the published date from the author page
|
||||
# default values from single link. Updated below if multiple chapter.
|
||||
date = storyLink.parent.parent.findAll('td')[-1].text
|
||||
urlTr = storyLink.parent.parent
|
||||
if urlTr['class'] == "sl":
|
||||
isSingleStory = False
|
||||
else:
|
||||
isSingleStory = True
|
||||
else:
|
||||
raise exceptions.FailedToDownload("Couldn't find story <%s> on author's page <%s>" % (url, authorurl))
|
||||
|
||||
if isSingleStory:
|
||||
self.story.setMetadata('title', storyLink.text)
|
||||
self.story.setMetadata('description', urlTr.findAll("td")[1].text)
|
||||
self.story.addToList('eroticatags', urlTr.findAll("td")[2].text)
|
||||
date = urlTr.findAll('td')[-1].text
|
||||
self.story.setMetadata('datePublished', makeDate(date, self.dateformat))
|
||||
self.story.setMetadata('dateUpdated',makeDate(date, self.dateformat))
|
||||
|
||||
# find num of pages
|
||||
# find a "3 Pages:" string on the page and parse it
|
||||
pgs = soup1.find("span", "b-pager-caption-t r-d45").string.split(' ')[0]
|
||||
|
||||
# If there are multiple pages, find and request the last page
|
||||
if "1" != pgs:
|
||||
logger.debug("last page number: "+pgs)
|
||||
try:
|
||||
data2 = self._fetchUrl(url1, {'page': pgs})
|
||||
soup2 = bs.BeautifulSoup(data2)
|
||||
[comment.extract() for comment in soup2.findAll(text=lambda text:isinstance(text, bs.Comment))]
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
# TODO: Probably should reformat this
|
||||
raise exceptions.StoryDoesNotExist(url1, {'page': pgs})
|
||||
else:
|
||||
raise e
|
||||
self.chapterUrls = [(storyLink.text, self.url)]
|
||||
else:
|
||||
#If we're already on the last page, copy the soup
|
||||
soup2 = soup1
|
||||
seriesTr = urlTr.previousSibling
|
||||
while seriesTr['class'] != 'ser-ttl':
|
||||
seriesTr = seriesTr.previousSibling
|
||||
m = re.match("^(?P<title>.*?):\s(?P<numChapters>\d+)\sPart\sSeries$", seriesTr.find("strong").text)
|
||||
self.story.setMetadata('title', m.group('title'))
|
||||
self.story.setMetadata('numChapters', int(m.group('numChapters')))
|
||||
|
||||
# parse out the list of chapters
|
||||
chaps = soup2.find('div', id='b-series')
|
||||
if chaps: # may be one post only
|
||||
#self.chapterUrls = [(ch.a.text, ch.a['href']) for ch in chaps.findAll('li')]
|
||||
|
||||
# if there are chapters, lets pull them and title from the
|
||||
# author page because *this* chapter is omitted from the
|
||||
# list on the last page.
|
||||
row = storyLink.parent.parent.previousSibling
|
||||
while row['class'] != 'ser-ttl':
|
||||
row = row.previousSibling
|
||||
|
||||
seriesTitle = stripHTML(row)
|
||||
if seriesTitle:
|
||||
# this regex is deliberately greedy. We want to get the biggest match before a ':'
|
||||
self.story.setMetadata('title', re.match('(.*):[^:]*$', seriesTitle).group(1))
|
||||
else:
|
||||
self.story.setMetadata('title', soup1.h1.string)
|
||||
|
||||
# now chapter list. Assumed oldest to newest.
|
||||
## Walk the chapters
|
||||
chapterTr = seriesTr.nextSibling
|
||||
self.chapterUrls = []
|
||||
row = row.nextSibling
|
||||
|
||||
self.story.setMetadata('datePublished',makeDate(stripHTML(row.find('td',{'class':'dt'})), self.dateformat))
|
||||
while row['class'] == 'sl':
|
||||
# pages include full URLs.
|
||||
self.chapterUrls.append((row.a.string,row.a['href']))
|
||||
if not row.nextSibling:
|
||||
break
|
||||
row = row.nextSibling
|
||||
dates = []
|
||||
descriptions = []
|
||||
while chapterTr is not None and chapterTr['class'] == 'sl':
|
||||
descriptions.append(chapterTr.findAll("td")[1].text)
|
||||
chapterLink = chapterTr.find("td", "fc").find("a")
|
||||
self.chapterUrls.append((chapterLink.text, "http:" + chapterLink["href"]))
|
||||
self.story.addToList('eroticatags', chapterTr.findAll("td")[2].text)
|
||||
dates.append(makeDate(chapterTr.findAll('td')[-1].text, self.dateformat))
|
||||
chapterTr = chapterTr.nextSibling
|
||||
|
||||
row = row.previousSibling
|
||||
self.story.setMetadata('dateUpdated',makeDate(stripHTML(row.find('td',{'class':'dt'})), self.dateformat))
|
||||
|
||||
else: # if one post only
|
||||
self.chapterUrls = [(soup1.h1.string, url1)]
|
||||
self.story.setMetadata('title', soup1.h1.string)
|
||||
## Set description to joint chapter descriptions
|
||||
self.story.setMetadata('description', " / ".join(descriptions))
|
||||
|
||||
# normalize on first chapter URL.
|
||||
self._setURL(self.chapterUrls[0][1])
|
||||
## Set the oldest date as publication date, the newest as update date
|
||||
dates.sort()
|
||||
self.story.setMetadata('datePublished', dates[0])
|
||||
self.story.setMetadata('dateUpdated', dates[-1])
|
||||
|
||||
# reset storyId to first chapter.
|
||||
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
|
||||
# normalize on first chapter URL.
|
||||
self._setURL(self.chapterUrls[0][1])
|
||||
|
||||
self.story.setMetadata('numChapters', len(self.chapterUrls))
|
||||
|
||||
self.story.setMetadata('category', soup1.find('div', 'b-breadcrumbs').findAll('a')[1].string)
|
||||
self.story.setMetadata('description', soup1.find('meta', {'name': 'description'})['content'])
|
||||
# set storyId to 'title-author' to avoid duplicates
|
||||
# self.story.setMetadata('storyId',
|
||||
# re.sub("[^a-z0-9]", "", self.story.getMetadata('title').lower())
|
||||
# + "-"
|
||||
# + re.sub("[^a-z0-9]", "", self.story.getMetadata('author').lower()))
|
||||
|
||||
return
|
||||
|
||||
def getChapterText(self, url):
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
time.sleep(0.5)
|
||||
logger.debug('Getting chapter text from <%s>' % url)
|
||||
data1 = self._fetchUrl(url)
|
||||
soup1 = bs.BeautifulSoup(data1)
|
||||
|
||||
@@ -203,7 +223,7 @@ class LiteroticaSiteAdapter(BaseSiteAdapter):
|
||||
# get story text
|
||||
story1 = soup1.find('div', 'b-story-body-x').p
|
||||
story1.name='div'
|
||||
story1.append('<br>')
|
||||
story1.append('<br />')
|
||||
storytext = self.utf8FromSoup(url,story1)
|
||||
|
||||
# find num pages
|
||||
@@ -220,7 +240,7 @@ class LiteroticaSiteAdapter(BaseSiteAdapter):
|
||||
[comment.extract() for comment in soup2.findAll(text=lambda text:isinstance(text, bs.Comment))]
|
||||
story2 = soup2.find('div', 'b-story-body-x').p
|
||||
story2.name='div'
|
||||
story2.append('<br>')
|
||||
story2.append('<br />')
|
||||
storytext += self.utf8FromSoup(url,story2)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import re
|
||||
import urllib2
|
||||
import urlparse
|
||||
|
||||
from .. import BeautifulSoup
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
from .. import exceptions
|
||||
|
||||
|
||||
def getClass():
|
||||
return NocturnalLightNetAdapter
|
||||
|
||||
|
||||
# yields Tag _and_ NavigableString siblings from the given tag. The
|
||||
# BeautifulSoup findNextSiblings() method for some reasons only returns either
|
||||
# NavigableStrings _or_ Tag objects, not both.
|
||||
def _yield_next_siblings(tag):
|
||||
sibling = tag.nextSibling
|
||||
while sibling:
|
||||
yield sibling
|
||||
sibling = sibling.nextSibling
|
||||
|
||||
|
||||
class NocturnalLightNetAdapter(BaseSiteAdapter):
|
||||
SITE_ABBREVIATION = 'nln'
|
||||
SITE_DOMAIN = 'nocturnal-light.net'
|
||||
BASE_URL = 'http://' + SITE_DOMAIN + '/fanfiction/'
|
||||
STORY_URL_TEMPLATE = BASE_URL + 'story/%s'
|
||||
AUTHORS_URL_TEMPLATE = BASE_URL + 'authors/%s'
|
||||
|
||||
DATETIME_FORMAT = '%m-%d-%y'
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
url_tokens = self.parsedUrl.path.split('/')
|
||||
story_id = url_tokens[url_tokens.index('story') + 1]
|
||||
|
||||
self.story.setMetadata('storyId', story_id)
|
||||
self._setURL(self.STORY_URL_TEMPLATE % story_id)
|
||||
self.story.setMetadata('siteabbrev', self.SITE_ABBREVIATION)
|
||||
|
||||
def _customized_fetch_url(self, url, exception=None, parameters=None):
|
||||
if exception:
|
||||
try:
|
||||
data = self._fetchUrl(url, parameters)
|
||||
except urllib2.HTTPError:
|
||||
raise exception(self.url)
|
||||
# Just let self._fetchUrl throw the exception, don't catch and
|
||||
# customize it.
|
||||
else:
|
||||
data = self._fetchUrl(url, parameters)
|
||||
|
||||
return BeautifulSoup.BeautifulSoup(data)
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return NocturnalLightNetAdapter.SITE_DOMAIN
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return cls.STORY_URL_TEMPLATE % 1234
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape(self.STORY_URL_TEMPLATE[:-2]) + r'\d+.*$'
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
soup = self._customized_fetch_url(self.url)
|
||||
|
||||
# Since no 404 error code we have to raise the exception ourselves.
|
||||
# A title that is just 'by' indicates that there is no author name
|
||||
# and no story title available.
|
||||
if soup.title.string.strip() == 'by':
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
|
||||
# "storycontent" is found in a single-chapter story
|
||||
author_anchor = soup.find('div', id=lambda id: id in ('main', 'storycontent')).h1.a
|
||||
self.story.setMetadata('author', author_anchor.string)
|
||||
|
||||
url_tokens = author_anchor['href'].split('/')
|
||||
author_id = url_tokens[url_tokens.index('authors')+1]
|
||||
self.story.setMetadata('authorId', author_id)
|
||||
self.story.setMetadata('authorUrl', self.AUTHORS_URL_TEMPLATE % author_id)
|
||||
|
||||
chapter_anchors = soup('a', href=lambda href: href and href.startswith('/fanfiction/story/'))
|
||||
for chapter_anchor in chapter_anchors:
|
||||
url = urlparse.urljoin(self.BASE_URL, chapter_anchor['href'])
|
||||
self.chapterUrls.append((chapter_anchor.string, url))
|
||||
|
||||
author_url = urlparse.urljoin(self.BASE_URL, author_anchor['href'])
|
||||
soup = self._customized_fetch_url(author_url)
|
||||
story_id = self.story.getMetadata('storyId')
|
||||
for listbox in soup('div', {'class': 'listbox'}):
|
||||
url_tokens = listbox.a['href'].split('/')
|
||||
# Found the div containing the story's metadata; break the loop and
|
||||
# parse the element
|
||||
if story_id == url_tokens[url_tokens.index('story')+1]:
|
||||
break
|
||||
else:
|
||||
raise exceptions.FailedToDownload(self.url)
|
||||
|
||||
title = listbox.a.string
|
||||
self.story.setMetadata('title', title)
|
||||
|
||||
# No chapter anchors found in the original story URL, so the story has
|
||||
# only a single chapter.
|
||||
if not chapter_anchors:
|
||||
self.chapterUrls.append((title, self.url))
|
||||
|
||||
for b_tag in listbox('b'):
|
||||
key = b_tag.string.strip(':')
|
||||
try:
|
||||
value = b_tag.nextSibling.string.replace('•', '').strip(': ')
|
||||
# This can happen with some fancy markup in the summary. Just
|
||||
# ignore this error and set value to None, the summary parsing
|
||||
# takes care of this
|
||||
except AttributeError:
|
||||
value = None
|
||||
|
||||
if key == 'Summary':
|
||||
contents = []
|
||||
keep_summary_html = self.getConfig('keep_summary_html')
|
||||
|
||||
for sibling in _yield_next_siblings(b_tag):
|
||||
if isinstance(sibling, BeautifulSoup.Tag):
|
||||
if sibling.name == 'b' and sibling.findPreviousSibling().name == 'br':
|
||||
break
|
||||
|
||||
if keep_summary_html:
|
||||
contents.append(self.utf8FromSoup(author_url, sibling))
|
||||
else:
|
||||
contents.append(''.join(sibling(text=True)))
|
||||
else:
|
||||
contents.append(sibling)
|
||||
|
||||
# Pop last break line tag
|
||||
contents.pop()
|
||||
self.story.setMetadata('description', ''.join(contents))
|
||||
|
||||
elif key == 'Category':
|
||||
for sibling in b_tag.findNextSiblings(['a', 'b']):
|
||||
if sibling.name == 'b':
|
||||
break
|
||||
|
||||
self.story.addToList('category', sibling.string)
|
||||
|
||||
elif key == 'Rating':
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
elif key == 'Chapters':
|
||||
self.story.setMetadata('numChapters', int(value))
|
||||
|
||||
# Also parse reviews number which lies right after the chapters
|
||||
# section
|
||||
reviews_anchor = b_tag.findNextSibling('a')
|
||||
reviews = reviews_anchor.string.split(' ')[1].strip('()')
|
||||
self.story.setMetadata('reviews', reviews)
|
||||
|
||||
elif key == 'Completed':
|
||||
self.story.setMetadata('status', 'Completed' if value == 'Yes' else 'In-Progress')
|
||||
|
||||
elif key == 'Date Added':
|
||||
self.story.setMetadata('datePublished', makeDate(value, self.DATETIME_FORMAT))
|
||||
|
||||
elif key == 'Last Updated':
|
||||
self.story.setMetadata('dateUpdated', makeDate(value, self.DATETIME_FORMAT))
|
||||
|
||||
elif key == 'Read':
|
||||
self.story.setMetadata('readings', value.split()[0])
|
||||
|
||||
if self.story.getMetadata('rating') == 'NC-17' and not (self.is_adult or self.getConfig('is_adult')):
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
def getChapterText(self, url):
|
||||
soup = self._customized_fetch_url(url)
|
||||
return self.utf8FromSoup(url, soup.find('div', id='storytext'))
|
||||
@@ -192,7 +192,7 @@ class OneDirectionFanfictionComAdapter(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
|
||||
self.setDescription(url,svalue)
|
||||
|
||||
@@ -163,12 +163,11 @@ class PotionsAndSnitchesNetSiteAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
if 'Published' in label:
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(value), "%d %b %Y"))
|
||||
# limit date values, there's some extra chars.
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(value[:12]), "%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), "%d %b %Y"))
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value[:12]), "%d %b %Y"))
|
||||
|
||||
try:
|
||||
# Find Series name from series URL.
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
import re
|
||||
import urllib2
|
||||
import urlparse
|
||||
|
||||
from .. import BeautifulSoup
|
||||
from ..htmlcleanup import stripHTML
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
from .. import exceptions
|
||||
|
||||
|
||||
def getClass():
|
||||
return SpikeluverComAdapter
|
||||
|
||||
|
||||
# yields Tag _and_ NavigableString siblings from the given tag. The
|
||||
# BeautifulSoup findNextSiblings() method for some reasons only returns either
|
||||
# NavigableStrings _or_ Tag objects, not both.
|
||||
def _yield_next_siblings(tag):
|
||||
sibling = tag.nextSibling
|
||||
while sibling:
|
||||
yield sibling
|
||||
sibling = sibling.nextSibling
|
||||
|
||||
|
||||
class SpikeluverComAdapter(BaseSiteAdapter):
|
||||
SITE_ABBREVIATION = 'slc'
|
||||
SITE_DOMAIN = 'spikeluver.com'
|
||||
|
||||
BASE_URL = 'http://' + SITE_DOMAIN + '/SpuffyRealm/'
|
||||
LOGIN_URL = BASE_URL + 'user.php?action=login'
|
||||
VIEW_STORY_URL_TEMPLATE = BASE_URL + 'viewstory.php?sid=%d'
|
||||
METADATA_URL_SUFFIX = '&index=1'
|
||||
AGE_CONSENT_URL_SUFFIX = '&ageconsent=ok&warning=5'
|
||||
|
||||
DATETIME_FORMAT = '%m/%d/%Y'
|
||||
STORY_DOES_NOT_EXIST_ERROR_TEXT = 'That story does not exist on this archive. You may search for it or return to the home page.'
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
query_data = urlparse.parse_qs(self.parsedUrl.query)
|
||||
story_id = query_data['sid'][0]
|
||||
|
||||
self.story.setMetadata('storyId', story_id)
|
||||
self._setURL(self.VIEW_STORY_URL_TEMPLATE % int(story_id))
|
||||
self.story.setMetadata('siteabbrev', self.SITE_ABBREVIATION)
|
||||
|
||||
def _customized_fetch_url(self, url, exception=None, parameters=None):
|
||||
if exception:
|
||||
try:
|
||||
data = self._fetchUrl(url, parameters)
|
||||
except urllib2.HTTPError:
|
||||
raise exception(self.url)
|
||||
# Just let self._fetchUrl throw the exception, don't catch and
|
||||
# customize it.
|
||||
else:
|
||||
data = self._fetchUrl(url, parameters)
|
||||
|
||||
return BeautifulSoup.BeautifulSoup(data)
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return SpikeluverComAdapter.SITE_DOMAIN
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return cls.VIEW_STORY_URL_TEMPLATE % 1234
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape(self.VIEW_STORY_URL_TEMPLATE[:-2]) + r'\d+$'
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
soup = self._customized_fetch_url(self.url + self.METADATA_URL_SUFFIX)
|
||||
|
||||
errortext_div = soup.find('div', {'class': 'errortext'})
|
||||
if errortext_div:
|
||||
error_text = ''.join(errortext_div(text=True)).strip()
|
||||
if error_text == self.STORY_DOES_NOT_EXIST_ERROR_TEXT:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
|
||||
# No additional login is required, just check for adult
|
||||
pagetitle_div = soup.find('div', id='pagetitle')
|
||||
if pagetitle_div.a['href'].startswith('javascript:'):
|
||||
if not(self.is_adult or self.getConfig('is_adult')):
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
url = ''.join([self.url, self.METADATA_URL_SUFFIX, self.AGE_CONSENT_URL_SUFFIX])
|
||||
soup = self._customized_fetch_url(url)
|
||||
|
||||
pagetitle_div = soup.find('div', id='pagetitle')
|
||||
self.story.setMetadata('title', stripHTML(pagetitle_div.a))
|
||||
|
||||
author_anchor = pagetitle_div.a.findNextSibling('a')
|
||||
url = urlparse.urljoin(self.BASE_URL, author_anchor['href'])
|
||||
components = urlparse.urlparse(url)
|
||||
query_data = urlparse.parse_qs(components.query)
|
||||
|
||||
self.story.setMetadata('author', stripHTML(author_anchor))
|
||||
self.story.setMetadata('authorId', query_data['uid'])
|
||||
self.story.setMetadata('authorUrl', url)
|
||||
|
||||
sort_div = soup.find('div', id='sort')
|
||||
self.story.setMetadata('reviews', stripHTML(sort_div('a')[1]))
|
||||
|
||||
listbox_tag = soup.find('div', {'class': 'listbox'})
|
||||
for span_tag in listbox_tag('span'):
|
||||
key = span_tag.string.strip(' :')
|
||||
try:
|
||||
value = stripHTML(span_tag.nextSibling)
|
||||
# This can happen with some fancy markup in the summary. Just
|
||||
# ignore this error and set value to None, the summary parsing
|
||||
# takes care of this
|
||||
except AttributeError:
|
||||
value = None
|
||||
|
||||
if key == 'Summary':
|
||||
contents = []
|
||||
keep_summary_html = self.getConfig('keep_summary_html')
|
||||
|
||||
for sibling in _yield_next_siblings(span_tag):
|
||||
if isinstance(sibling, BeautifulSoup.Tag):
|
||||
# Encountered next label, break. Not as bad as other
|
||||
# e-fiction sites, let's hope this is enough for proper
|
||||
# parsing.
|
||||
if sibling.name == 'span' and sibling.get('class', None) == 'label':
|
||||
break
|
||||
|
||||
if keep_summary_html:
|
||||
contents.append(self.utf8FromSoup(self.url, sibling))
|
||||
else:
|
||||
contents.append(''.join(sibling(text=True)))
|
||||
else:
|
||||
contents.append(sibling)
|
||||
|
||||
# Remove the preceding break line tag and other crud
|
||||
contents.pop()
|
||||
contents.pop()
|
||||
self.story.setMetadata('description', ''.join(contents))
|
||||
|
||||
elif key == 'Rated':
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
elif key == 'Categories':
|
||||
for sibling in span_tag.findNextSiblings(['a', 'br']):
|
||||
if sibling.name == 'br':
|
||||
break
|
||||
|
||||
self.story.addToList('category', stripHTML(sibling))
|
||||
|
||||
# Seems to be always "None" for some reason
|
||||
elif key == 'Characters':
|
||||
for sibling in span_tag.findNextSiblings(['a', 'br']):
|
||||
if sibling.name == 'br':
|
||||
break
|
||||
self.story.addToList('characters', stripHTML(sibling))
|
||||
|
||||
elif key == 'Genres':
|
||||
for sibling in span_tag.findNextSiblings(['a', 'br']):
|
||||
if sibling.name == 'br':
|
||||
break
|
||||
|
||||
self.story.addToList('genre', stripHTML(sibling))
|
||||
|
||||
elif key == 'Warnings':
|
||||
for sibling in span_tag.findNextSiblings(['a', 'br']):
|
||||
if sibling.name == 'br':
|
||||
break
|
||||
self.story.addToList('warnings', stripHTML(sibling))
|
||||
|
||||
# Challenges
|
||||
|
||||
elif key == 'Series':
|
||||
a = span_tag.findNextSibling('a')
|
||||
if not a:
|
||||
continue
|
||||
self.story.setMetadata('series', stripHTML(a))
|
||||
self.story.setMetadata('seriesUrl', urlparse.urljoin(self.BASE_URL, a['href']))
|
||||
|
||||
elif key == 'Chapters':
|
||||
self.story.setMetadata('numChapters', int(value))
|
||||
|
||||
elif key == 'Completed':
|
||||
self.story.setMetadata('status', 'Completed' if value == 'Yes' else 'In-Progress')
|
||||
|
||||
elif key == 'Word count':
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
elif key == 'Published':
|
||||
self.story.setMetadata('datePublished', makeDate(value, self.DATETIME_FORMAT))
|
||||
|
||||
elif key == 'Updated':
|
||||
self.story.setMetadata('dateUpdated', makeDate(value, self.DATETIME_FORMAT))
|
||||
|
||||
for p_tag in listbox_tag.findNextSiblings('p'):
|
||||
chapter_anchor = p_tag.find('a', href=lambda href: href and href.startswith('viewstory.php?sid='))
|
||||
if not chapter_anchor:
|
||||
continue
|
||||
|
||||
title = stripHTML(chapter_anchor)
|
||||
url = urlparse.urljoin(self.BASE_URL, chapter_anchor['href'])
|
||||
self.chapterUrls.append((title, url))
|
||||
|
||||
def getChapterText(self, url):
|
||||
url += self.AGE_CONSENT_URL_SUFFIX
|
||||
soup = self._customized_fetch_url(url)
|
||||
return self.utf8FromSoup(url, soup.find('div', id='story'))
|
||||
@@ -62,7 +62,7 @@ class SquidgeOrgPejaAdapter(BaseSiteAdapter):
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/peja/cgi-bin/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
self._setURL('https://' + self.getSiteDomain() + '/peja/cgi-bin/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','wwomb')
|
||||
@@ -83,10 +83,10 @@ class SquidgeOrgPejaAdapter(BaseSiteAdapter):
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://"+self.getSiteDomain()+"/peja/cgi-bin/viewstory.php?sid=1234"
|
||||
return "https://"+self.getSiteDomain()+"/peja/cgi-bin/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain()+"/")+"~?"+re.escape("peja/cgi-bin/viewstory.php?sid=")+r"\d+$"
|
||||
return r"https?"+re.escape("://"+self.getSiteDomain()+"/")+r"~?"+re.escape("peja/cgi-bin/viewstory.php?sid=")+r"\d+$"
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
@@ -116,7 +116,7 @@ class SquidgeOrgPejaAdapter(BaseSiteAdapter):
|
||||
# Find authorid and URL from... author url.
|
||||
author = soup.find('div', {'id':"pagetitle"}).find('a')
|
||||
self.story.setMetadata('authorId',author['href'].split('=')[1])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/peja/cgi-bin/'+author['href'])
|
||||
self.story.setMetadata('authorUrl','https://'+self.host+'/peja/cgi-bin/'+author['href'])
|
||||
self.story.setMetadata('author',author.string)
|
||||
|
||||
authorSoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')))
|
||||
@@ -131,7 +131,7 @@ class SquidgeOrgPejaAdapter(BaseSiteAdapter):
|
||||
chapterselect=soup.find('select',{'name':'chapter'})
|
||||
if chapterselect:
|
||||
for ch in chapterselect.findAll('option'):
|
||||
self.chapterUrls.append((stripHTML(ch),'http://'+self.host+'/peja/cgi-bin/viewstory.php?sid='+self.story.getMetadata('storyId')+'&chapter='+ch['value']))
|
||||
self.chapterUrls.append((stripHTML(ch),'https://'+self.host+'/peja/cgi-bin/viewstory.php?sid='+self.story.getMetadata('storyId')+'&chapter='+ch['value']))
|
||||
else:
|
||||
self.chapterUrls.append((title,url))
|
||||
|
||||
@@ -207,7 +207,7 @@ class SquidgeOrgPejaAdapter(BaseSiteAdapter):
|
||||
# http://www.squidge.org/peja/cgi-bin/series.php?seriesid=254
|
||||
a = titleblock.find('a', href=re.compile(r"series.php\?seriesid=\d+"))
|
||||
series_name = a.string
|
||||
series_url = 'http://'+self.host+'/peja/cgi-bin/'+a['href']
|
||||
series_url = 'https://'+self.host+'/peja/cgi-bin/'+a['href']
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url))
|
||||
|
||||
@@ -66,18 +66,18 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
return "http://"+self.getSiteDomain()+"/s/1234 http://"+self.getSiteDomain()+"/s/1234:4010"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain())+r"/s/\d+((:\d+)?(;\d+)?$|(:i)?$)"
|
||||
return re.escape("http://"+self.getSiteDomain())+r"/s/\d+((:\d+)?(;\d+)?$|(:i)?$)?"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
if 'Free Registration' in data \
|
||||
if self.needToLogin \
|
||||
or 'Free Registration' in data \
|
||||
or "Invalid Password!" in data \
|
||||
or "Invalid User Name!" in data \
|
||||
or "Log In" in data \
|
||||
or "Access to unlinked chapters requires" in data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
self.needToLogin = True
|
||||
return self.needToLogin
|
||||
|
||||
def performLogin(self, url):
|
||||
params = {}
|
||||
@@ -114,11 +114,15 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
url = self.url
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
self.needToLogin = False
|
||||
try:
|
||||
data = self._fetchUrl(url+":i")
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
elif e.code == 401:
|
||||
self.needToLogin = True
|
||||
data = ''
|
||||
else:
|
||||
raise e
|
||||
|
||||
@@ -171,7 +175,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
|
||||
a = asoup.findAll('td', {'class' : 'lc2'})
|
||||
for lc2 in a:
|
||||
if lc2.find('a')['href'] == '/s/'+self.story.getMetadata('storyId'):
|
||||
if lc2.find('a', href=re.compile(r'^/s/'+self.story.getMetadata('storyId'))):
|
||||
i=1
|
||||
break
|
||||
if a[len(a)-1] == lc2:
|
||||
|
||||
@@ -219,15 +219,18 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
|
||||
elif idstr == '81':
|
||||
self.story.addToList('category',u'Pitch Perfect')
|
||||
self.story.addToList('characters','Chloe B.')
|
||||
elif idstr == '82':
|
||||
self.story.addToList('characters','Henry (Once Upon a Time)')
|
||||
self.story.addToList('category',u'Once Upon a Time (TV)')
|
||||
elif idstr == '83':
|
||||
self.story.addToList('category',u'Rizzoli & Isles')
|
||||
self.story.addToList('characters','J. Rizzoli')
|
||||
self.story.addToList('category',u'Pitch Perfect')
|
||||
self.story.addToList('characters','Chloe B.')
|
||||
self.story.addToList('ships','Chloe B. & J. Rizzoli')
|
||||
elif idstr == '82':
|
||||
self.story.addToList('characters','Henry (Once Upon a Time)')
|
||||
self.story.addToList('category',u'Once Upon a Time (TV)')
|
||||
elif idstr == '90':
|
||||
self.story.setMetadata('characters','Henry (Once Upon a Time)')
|
||||
self.story.setMetadata('category',u'Once Upon a Time (TV)')
|
||||
else:
|
||||
self.story.addToList('category','Harry Potter')
|
||||
self.story.addToList('category','Furbie')
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2014 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 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
|
||||
|
||||
|
||||
class TheMapleBookshelfComSiteAdapter(BaseSiteAdapter):
|
||||
"""
|
||||
Use Printable version which is easier to parse and has everything in one
|
||||
page and cache between extractChapterUrlsAndMetadata and getChapterText
|
||||
"""
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
self.story.setMetadata('siteabbrev','maplebook')
|
||||
self.decode = ["Windows-1252", "utf8"]
|
||||
self.story.setMetadata('storyId', re.compile(self.getSiteURLPattern()).match(url).group('storyId'))
|
||||
self._setURL("http://%s/Literati/viewstory.php?sid=%s" % (self.getSiteDomain(), self.story.getMetadata('storyId')))
|
||||
self.dateformat = "%b %d, %Y"
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return 'themaplebookshelf.com'
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://www.themaplebookshelf.com/Literati/viewstory.php?sid=227 http://themaplebookshelf.com/Literati/viewstory.php?sid=227&chapter=2"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return r"http://themaplebookshelf.com/Literati/viewstory.php\?sid=(?P<storyId>\d+)"
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
logger.debug(self.url)
|
||||
self._setURL(self.url + "&action=printable")
|
||||
try:
|
||||
html = self._fetchUrl(self.url)
|
||||
soup = bs.BeautifulSoup(html)
|
||||
# #strip comments from soup
|
||||
# [comment.extract() for comment in soup1.findAll(text=lambda text:isinstance(text, bs.Comment))]
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
## title + author
|
||||
pagetitleDiv = soup.find("div", {"id": "pagetitle"})
|
||||
self.story.setMetadata('title', pagetitleDiv.find("a").text)
|
||||
authorLink = pagetitleDiv.findAll("a")[1]
|
||||
self.story.setMetadata('author', authorLink.text)
|
||||
self.story.setMetadata('authorUrl', "http://" + self.getSiteDomain() + "/" + authorLink['href'])
|
||||
self.story.setMetadata('authorId', re.search("\d+", authorLink['href']).group(0))
|
||||
|
||||
## Description
|
||||
description = ""
|
||||
summaryEnd = soup.find("div", "content").find("span", "label").nextSibling
|
||||
while summaryEnd is not None:
|
||||
description += stripHTML(summaryEnd)
|
||||
summaryEnd = summaryEnd.nextSibling
|
||||
if type(summaryEnd) != bs.NavigableString and summaryEnd.name == 'br':
|
||||
break
|
||||
self.story.setMetadata('description', description)
|
||||
|
||||
## General Metadata
|
||||
for kSpan in soup.findAll("span", "label"):
|
||||
k = kSpan.text.strip().replace(':', '')
|
||||
vSpan = kSpan.nextSibling
|
||||
if k == 'Summary:' or not vSpan or not vSpan.string:
|
||||
continue
|
||||
v = vSpan.string.strip()
|
||||
if v == 'None':
|
||||
continue
|
||||
logger.debug("%s '%s'" %(k, v))
|
||||
if k == 'Genre':
|
||||
for genre in v.split(", "):
|
||||
self.story.addToList('genre', genre)
|
||||
elif k == 'Chapters':
|
||||
self.story.setMetadata('numChapters', int(v))
|
||||
elif k == 'Word count':
|
||||
self.story.setMetadata('numWords', v)
|
||||
elif k == 'Published':
|
||||
self.story.setMetadata('datePublished', makeDate(v, self.dateformat))
|
||||
elif k == 'Updated':
|
||||
self.story.setMetadata('dateUpdated', makeDate(v, self.dateformat))
|
||||
# TODO: Series, Warnings
|
||||
|
||||
## Chapter URLs (fragment identifiers in the document, so we don' need to fetch so much)
|
||||
for chapterNumB in soup.findAll("b", text=re.compile("^\d+\.$")):
|
||||
self.chapterUrls.append((
|
||||
chapterNumB.parent.parent.find("a").text,
|
||||
self.url + chapterNumB.parent.parent.find("a")["href"]
|
||||
))
|
||||
|
||||
## fix all local image 'src' to absolute
|
||||
for img in soup.findAll("img", {"src": re.compile("^(?!http)")}):
|
||||
img['src'] = re.sub("viewstory.php\?.*", "", self.url) + img['src']
|
||||
|
||||
self.html = soup
|
||||
|
||||
def getChapterText(self, url):
|
||||
logger.debug('Getting chapter text from <%s>' % url)
|
||||
anchor = url.replace(self.url, "")
|
||||
anchor = anchor.replace("#", "")
|
||||
chapterDiv = self.html.find("a", {"name": anchor}).parent.findNext("div", "chapter")
|
||||
return self.utf8FromSoup(self.url, chapterDiv)
|
||||
|
||||
def getClass():
|
||||
return TheMapleBookshelfComSiteAdapter
|
||||
@@ -0,0 +1,238 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
FFDL Adapter for TolkienFanFiction.com.
|
||||
|
||||
Chapter URL: http://www.tolkienfanfiction.com/Story_Read_Chapter.php?CHid=1234
|
||||
Metadata
|
||||
Link to Story URL [Index]
|
||||
chapterTitle
|
||||
storyTitle
|
||||
Story URL: http://www.tolkienfanfiction.com/Story_Read_Head.php?STid=1034
|
||||
Metadata
|
||||
Links to Chapter URLs
|
||||
storyTitle
|
||||
chapterTitle[s]
|
||||
author
|
||||
authorId
|
||||
authorUrl
|
||||
numChapters
|
||||
wordCount
|
||||
description/summary
|
||||
rating TODO
|
||||
genre TODO
|
||||
Characters
|
||||
Ages (specific) TODO
|
||||
Search: http://www.tolkienfanfiction.com/Story_Chapter_Search.php?text=From+Wilderness+to+Cities+White&field=1&type=3&search=Search
|
||||
Strategy
|
||||
Search by exact phrase for styo
|
||||
Metadata
|
||||
dateUpdated
|
||||
Parameters
|
||||
field (field to search)
|
||||
1: title
|
||||
2: description
|
||||
3: chapter text
|
||||
type (any, all or exact phrase)
|
||||
1: any
|
||||
2: all
|
||||
3: exact phrase
|
||||
|
||||
"""
|
||||
# Copyright 2014 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 urllib
|
||||
import urllib2
|
||||
import urlparse
|
||||
import string
|
||||
|
||||
from .. import BeautifulSoup as bs
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
def _is_story_url(url):
|
||||
return "Story_Read_Head.php" in url
|
||||
|
||||
def _latinize(text):
|
||||
"""
|
||||
See http://stackoverflow.com/a/19114706/201318
|
||||
"""
|
||||
src = u"áâäÉéêëíóôöúû"
|
||||
tgt = u"aaaEeeeiooouu"
|
||||
src_ord = [ord(char) for char in src]
|
||||
translate_table = dict(zip(src_ord, tgt))
|
||||
return text.translate(translate_table)
|
||||
|
||||
def _fix_broken_markup(html):
|
||||
"""Replaces invalid comment tags"""
|
||||
if html.startswith("<CENTER>"):
|
||||
logger.error("TolkienFanFiction.com couldn't handle this request: '%s'" % html)
|
||||
html = re.sub("<!-.+?->", "", html)
|
||||
return html
|
||||
|
||||
|
||||
class TolkienFanfictionAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.decode = ["ISO-8859-1", "Windows-1252"]
|
||||
|
||||
self.story.setMetadata('siteabbrev','tolkien')
|
||||
|
||||
self.dateformat = '%B %d, %Y'
|
||||
|
||||
self._normalizeURL(url)
|
||||
|
||||
def _normalizeURL(self, url):
|
||||
if _is_story_url(url):
|
||||
self.story.setMetadata('storyId', re.compile(self.getSiteURLPattern()).match(url).group('storyId'))
|
||||
self._setURL('http://' + self.getSiteDomain() + '/Story_Read_Head.php?STid=' + self.story.getMetadata('storyId'))
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return 'tolkienfanfiction.com'
|
||||
|
||||
@classmethod
|
||||
def getAcceptDomains(cls):
|
||||
return ['tolkienfanfiction.com', 'www.tolkienfanfiction.com']
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(self):
|
||||
return 'http://www.tolkienfanfiction.com/Story_Read_Head.php?STid=1034 http://www.tolkienfanfiction.com/Story_Read_Chapter.php?CHid=4945'
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return r"http://(?:www.)?tolkienfanfiction.com/(?:Story_Read_Chapter\.php\?CH|Story_Read_Head\.php\?ST)id=(?P<storyId>[0-9]+)"
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
if not _is_story_url(self.url):
|
||||
# Get the link to the index page
|
||||
try:
|
||||
chapterHtml = _fix_broken_markup(self._fetchUrl(self.url))
|
||||
chapterSoup = bs.BeautifulSoup(chapterHtml)
|
||||
indexLink = chapterSoup.find("a", text="[Index]").parent
|
||||
self._normalizeURL('http://' + self.getSiteDomain() + '/' + indexLink.get('href'))
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
logger.debug("Determined index page: <%s>" % self.url)
|
||||
|
||||
try:
|
||||
indexHtml = _fix_broken_markup(self._fetchUrl(self.url))
|
||||
soup = bs.BeautifulSoup(indexHtml)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
# chapterUrls
|
||||
for pfLink in soup.findAll("a", text='[PF] '):
|
||||
chapterLink = pfLink.parent.findNext("a")
|
||||
chapterTitle = chapterLink.string
|
||||
if self.getConfig('strip_chapter_numeral'):
|
||||
chapterTitle = re.sub("^\d+:", "", chapterTitle)
|
||||
chapterUrl = 'http://' + self.host + '/' + chapterLink['href']
|
||||
self.chapterUrls.append((chapterTitle, chapterUrl))
|
||||
numChapters = len(self.chapterUrls)
|
||||
self.story.setMetadata('numChapters', numChapters)
|
||||
logger.debug('Number of Chapters: %s' % numChapters)
|
||||
|
||||
# title
|
||||
title = soup.find("table", "headertitle").find("tr").contents[1].string
|
||||
logger.debug("Title: '%s'" % title)
|
||||
self.story.setMetadata('title', title)
|
||||
|
||||
# author
|
||||
authorLink = soup.find("a", {"href":lambda x: x.startswith("Author_Profile.php")})
|
||||
authorName = authorLink.find("b").string
|
||||
authorHref = authorLink['href']
|
||||
authorUrl = 'http:' + self.host + '/' + authorHref
|
||||
authorId = authorHref[authorHref.index('=')+1:]
|
||||
self.story.setMetadata('author', authorName)
|
||||
self.story.setMetadata('authorId', authorId)
|
||||
self.story.setMetadata('authorUrl', authorUrl)
|
||||
logger.debug("Author: %s [%s] @ <%s>" % (authorId, authorName, authorUrl))
|
||||
|
||||
# numWords
|
||||
numWordsMatch = re.search("Word Count: (\d+)<BR>", indexHtml)
|
||||
if numWordsMatch:
|
||||
numWords = numWordsMatch.group(1)
|
||||
logger.debug('Number of words: %s' % numWords)
|
||||
self.story.setMetadata('numWords', numWords)
|
||||
|
||||
# description
|
||||
description = soup.find("b", text="Description:").parent.nextSibling.nextSibling
|
||||
self.story.setDescription(description)
|
||||
logger.debug("Summary: '%s'" % description)
|
||||
|
||||
# characters
|
||||
characters = soup.find("b", text="Characters").parent.nextSibling.nextSibling.nextSibling
|
||||
for character in characters.split(", "):
|
||||
self.story.addToList('characters', character)
|
||||
logger.debug("Characters: %s" % self.story.getMetadata('characters'))
|
||||
|
||||
logger.debug('Title as `str`: ' + str(title))
|
||||
# For publication date we need to search
|
||||
try:
|
||||
queryString = urllib.urlencode((
|
||||
('type', 3),
|
||||
('field', 1),
|
||||
# need translate here for the weird accented letters
|
||||
('text', _latinize(title)),
|
||||
('search', 'Search'),
|
||||
))
|
||||
searchUrl = 'http://%s/Story_Chapter_Search.php?%s' % (self.host, queryString)
|
||||
logger.debug("Search URL: <%s>" % searchUrl)
|
||||
searchHtml = _fix_broken_markup(self._fetchUrl(searchUrl))
|
||||
searchSoup = bs.BeautifulSoup(searchHtml)
|
||||
date = searchSoup.find(text="Updated:").nextSibling.string
|
||||
logger.debug("Last Updated: '%s'" % date)
|
||||
self.story.setMetadata('dateUpdated', makeDate(date, self.dateformat))
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
def getChapterText(self, url):
|
||||
|
||||
logger.debug('Downloading chapter <%s>' % url)
|
||||
|
||||
time.sleep(0.5)
|
||||
htmldata = _fix_broken_markup(self._fetchUrl(url))
|
||||
soup = bs.BeautifulSoup(htmldata)
|
||||
|
||||
#strip comments from soup
|
||||
[comment.extract() for comment in soup.findAll(text=lambda text:isinstance(text, bs.Comment))]
|
||||
|
||||
# Strip redundant headings
|
||||
[font.parent.extract() for font in soup.findAll("font", {"size": "4"})]
|
||||
|
||||
# get story text
|
||||
textDiv = soup.find("div", "text")
|
||||
return self.utf8FromSoup(url, textDiv)
|
||||
|
||||
def getClass():
|
||||
return TolkienFanfictionAdapter
|
||||
@@ -0,0 +1,232 @@
|
||||
import re
|
||||
import urllib2
|
||||
import urlparse
|
||||
|
||||
from .. import BeautifulSoup
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
from .. import exceptions
|
||||
|
||||
|
||||
def getClass():
|
||||
return Voracity2EficComAdapter
|
||||
|
||||
|
||||
# yields Tag _and_ NavigableString siblings from the given tag. The
|
||||
# BeautifulSoup findNextSiblings() method for some reasons only returns either
|
||||
# NavigableStrings _or_ Tag objects, not both.
|
||||
def _yield_next_siblings(tag):
|
||||
sibling = tag.nextSibling
|
||||
while sibling:
|
||||
yield sibling
|
||||
sibling = sibling.nextSibling
|
||||
|
||||
|
||||
class Voracity2EficComAdapter(BaseSiteAdapter):
|
||||
SITE_ABBREVIATION = 'voe'
|
||||
SITE_DOMAIN = 'voracity2.e-fic.com'
|
||||
|
||||
BASE_URL = 'http://' + SITE_DOMAIN + '/'
|
||||
LOGIN_URL = BASE_URL + 'user.php?action=login'
|
||||
VIEW_STORY_URL_TEMPLATE = BASE_URL + 'viewstory.php?sid=%d'
|
||||
METADATA_URL_SUFFIX = '&index=1'
|
||||
AGE_CONSENT_URL_SUFFIX = '&ageconsent=ok&warning=4'
|
||||
|
||||
DATETIME_FORMAT = '%m/%d/%Y'
|
||||
REQUIRED_SKIN = 'Simple Elegance'
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
query_data = urlparse.parse_qs(self.parsedUrl.query)
|
||||
story_id = query_data['sid'][0]
|
||||
|
||||
self.story.setMetadata('storyId', story_id)
|
||||
self._setURL(self.VIEW_STORY_URL_TEMPLATE % int(story_id))
|
||||
self.story.setMetadata('siteabbrev', self.SITE_ABBREVIATION)
|
||||
|
||||
self.is_logged_in = False
|
||||
|
||||
def _login(self):
|
||||
# Apparently self.password is only set when login fails, i.e.
|
||||
# the FailedToLogin exception is raised, so the adapter gets new
|
||||
# login data and tries again
|
||||
if self.password:
|
||||
password = self.password
|
||||
username = self.username
|
||||
else:
|
||||
username = self.getConfig('username')
|
||||
password = self.getConfig('password')
|
||||
|
||||
parameters = {
|
||||
'penname': username,
|
||||
'password': password,
|
||||
'submit': 'Submit'}
|
||||
|
||||
class CustomizedFailedToLogin(exceptions.FailedToLogin):
|
||||
def __init__(self, url, passwdonly=False):
|
||||
# Use username variable from outer scope
|
||||
exceptions.FailedToLogin.__init__(self, url, username, passwdonly)
|
||||
|
||||
soup = self._customized_fetch_url(self.LOGIN_URL, CustomizedFailedToLogin, parameters)
|
||||
div = soup.find('div', id='useropts')
|
||||
if not div:
|
||||
raise CustomizedFailedToLogin(self.LOGIN_URL)
|
||||
|
||||
self.is_logged_in = True
|
||||
|
||||
def _customized_fetch_url(self, url, exception=None, parameters=None):
|
||||
if exception:
|
||||
try:
|
||||
data = self._fetchUrl(url, parameters)
|
||||
except urllib2.HTTPError:
|
||||
raise exception(self.url)
|
||||
# Just let self._fetchUrl throw the exception, don't catch and
|
||||
# customize it.
|
||||
else:
|
||||
data = self._fetchUrl(url, parameters)
|
||||
|
||||
return BeautifulSoup.BeautifulSoup(data)
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return Voracity2EficComAdapter.SITE_DOMAIN
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return cls.VIEW_STORY_URL_TEMPLATE % 1234
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape(self.VIEW_STORY_URL_TEMPLATE[:-2]) + r'\d+$'
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
soup = self._customized_fetch_url(self.url + self.METADATA_URL_SUFFIX)
|
||||
|
||||
# Check if the story is for "Registered Users Only", i.e. has adult
|
||||
# content. Based on the "is_adult" attributes either login or raise an
|
||||
# error.
|
||||
errortext_div = soup.find('div', {'class': 'errortext'})
|
||||
if errortext_div:
|
||||
error_text = ''.join(errortext_div(text=True)).strip()
|
||||
if error_text == 'Registered Users Only':
|
||||
if not (self.is_adult or self.getConfig('is_adult')):
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
self._login()
|
||||
else:
|
||||
# This case usually occurs when the story doesn't exist, but
|
||||
# might potentially be something else, so just raise
|
||||
# FailedToDownload exception with the found error text.
|
||||
raise exceptions.FailedToDownload(error_text)
|
||||
|
||||
url = ''.join([self.url, self.METADATA_URL_SUFFIX, self.AGE_CONSENT_URL_SUFFIX])
|
||||
soup = self._customized_fetch_url(url)
|
||||
|
||||
# If logged in and the skin doesn't match the required skin throw an
|
||||
# error
|
||||
if self.is_logged_in:
|
||||
skin = soup.find('select', {'name': 'skin'}).find('option', selected=True)['value']
|
||||
if skin != self.REQUIRED_SKIN:
|
||||
raise exceptions.FailedToDownload('Required skin "%s" must be set in preferences' % self.REQUIRED_SKIN)
|
||||
|
||||
pagetitle_div = soup.find('div', id='pagetitle')
|
||||
self.story.setMetadata('title', pagetitle_div.a.string)
|
||||
|
||||
author_anchor = pagetitle_div.a.findNextSibling('a')
|
||||
url = urlparse.urljoin(self.BASE_URL, author_anchor['href'])
|
||||
components = urlparse.urlparse(url)
|
||||
query_data = urlparse.parse_qs(components.query)
|
||||
|
||||
self.story.setMetadata('author', author_anchor.string)
|
||||
self.story.setMetadata('authorId', query_data['uid'])
|
||||
self.story.setMetadata('authorUrl', url)
|
||||
|
||||
sort_div = soup.find('div', id='sort')
|
||||
self.story.setMetadata('reviews', sort_div('a')[1].string)
|
||||
|
||||
for b_tag in soup.find('div', {'class': 'listbox'})('b'):
|
||||
key = b_tag.string.strip(' :')
|
||||
try:
|
||||
value = b_tag.nextSibling.string.strip()
|
||||
# This can happen with some fancy markup in the summary. Just
|
||||
# ignore this error and set value to None, the summary parsing
|
||||
# takes care of this
|
||||
except AttributeError:
|
||||
value = None
|
||||
|
||||
if key == 'Summary':
|
||||
contents = []
|
||||
keep_summary_html = self.getConfig('keep_summary_html')
|
||||
|
||||
for sibling in _yield_next_siblings(b_tag):
|
||||
if isinstance(sibling, BeautifulSoup.Tag):
|
||||
# Encountered next label, break. This method is the
|
||||
# safest and most reliable I could think of. Blame
|
||||
# e-fiction sites that allow their users to include
|
||||
# arbitrary markup into their summaries and the
|
||||
# horrible HTML markup.
|
||||
if sibling.name == 'b' and sibling.findPreviousSibling().name == 'br':
|
||||
break
|
||||
|
||||
if keep_summary_html:
|
||||
contents.append(self.utf8FromSoup(self.url, sibling))
|
||||
else:
|
||||
contents.append(''.join(sibling(text=True)))
|
||||
else:
|
||||
contents.append(sibling)
|
||||
|
||||
# Remove the preceding break line tag and other crud
|
||||
contents.pop()
|
||||
contents.pop()
|
||||
self.story.setMetadata('description', ''.join(contents))
|
||||
|
||||
elif key == 'Rating':
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
elif key == 'Category':
|
||||
for sibling in b_tag.findNextSiblings(['a', 'br']):
|
||||
if sibling.name == 'br':
|
||||
break
|
||||
self.story.addToList('category', sibling.string)
|
||||
|
||||
# Seems to be always "None" for some reason
|
||||
elif key == 'Characters':
|
||||
for sibling in b_tag.findNextSiblings(['a', 'br']):
|
||||
if sibling.name == 'br':
|
||||
break
|
||||
self.story.addToList('characters', sibling.string)
|
||||
|
||||
elif key == 'Series':
|
||||
a = b_tag.findNextSibling('a')
|
||||
if not a:
|
||||
continue
|
||||
self.story.setMetadata('series', a.string)
|
||||
self.story.setMetadata('seriesUrl', urlparse.urljoin(self.BASE_URL, a['href']))
|
||||
|
||||
elif key == 'Chapter':
|
||||
self.story.setMetadata('numChapters', int(value))
|
||||
|
||||
elif key == 'Completed':
|
||||
self.story.setMetadata('status', 'Completed' if value == 'Yes' else 'In-Progress')
|
||||
|
||||
elif key == 'Words':
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
elif key == 'Read':
|
||||
self.story.setMetadata('readings', value)
|
||||
|
||||
elif key == 'Published':
|
||||
self.story.setMetadata('datePublished', makeDate(value, self.DATETIME_FORMAT))
|
||||
|
||||
elif key == 'Updated':
|
||||
self.story.setMetadata('dateUpdated', makeDate(value, self.DATETIME_FORMAT))
|
||||
|
||||
for b_tag in soup.find('div', id='output').findNextSiblings('b'):
|
||||
chapter_anchor = b_tag.a
|
||||
title = chapter_anchor.string
|
||||
url = urlparse.urljoin(self.BASE_URL, chapter_anchor['href'])
|
||||
self.chapterUrls.append((title, url))
|
||||
|
||||
def getChapterText(self, url):
|
||||
url += self.AGE_CONSENT_URL_SUFFIX
|
||||
soup = self._customized_fetch_url(url)
|
||||
return self.utf8FromSoup(url, soup.find('div', id='story'))
|
||||
@@ -125,6 +125,10 @@ class WraithBaitComAdapter(BaseSiteAdapter):
|
||||
rating=pt.text.split('[')[1].split(']')[0]
|
||||
self.story.setMetadata('rating', rating)
|
||||
|
||||
st = soup.find('div', {'class' : 'storytitle'})
|
||||
a = st.findAll('a', href=re.compile(r'reviews.php\?type=ST&item='+self.story.getMetadata('storyId')+"$"))[1] # second one.
|
||||
self.story.setMetadata('reviews',stripHTML(a))
|
||||
|
||||
# Find the chapters:
|
||||
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")):
|
||||
# include author on chapters if multiple authors.
|
||||
|
||||
@@ -160,13 +160,13 @@ class BaseSiteAdapter(Configurable):
|
||||
req = u2.Request(url,
|
||||
data=urllib.urlencode(parameters),
|
||||
headers=headers)
|
||||
return self._decode(self.opener.open(req).read())
|
||||
return self._decode(self.opener.open(req,None,float(self.getConfig('connect_timeout',30.0))).read())
|
||||
|
||||
def _fetchUrlRaw(self, url, parameters=None):
|
||||
if parameters != None:
|
||||
return self.opener.open(url.replace(' ','%20'),urllib.urlencode(parameters)).read()
|
||||
return self.opener.open(url.replace(' ','%20'),urllib.urlencode(parameters),float(self.getConfig('connect_timeout',30.0))).read()
|
||||
else:
|
||||
return self.opener.open(url.replace(' ','%20')).read()
|
||||
return self.opener.open(url.replace(' ','%20'),None,float(self.getConfig('connect_timeout',30.0))).read()
|
||||
|
||||
def set_sleep(self,val):
|
||||
print("\n===========\n set sleep time %s\n==========="%val)
|
||||
@@ -188,9 +188,9 @@ class BaseSiteAdapter(Configurable):
|
||||
try:
|
||||
return self._decode(self._fetchUrlRaw(url,parameters))
|
||||
except u2.HTTPError, he:
|
||||
excpt=he
|
||||
if he.code == 404:
|
||||
logger.warn("Caught an exception reading URL: %s Exception %s."%(unicode(url),unicode(he)))
|
||||
excpt=he
|
||||
break # break out on 404
|
||||
except Exception, e:
|
||||
excpt=e
|
||||
@@ -297,9 +297,10 @@ class BaseSiteAdapter(Configurable):
|
||||
@classmethod
|
||||
def getSiteExampleURLs(self):
|
||||
"""
|
||||
Return a string of space separated example URLs.
|
||||
Needs to be overriden in each adapter class. It's the adapter
|
||||
writer's responsibility to make sure the example(s) pass the
|
||||
URL validate.
|
||||
validateURL method.
|
||||
"""
|
||||
return 'no such example'
|
||||
|
||||
@@ -328,7 +329,8 @@ class BaseSiteAdapter(Configurable):
|
||||
|
||||
def setCoverImage(self,storyurl,imgurl):
|
||||
if self.getConfig('include_images'):
|
||||
self.story.addImgUrl(storyurl,imgurl,self._fetchUrlRaw,cover=True)
|
||||
self.story.addImgUrl(storyurl,imgurl,self._fetchUrlRaw,cover=True,
|
||||
coverexclusion=self.getConfig('cover_exclusion_regexp'))
|
||||
|
||||
# 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...)
|
||||
|
||||
@@ -53,46 +53,52 @@ class Configuration(ConfigParser.SafeConfigParser):
|
||||
self.addConfigSection(sitewithout+":"+fileform)
|
||||
self.addConfigSection("overrides")
|
||||
|
||||
self.validEntries = [
|
||||
self.listTypeEntries = [
|
||||
'category',
|
||||
'genre',
|
||||
'language',
|
||||
'characters',
|
||||
'ships',
|
||||
'warnings',
|
||||
'extratags',
|
||||
'author',
|
||||
'authorId',
|
||||
'authorUrl',
|
||||
'lastupdate',
|
||||
]
|
||||
|
||||
self.validEntries = self.listTypeEntries + [
|
||||
'series',
|
||||
'seriesUrl',
|
||||
'language',
|
||||
'status',
|
||||
'datePublished',
|
||||
'dateUpdated',
|
||||
'dateCreated',
|
||||
'rating',
|
||||
'warnings',
|
||||
'numChapters',
|
||||
'numWords',
|
||||
'site',
|
||||
'storyId',
|
||||
'authorId',
|
||||
'extratags',
|
||||
'title',
|
||||
'storyUrl',
|
||||
'description',
|
||||
'author',
|
||||
'authorUrl',
|
||||
'formatname',
|
||||
'formatext',
|
||||
'siteabbrev',
|
||||
'version',
|
||||
# internal stuff.
|
||||
'langcode',
|
||||
'output_css',
|
||||
'authorHTML',
|
||||
'seriesHTML',
|
||||
'lastupdate'
|
||||
'langcode',
|
||||
'output_css',
|
||||
]
|
||||
|
||||
def addConfigSection(self,section):
|
||||
self.sectionslist.insert(0,section)
|
||||
|
||||
def isListType(self,key):
|
||||
return key in self.listTypeEntries or self.hasConfig("include_in_"+key)
|
||||
|
||||
def isValidMetaEntry(self, key):
|
||||
return key in self.getValidMetaList()
|
||||
|
||||
@@ -162,6 +168,9 @@ class Configurable(object):
|
||||
def __init__(self, configuration):
|
||||
self.configuration = configuration
|
||||
|
||||
def isListType(self,key):
|
||||
return self.configuration.isListType(key)
|
||||
|
||||
def isValidMetaEntry(self, key):
|
||||
return self.configuration.isValidMetaEntry(key)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2012, Jim Miller'
|
||||
__copyright__ = '2014, Jim Miller'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import logging
|
||||
|
||||
+212
-34
@@ -28,6 +28,9 @@ import exceptions
|
||||
from htmlcleanup import conditionalRemoveEntities, removeAllEntities
|
||||
from configurable import Configurable
|
||||
|
||||
SPACE_REPLACE=u'\s'
|
||||
SPLIT_META=u'\,'
|
||||
|
||||
# Create convert_image method depending on which graphics lib we can
|
||||
# load. Preferred: calibre, PIL, none
|
||||
|
||||
@@ -221,6 +224,65 @@ langs = {
|
||||
"Devanagari":"hi",
|
||||
}
|
||||
|
||||
class InExMatch:
|
||||
keys = []
|
||||
regex = None
|
||||
match = None
|
||||
negate = False
|
||||
|
||||
def __init__(self,line):
|
||||
if "=~" in line:
|
||||
(self.keys,self.match) = line.split("=~")
|
||||
self.match = self.match.replace(SPACE_REPLACE,' ')
|
||||
self.regex = re.compile(self.match)
|
||||
elif "!~" in line:
|
||||
(self.keys,self.match) = line.split("!~")
|
||||
self.match = self.match.replace(SPACE_REPLACE,' ')
|
||||
self.regex = re.compile(self.match)
|
||||
self.negate = True
|
||||
elif "==" in line:
|
||||
(self.keys,self.match) = line.split("==")
|
||||
self.match = self.match.replace(SPACE_REPLACE,' ')
|
||||
elif "!=" in line:
|
||||
(self.keys,self.match) = line.split("!=")
|
||||
self.match = self.match.replace(SPACE_REPLACE,' ')
|
||||
self.negate = True
|
||||
self.keys = map( lambda x: x.strip(), self.keys.split(",") )
|
||||
|
||||
# For conditional, only one key
|
||||
def is_key(self,key):
|
||||
return key == self.keys[0]
|
||||
|
||||
# For conditional, only one key
|
||||
def key(self):
|
||||
return self.keys[0]
|
||||
|
||||
def in_keys(self,key):
|
||||
return key in self.keys
|
||||
|
||||
def is_match(self,value):
|
||||
retval = False
|
||||
if self.regex:
|
||||
if self.regex.search(value):
|
||||
retval = True
|
||||
#print(">>>>>>>>>>>>>%s=~%s r: %s,%s=%s"%(self.match,value,self.negate,retval,self.negate != retval))
|
||||
else:
|
||||
retval = self.match == value
|
||||
#print(">>>>>>>>>>>>>%s==%s r: %s,%s=%s"%(self.match,value,self.negate,retval, self.negate != retval))
|
||||
|
||||
return self.negate != retval
|
||||
|
||||
def __str__(self):
|
||||
if self.negate:
|
||||
f='!'
|
||||
else:
|
||||
f='='
|
||||
if self.regex:
|
||||
s='~'
|
||||
else:
|
||||
s='='
|
||||
return u'InExMatch(%s %s%s %s)'%(self.keys,f,s,self.match)
|
||||
|
||||
class Story(Configurable):
|
||||
|
||||
def __init__(self, configuration):
|
||||
@@ -231,6 +293,7 @@ class Story(Configurable):
|
||||
except:
|
||||
self.metadata = {'version':'4.4'}
|
||||
self.replacements = []
|
||||
self.in_ex_cludes = {}
|
||||
self.chapters = [] # chapters will be tuples of (title,html)
|
||||
self.imgurls = []
|
||||
self.imgtuples = []
|
||||
@@ -241,8 +304,7 @@ class Story(Configurable):
|
||||
self.logfile=None # cheesy way to carry log file forward across update.
|
||||
|
||||
## Look for config parameter, split and add each to metadata field.
|
||||
for (config,metadata) in [("extratags","extratags"),
|
||||
("extracategories","category"),
|
||||
for (config,metadata) in [("extracategories","category"),
|
||||
("extragenres","genre"),
|
||||
("extracharacters","characters"),
|
||||
("extraships","ships"),
|
||||
@@ -252,23 +314,92 @@ class Story(Configurable):
|
||||
|
||||
self.setReplace(self.getConfig('replace_metadata'))
|
||||
|
||||
in_ex_clude_list = ['include_metadata_pre','exclude_metadata_pre',
|
||||
'include_metadata_post','exclude_metadata_post']
|
||||
for ie in in_ex_clude_list:
|
||||
ies = self.getConfig(ie)
|
||||
# print("%s %s"%(ie,ies))
|
||||
if ies:
|
||||
iel = []
|
||||
self.in_ex_cludes[ie] = self.set_in_ex_clude(ies)
|
||||
|
||||
def join_list(self, key, vallist):
|
||||
return self.getConfig("join_string_"+key,u", ").replace(SPACE_REPLACE,' ').join(map(unicode, vallist))
|
||||
|
||||
def setMetadata(self, key, value, condremoveentities=True):
|
||||
## still keeps < < and &
|
||||
if condremoveentities:
|
||||
self.metadata[key]=conditionalRemoveEntities(value)
|
||||
|
||||
# keep as list type, but set as only value.
|
||||
if self.isList(key):
|
||||
self.addToList(key,value,condremoveentities=condremoveentities,clear=True)
|
||||
else:
|
||||
self.metadata[key]=value
|
||||
## still keeps < < and &
|
||||
if condremoveentities:
|
||||
self.metadata[key]=conditionalRemoveEntities(value)
|
||||
else:
|
||||
self.metadata[key]=value
|
||||
|
||||
if key == "language":
|
||||
try:
|
||||
# getMetadata not just self.metadata[] to do replace_metadata.
|
||||
self.metadata['langcode'] = langs[self.getMetadata(key)]
|
||||
self.setMetadata('langcode',langs[self.getMetadata(key)])
|
||||
except:
|
||||
self.metadata['langcode'] = 'en'
|
||||
if key == 'dateUpdated':
|
||||
self.setMetadata('langcode','en')
|
||||
|
||||
if key == 'dateUpdated' and value:
|
||||
# Last Update tags for Bill.
|
||||
self.addToList('lastupdate',value.strftime("Last Update Year/Month: %Y/%m"))
|
||||
self.addToList('lastupdate',value.strftime("Last Update: %Y/%m/%d"))
|
||||
|
||||
|
||||
## metakey[,metakey]=~pattern
|
||||
## metakey[,metakey]==string
|
||||
## *for* part lines. Effect only when trailing conditional key=~regexp matches
|
||||
## metakey[,metakey]=~pattern[&&metakey=~regexp]
|
||||
## metakey[,metakey]==string[&&metakey=~regexp]
|
||||
## metakey[,metakey]=~pattern[&&metakey==string]
|
||||
## metakey[,metakey]==string[&&metakey==string]
|
||||
def set_in_ex_clude(self,setting):
|
||||
dest = []
|
||||
# print("set_in_ex_clude:"+setting)
|
||||
for line in setting.splitlines():
|
||||
if line:
|
||||
(match,condmatch)=(None,None)
|
||||
if "&&" in line:
|
||||
(line,conditional) = line.split("&&")
|
||||
condmatch = InExMatch(conditional)
|
||||
match = InExMatch(line)
|
||||
dest.append([match,condmatch])
|
||||
return dest
|
||||
|
||||
def do_in_ex_clude(self,which,value,key):
|
||||
if value and which in self.in_ex_cludes:
|
||||
include = 'include' in which
|
||||
keyfound = False
|
||||
found = False
|
||||
for (match,condmatch) in self.in_ex_cludes[which]:
|
||||
keyfndnow = False
|
||||
if match.in_keys(key):
|
||||
# key in keys and either no conditional, or conditional matched
|
||||
if condmatch == None or condmatch.is_key(key):
|
||||
keyfndnow = True
|
||||
else:
|
||||
condval = self.getMetadata(condmatch.key())
|
||||
keyfndnow = condmatch.is_match(condval)
|
||||
keyfound |= keyfndnow
|
||||
# print("match:%s %s\ncondmatch:%s %s\n\tkeyfound:%s\n\tfound:%s"%(
|
||||
# match,value,condmatch,condval,keyfound,found))
|
||||
if keyfndnow:
|
||||
found = isinstance(value,basestring) and match.is_match(value)
|
||||
if found:
|
||||
# print("match:%s %s\n\tkeyfndnow:%s\n\tfound:%s"%(
|
||||
# match,value,keyfndnow,found))
|
||||
if not include:
|
||||
value = None
|
||||
break
|
||||
if include and keyfound and not found:
|
||||
value = None
|
||||
return value
|
||||
|
||||
|
||||
## Two or three part lines. Two part effect everything.
|
||||
## Three part effect only those key(s) lists.
|
||||
@@ -278,6 +409,7 @@ class Story(Configurable):
|
||||
## metakey[,metakey]=>pattern=>replacement[&&metakey=>regexp]
|
||||
def setReplace(self,replace):
|
||||
for line in replace.splitlines():
|
||||
# print("replacement line:%s"%line)
|
||||
(metakeys,regexp,replacement,condkey,condregexp)=(None,None,None,None,None)
|
||||
if "&&" in line:
|
||||
(line,conditional) = line.split("&&")
|
||||
@@ -297,11 +429,20 @@ class Story(Configurable):
|
||||
# A way to explicitly include spaces in the
|
||||
# replacement string. The .ini parser eats any
|
||||
# trailing spaces.
|
||||
replacement=replacement.replace('\s',' ')
|
||||
replacement=replacement.replace(SPACE_REPLACE,' ')
|
||||
self.replacements.append([metakeys,regexp,replacement,condkey,condregexp])
|
||||
|
||||
def doReplacements(self,value,key):
|
||||
for (metakeys,regexp,replacement,condkey,condregexp) in self.replacements:
|
||||
def doReplacements(self,value,key,return_list=False,seen_list=[]):
|
||||
value = self.do_in_ex_clude('include_metadata_pre',value,key)
|
||||
value = self.do_in_ex_clude('exclude_metadata_pre',value,key)
|
||||
|
||||
retlist = [value]
|
||||
for replaceline in self.replacements:
|
||||
if replaceline in seen_list: # recursion on pattern, bail
|
||||
# print("bailing on %s"%replaceline)
|
||||
continue
|
||||
#print("replacement tuple:%s"%replaceline)
|
||||
(metakeys,regexp,replacement,condkey,condregexp) = replaceline
|
||||
if (metakeys == None or key in metakeys) \
|
||||
and isinstance(value,basestring) \
|
||||
and regexp.search(value):
|
||||
@@ -311,8 +452,34 @@ class Story(Configurable):
|
||||
doreplace = condval != None and condregexp.search(condval)
|
||||
|
||||
if doreplace:
|
||||
value = regexp.sub(replacement,value)
|
||||
return value
|
||||
# split into more than one list entry if
|
||||
# SPLIT_META present in replacement string. Split
|
||||
# first, then regex sub, then recurse call replace
|
||||
# on each. Break out of loop, each split element
|
||||
# handled individually by recursion call.
|
||||
if SPLIT_META in replacement:
|
||||
retlist = []
|
||||
for splitrepl in replacement.split(SPLIT_META):
|
||||
retlist.extend(self.doReplacements(regexp.sub(splitrepl,value),
|
||||
key,
|
||||
return_list=True,
|
||||
seen_list=seen_list+[replaceline]))
|
||||
break
|
||||
else:
|
||||
# print("replacement,value:%s,%s->%s"%(replacement,value,regexp.sub(replacement,value)))
|
||||
value = regexp.sub(replacement,value)
|
||||
retlist = [value]
|
||||
|
||||
for val in retlist:
|
||||
retlist = map(partial(self.do_in_ex_clude,'include_metadata_post',key=key),retlist)
|
||||
retlist = map(partial(self.do_in_ex_clude,'exclude_metadata_post',key=key),retlist)
|
||||
# value = self.do_in_ex_clude('include_metadata_post',value,key)
|
||||
# value = self.do_in_ex_clude('exclude_metadata_post',value,key)
|
||||
|
||||
if return_list:
|
||||
return retlist
|
||||
else:
|
||||
return self.join_list(key,retlist)
|
||||
|
||||
def getMetadataRaw(self,key):
|
||||
if self.isValidMetaEntry(key) and self.metadata.has_key(key):
|
||||
@@ -326,8 +493,9 @@ class Story(Configurable):
|
||||
return value
|
||||
|
||||
if self.isList(key):
|
||||
join_string = self.getConfig("join_string_"+key,u", ").replace('\s',' ')
|
||||
value = join_string.join(self.getList(key, removeallentities, doreplacements=True))
|
||||
# join_string = self.getConfig("join_string_"+key,u", ").replace(SPACE_REPLACE,' ')
|
||||
# value = join_string.join(self.getList(key, removeallentities, doreplacements=True))
|
||||
value = self.join_list(key,self.getList(key, removeallentities, doreplacements=True))
|
||||
if doreplacements:
|
||||
value = self.doReplacements(value,key+"_LIST")
|
||||
return value
|
||||
@@ -357,7 +525,8 @@ class Story(Configurable):
|
||||
doreplacements=True,
|
||||
keeplists=False):
|
||||
'''
|
||||
All single value *and* list value metadata as strings (unless keeplists=True, then keep lists).
|
||||
All single value *and* list value metadata as strings (unless
|
||||
keeplists=True, then keep lists).
|
||||
'''
|
||||
allmetadata = {}
|
||||
|
||||
@@ -377,16 +546,16 @@ class Story(Configurable):
|
||||
auth=removeAllEntities(auth)
|
||||
|
||||
htmllist.append(linkhtml%('author',aurl,auth))
|
||||
join_string = self.getConfig("join_string_authorHTML",u", ").replace('\s',' ')
|
||||
self.setMetadata('authorHTML',join_string.join(htmllist))
|
||||
# join_string = self.getConfig("join_string_authorHTML",u", ").replace(SPACE_REPLACE,' ')
|
||||
self.setMetadata('authorHTML',self.join_list("join_string_authorHTML",htmllist))
|
||||
else:
|
||||
self.setMetadata('authorHTML',linkhtml%('author',self.getMetadata('authorUrl', removeallentities, doreplacements),
|
||||
self.getMetadata('author', removeallentities, doreplacements)))
|
||||
|
||||
if self.getMetadataRaw('seriesUrl') != None:
|
||||
if self.getMetadataRaw('seriesUrl'):
|
||||
self.setMetadata('seriesHTML',linkhtml%('series',self.getMetadata('seriesUrl', removeallentities, doreplacements),
|
||||
self.getMetadata('series', removeallentities, doreplacements)))
|
||||
elif self.getMetadataRaw('series') != None:
|
||||
elif self.getMetadataRaw('series'):
|
||||
self.setMetadata('seriesHTML',self.getMetadataRaw('series'))
|
||||
|
||||
# logger.debug("make_linkhtml_entries:%s"%self.getConfig('make_linkhtml_entries'))
|
||||
@@ -409,8 +578,8 @@ class Story(Configurable):
|
||||
v=removeAllEntities(v)
|
||||
|
||||
htmllist.append(linkhtml%(k,url,v))
|
||||
join_string = self.getConfig("join_string_"+k+"HTML",u", ").replace('\s',' ')
|
||||
self.setMetadata(k+'HTML',join_string.join(htmllist))
|
||||
# join_string = self.getConfig("join_string_"+k+"HTML",u", ").replace(SPACE_REPLACE,' ')
|
||||
self.setMetadata(k+'HTML',self.join_list("join_string_"+k+"HTML",htmllist))
|
||||
|
||||
for k in self.getValidMetaList():
|
||||
if self.isList(k) and keeplists:
|
||||
@@ -425,11 +594,12 @@ class Story(Configurable):
|
||||
for v in l:
|
||||
self.addToList(listname,v.strip())
|
||||
|
||||
def addToList(self,listname,value):
|
||||
def addToList(self,listname,value,condremoveentities=True,clear=False):
|
||||
if value==None:
|
||||
return
|
||||
value = conditionalRemoveEntities(value)
|
||||
if not self.isList(listname) or not listname in self.metadata:
|
||||
if condremoveentities:
|
||||
value = conditionalRemoveEntities(value)
|
||||
if clear or not self.isList(listname) or not listname in self.metadata:
|
||||
# Calling addToList to a non-list meta will overwrite it.
|
||||
self.metadata[listname]=[]
|
||||
# prevent duplicates.
|
||||
@@ -441,7 +611,7 @@ class Story(Configurable):
|
||||
|
||||
def isList(self,listname):
|
||||
'Everything set with an include_in_* is considered a list.'
|
||||
return self.hasConfig("include_in_"+listname) or \
|
||||
return self.isListType(listname) or \
|
||||
( self.isValidMetaEntry(listname) and self.metadata.has_key(listname) \
|
||||
and isinstance(self.metadata[listname],list) )
|
||||
|
||||
@@ -470,16 +640,20 @@ class Story(Configurable):
|
||||
|
||||
if retlist:
|
||||
if doreplacements:
|
||||
retlist = filter( lambda x : x!=None and x!='' ,
|
||||
map(partial(self.doReplacements,key=listname),retlist) )
|
||||
newretlist = []
|
||||
for val in retlist:
|
||||
newretlist.extend(self.doReplacements(val,listname,return_list=True))
|
||||
retlist = newretlist
|
||||
|
||||
if removeallentities:
|
||||
retlist = filter( lambda x : x!=None and x!='' ,
|
||||
map(removeAllEntities,retlist) )
|
||||
retlist = map(removeAllEntities,retlist)
|
||||
|
||||
retlist = filter( lambda x : x!=None and x!='' ,retlist)
|
||||
|
||||
# reorder ships so b/a and c/b/a become a/b and a/b/c. Only on '/',
|
||||
# use replace_metadata to change separator first if needed.
|
||||
# ships=>[ ]*(/|&|&)[ ]*=>/
|
||||
if listname == 'ships' and self.getConfig('sort_ships'):
|
||||
if listname == 'ships' and self.getConfig('sort_ships') and retlist:
|
||||
retlist = [ '/'.join(sorted(x.split('/'))) for x in retlist ]
|
||||
|
||||
if retlist:
|
||||
@@ -598,6 +772,10 @@ class Story(Configurable):
|
||||
'','',''))
|
||||
#print("\n===========\nparsedUrl.path:%s\ntoppath:%s\nimgurl:%s\n\n"%(parsedUrl.path,toppath,imgurl))
|
||||
|
||||
# apply coverexclusion to explicit covers, too. Primarily for ffnet imageu.
|
||||
if cover and coverexclusion and re.search(coverexclusion,imgurl):
|
||||
return
|
||||
|
||||
prefix='ffdl'
|
||||
if imgurl not in self.imgurls:
|
||||
parsedUrl = urlparse.urlparse(imgurl)
|
||||
@@ -629,7 +807,7 @@ class Story(Configurable):
|
||||
return "failedtoload"
|
||||
|
||||
# explicit cover, make the first image.
|
||||
if cover and not self.getConfig('never_make_cover'):
|
||||
if cover:
|
||||
if len(self.imgtuples) > 0 and 'cover' in self.imgtuples[0]['newsrc']:
|
||||
# remove existing cover, if there is one.
|
||||
del self.imgurls[0]
|
||||
@@ -648,7 +826,7 @@ class Story(Configurable):
|
||||
if self.cover == None and \
|
||||
self.getConfig('make_firstimage_cover') and \
|
||||
not self.getConfig('never_make_cover') and \
|
||||
(not coverexclusion or not re.search(coverexclusion,imgurl)):
|
||||
not (coverexclusion and re.search(coverexclusion,imgurl)):
|
||||
newsrc = "images/cover.%s"%ext
|
||||
self.cover=newsrc
|
||||
self.imgtuples.append({'newsrc':newsrc,'mime':mime,'data':data})
|
||||
|
||||
+8
-11
@@ -46,13 +46,6 @@
|
||||
{{yourfile}}
|
||||
<!-- </div> -->
|
||||
|
||||
<h3>fanfiction.net / fimfiction.net</h3>
|
||||
<p>
|
||||
As of Jan 13, 2014, fanfiction.net & fimfiction.net
|
||||
are working again. I'd ask that users limit the number of
|
||||
stories they download from those sites, thanks.
|
||||
</p>
|
||||
|
||||
{% if authorized %}
|
||||
<form action="/fdown" method="post">
|
||||
<div id='urlbox'>
|
||||
@@ -60,14 +53,18 @@
|
||||
<p>Hi, {{ nickname }}! This is FanFictionDownLoader, which makes reading stories from various websites
|
||||
much easier. </p>
|
||||
</div>
|
||||
<!-- put announcements here, h3 is a good title size.
|
||||
<!-- put announcements here, h3 is a good title size. -->
|
||||
<h3>Changes:</h3>
|
||||
<p>
|
||||
Now supporting over 100 different sites! Thanks, cryzed, for pushing us over the top.
|
||||
</p>
|
||||
<p>
|
||||
<ul>
|
||||
<li>Fix for cover image.</li>
|
||||
<li>Fix for AO3 authors all coming out as Anonymous.</li>
|
||||
<li>Known issue: Password protected FimFiction.net stories aren't working. FimF changed API access.</li>
|
||||
<li>Known issue: Specific metadata 'eroticatags' for literotica.com doesn't work on all stories.</li>
|
||||
</ul>
|
||||
</p>
|
||||
-->
|
||||
<p>
|
||||
Questions? Check out our
|
||||
<a href="http://code.google.com/p/fanficdownloader/wiki/FanFictionDownloaderFAQs">FAQs</a>.
|
||||
@@ -76,7 +73,7 @@
|
||||
If you have any problems with this application, please
|
||||
report them in
|
||||
the <a href="http://groups.google.com/group/fanfic-downloader">FanFictionDownLoader Google Group</a>. The
|
||||
<a href="http://4-4-92.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
|
||||
<a href="http://2-0-01.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
|
||||
</p>
|
||||
<div id='error'>
|
||||
{{ error_message }}
|
||||
|
||||
+1
-3
@@ -1,9 +1,7 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# epubmerge.py 1.0
|
||||
|
||||
# Copyright 2011, Jim Miller
|
||||
# Copyright 2014, Jim Miller
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
||||
+244
-41
@@ -154,6 +154,11 @@ extratags: FanFiction
|
||||
## useful if pulling large numbers of stories or if the site is slow.
|
||||
#slow_down_sleep_time:0.5
|
||||
|
||||
## How long to wait for each HTTP connection to finish. Longer times
|
||||
## are better for sites that are slow to respond. Shorter times
|
||||
## prevent excessive wait when your network or the site is down.
|
||||
connect_timeout:60.0
|
||||
|
||||
## Use regular expressions to find and replace (or remove) metadata.
|
||||
## For example, you could change Sci-Fi=>SF, remove *-Centered tags,
|
||||
## etc. See http://docs.python.org/library/re.html (look for re.sub)
|
||||
@@ -181,7 +186,32 @@ extratags: FanFiction
|
||||
# .*-Centered=>
|
||||
# characters=>Sam W\.=>Sam Witwicky&&category=>Transformers
|
||||
# characters=>Sam W\.=>Sam Winchester&&category=>Supernatural
|
||||
|
||||
|
||||
## Include/Exclude metadata
|
||||
##
|
||||
## You can use the include/exclude metadata features to either limit
|
||||
## the values of particular metadata lists to specific values or to
|
||||
## exclude specific values. Further, you can conditionally apply each
|
||||
## line depending on other metadata, use exact strings or regular
|
||||
## expressions(regex) to match values, and negate matches.
|
||||
##
|
||||
## The settings are:
|
||||
## include_metadata_pre
|
||||
## exclude_metadata_pre
|
||||
## include_metadata_post
|
||||
## exclude_metadata_post
|
||||
##
|
||||
## The form of each line is:
|
||||
## metakey[,metakey]==exactvalue
|
||||
## metakey[,metakey]=~regex
|
||||
## metakey[,metakey]==exactvalue&&conditionalkey==exactcondvalue
|
||||
## metakey[,metakey]=~regex&&conditionalkey==exactcondvalue
|
||||
## metakey[,metakey]==exactvalue&&conditionalkey=~condregex
|
||||
##
|
||||
## This is fairly complicated, so it's documented on its own wiki
|
||||
## page:
|
||||
## https://code.google.com/p/fanficdownloader/wiki/InExcludeMetadataFeature
|
||||
|
||||
## Some readers don't show horizontal rule (<hr />) tags correctly.
|
||||
## This replaces them all with a centered '* * *'. (Note centering
|
||||
## doesn't work on some devices either.)
|
||||
@@ -202,6 +232,9 @@ extratags: FanFiction
|
||||
## Make sure to keep at least one space at the start of each line and
|
||||
## to escape % to %%, if used.
|
||||
## template => regexp to match => GC Setting to use.
|
||||
## To use this, make sure you go to the Generate Cover tab in FFDL
|
||||
## config and check 'Allow generate_cover_settings from personal.ini
|
||||
## to override'
|
||||
#generate_cover_settings:
|
||||
# ${category} => Buffy:? [tT]he Vampire Slayer => BuffyCover
|
||||
# ${category} => Star Trek => StarTrekCover
|
||||
@@ -259,6 +292,10 @@ chapter_title_add_pattern:${index}. ${title}
|
||||
## anthologies.
|
||||
anthology_title_pattern:${title} Anthology
|
||||
|
||||
## Add tag(s) for anthology (series) books. Set to empty to not add
|
||||
## any anthology tags.
|
||||
anthology_tags:Anthology
|
||||
|
||||
## Reorder ships so b/a and c/b/a become a/b and a/b/c. Only separates
|
||||
## on '/', so use replace_metadata to change separator first if
|
||||
## needed. Something like: ships=>[ ]*(/|&|&)[ ]*=>/ You can use
|
||||
@@ -537,6 +574,12 @@ include_in_freefromtags:freeformtags
|
||||
## adds to include_subject_tags instead of replacing it.
|
||||
#extra_subject_tags:fandoms,freeformtags,ao3categories
|
||||
|
||||
## AO3 chapters can include several different types of notes. We've
|
||||
## traditional included them all in the chapter text, but this allows
|
||||
## you to customize which you include. Copy this parameter to your
|
||||
## personal.ini and list the ones you don't want.
|
||||
#exclude_notes:authorheadnotes,chaptersummary,chapterheadnotes,chapterfootnotes,authorfootnotes
|
||||
|
||||
[ashwinder.sycophanthex.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -559,6 +602,41 @@ extracategories:The Sentinel
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[bdsm-geschichten.net]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## This site offers no index page so we can either guess the chapter URLs
|
||||
## by dec/incrementing numbers ('guess') or walk all the chapters in the metadata
|
||||
## parsing state ('parse'). Since guessing can lead to errors for non-standard
|
||||
## story URLs, the default is to parse
|
||||
#find_chapters:guess
|
||||
|
||||
[bloodshedverse.com]
|
||||
## website encoding(s) In theory, each website reports the character
|
||||
## encoding they use for each page. In practice, some sites report it
|
||||
## incorrectly. Each adapter has a default list, usually "utf8,
|
||||
## Windows-1252" or "Windows-1252, utf8", but this will let you
|
||||
## explicitly set the encoding and order if you need to. The special
|
||||
## value 'auto' will call chardet and use the encoding it reports if
|
||||
## it has +90% confidence. 'auto' is not reliable.
|
||||
website_encodings:Windows-1252,ISO-8859-1,auto
|
||||
|
||||
## Extra metadata that this adapter knows about. See [dramione.org]
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:warnings,reviews
|
||||
reviews_label:Reviews
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracharacters:Spike,Buffy
|
||||
extracategories:Buffy the Vampire Slayer
|
||||
|
||||
## Strips links found in the story text
|
||||
## Specific to bloodshedverse.com
|
||||
strip_text_links:true
|
||||
|
||||
[bloodties-fans.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -718,6 +796,38 @@ extracategories:Harry Potter
|
||||
## cover image. This lets you exclude them.
|
||||
cover_exclusion_regexp:/images/.*?ribbon.gif
|
||||
|
||||
[fanfiction.csodaidok.hu]
|
||||
## website encoding(s) In theory, each website reports the character
|
||||
## encoding they use for each page. In practice, some sites report it
|
||||
## incorrectly. Each adapter has a default list, usually "utf8,
|
||||
## Windows-1252" or "Windows-1252, utf8", but this will let you
|
||||
## explicitly set the encoding and order if you need to. The special
|
||||
## value 'auto' will call chardet and use the encoding it reports if
|
||||
## it has +90% confidence. 'auto' is not reliable.
|
||||
website_encodings:ISO-8859-2,auto
|
||||
|
||||
## Extra metadata that this adapter knows about. See [dramione.org]
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:reviews,challenge
|
||||
reviews_label:Reviews
|
||||
challenge_label:Challenge
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
|
||||
[fanfic.hu]
|
||||
## website encoding(s) In theory, each website reports the character
|
||||
## encoding they use for each page. In practice, some sites report it
|
||||
## incorrectly. Each adapter has a default list, usually "utf8,
|
||||
## Windows-1252" or "Windows-1252, utf8", but this will let you
|
||||
## explicitly set the encoding and order if you need to. The special
|
||||
## value 'auto' will call chardet and use the encoding it reports if
|
||||
## it has +90% confidence. 'auto' is not reliable.
|
||||
website_encodings:ISO-8859-1,auto
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
|
||||
[fanfiction.mugglenet.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
@@ -768,6 +878,53 @@ extraships:Harry Potter/Hermione Granger
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
[ficwad.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
[fictionmania.tv]
|
||||
## website encoding(s) In theory, each website reports the character
|
||||
## encoding they use for each page. In practice, some sites report it
|
||||
## incorrectly. Each adapter has a default list, usually "utf8,
|
||||
## Windows-1252" or "Windows-1252, utf8", but this will let you
|
||||
## explicitly set the encoding and order if you need to. The special
|
||||
## value 'auto' will call chardet and use the encoding it reports if
|
||||
## it has +90% confidence. 'auto' is not reliable.
|
||||
website_encodings:ISO-8859-1,auto
|
||||
|
||||
## items to include in the log page Empty metadata entries, or those
|
||||
## that haven't changed since the last update, will *not* appear, even
|
||||
## if in the list. You can include extra text or HTML that will be
|
||||
## included as-is in each log entry. Eg: logpage_entries: ...,<br />,
|
||||
## summary,<br />,...
|
||||
## Don't include numChapters since all stories are a single "chapter", there's
|
||||
## no way to reliably find the next chapter
|
||||
logpage_entries: dateCreated,datePublished,dateUpdated,numChapters,numWords,status,series,title,author,description,category,genre,rating,warnings
|
||||
|
||||
## items to include in the title page
|
||||
## Empty metadata entries will *not* appear, even if in the list.
|
||||
## You can include extra text or HTML that will be included as-is in
|
||||
## the title page. Eg: titlepage_entries: ...,<br />,summary,<br />,...
|
||||
## All current formats already include title and author.
|
||||
## Don't include numChapters since all stories are a single "chapter", there's
|
||||
## no way to reliably find the next chapter
|
||||
titlepage_entries: seriesHTML,category,genre,language,characters,ships,status,datePublished,dateUpdated,dateCreated,rating,warnings,numWords,site,description
|
||||
|
||||
## Extra metadata that this adapter knows about. See [dramione.org]
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:fileName,fileSize,oldName,newName,keyWords,mainCharactersAge,readings
|
||||
|
||||
## Turns all space characters into " " HTML entities to forcefully preserve
|
||||
## formatting with spaces. Enabling this will blow up the filesize quite a bit
|
||||
## and is probably not a good idea, unless you absolutely need the story
|
||||
## formatting.
|
||||
## Specific to fictionmania.tv
|
||||
non_breaking_spaces:false
|
||||
|
||||
[fictionpad.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -793,36 +950,6 @@ dislikes_label:Dislikes
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
[storiesonline.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
## Clear FanFiction from defaults, site is original fiction.
|
||||
extratags:
|
||||
|
||||
extra_valid_entries:size,universe,universeUrl,universeHTML,codes,notice
|
||||
#extra_titlepage_entries:size,universeHTML,codes,notice
|
||||
|
||||
size_label:Size
|
||||
universe_label:Universe
|
||||
universeUrl_label:Universe URL
|
||||
universeHTML_label:Universe
|
||||
codes_label:Codes
|
||||
notice_label:Notice
|
||||
|
||||
## Assume entryUrl, apply to "<a class='%slink' href='%s'>%s</a>" to
|
||||
## make entryHTML.
|
||||
make_linkhtml_entries:universe
|
||||
|
||||
## storiesonline.net stories can be in a series or a universe, but not
|
||||
## both. By default, universe will be populated in 'series' with
|
||||
## index=0
|
||||
universe_as_series: true
|
||||
|
||||
[grangerenchanted.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -875,6 +1002,11 @@ extracategories:Star Trek
|
||||
extracharacters:Kirk,Spock
|
||||
extraships:Kirk/Spock
|
||||
|
||||
[literotica.com]
|
||||
extra_valid_entries:eroticatags
|
||||
eroticatags_label:Erotica Tags
|
||||
extra_titlepage_entries: eroticatags
|
||||
|
||||
[lumos.sycophanthex.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
@@ -924,6 +1056,17 @@ extracategories:NCIS
|
||||
extracategories:Buffy: The Vampire Slayer
|
||||
extracharacters:Willow
|
||||
|
||||
[nocturnal-light.net]
|
||||
## Extra metadata that this adapter knows about. See [dramione.org]
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:readings,reviews
|
||||
readings_label:Readings
|
||||
reviews_label:Reviews
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracharacters:Spike,Buffy
|
||||
extracategories:Buffy the Vampire Slayer
|
||||
|
||||
[occlumency.sycophanthex.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -1014,10 +1157,50 @@ extracategories:Harry Potter
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[spikeluver.com]
|
||||
## Extra metadata that this adapter knows about. See [dramione.org]
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:warnings,reviews
|
||||
reviews_label:Reviews
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracharacters:Spike,Buffy
|
||||
extracategories:Buffy the Vampire Slayer
|
||||
|
||||
[stargate-atlantis.org]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Stargate: Atlantis
|
||||
|
||||
[storiesonline.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
## Clear FanFiction from defaults, site is original fiction.
|
||||
extratags:
|
||||
|
||||
extra_valid_entries:size,universe,universeUrl,universeHTML,codes,notice
|
||||
#extra_titlepage_entries:size,universeHTML,codes,notice
|
||||
|
||||
size_label:Size
|
||||
universe_label:Universe
|
||||
universeUrl_label:Universe URL
|
||||
universeHTML_label:Universe
|
||||
codes_label:Codes
|
||||
notice_label:Notice
|
||||
|
||||
## Assume entryUrl, apply to "<a class='%slink' href='%s'>%s</a>" to
|
||||
## make entryHTML.
|
||||
make_linkhtml_entries:universe
|
||||
|
||||
## storiesonline.net stories can be in a series or a universe, but not
|
||||
## both. By default, universe will be populated in 'series' with
|
||||
## index=0
|
||||
universe_as_series: true
|
||||
|
||||
[svufiction.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -1053,6 +1236,12 @@ extraships:Harry Potter/Draco Malfoy
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Criminal Minds
|
||||
|
||||
[themaplebookshelf.com]
|
||||
## Some sites also require the user to confirm they are adult for
|
||||
## adult content. In commandline version, this should go in your
|
||||
## personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[themasque.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -1091,6 +1280,10 @@ extracategories:Harry Potter
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Stargate: SG-1
|
||||
|
||||
[tolkienfanfiction.com]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Lord of the Rings
|
||||
|
||||
[trekiverse.org]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -1112,6 +1305,13 @@ awards_label:Awards
|
||||
|
||||
cover_exclusion_regexp:art/.*Awards.jpg
|
||||
|
||||
[voracity2.e-fic.com]
|
||||
## Extra metadata that this adapter knows about. See [dramione.org]
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:reviews,readings
|
||||
reviews_label:Reviews
|
||||
readings_label:Readings
|
||||
|
||||
[www.adastrafanfic.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
@@ -1199,8 +1399,11 @@ type_label:Type of Couple
|
||||
[www.fanfiction.net]
|
||||
user_agent:
|
||||
## fanfiction.net's 'cover' images are really just tiny thumbnails.
|
||||
## Change this to false to use them anyway.
|
||||
never_make_cover: true
|
||||
## Set this to true to never use them.
|
||||
#never_make_cover: false
|
||||
|
||||
## fanfiction.net shows the user's
|
||||
cover_exclusion_regexp:/imageu/
|
||||
|
||||
## fanfiction.net is blocking people more aggressively. If you
|
||||
## download fewer stories less often you can likely get by with
|
||||
@@ -1270,14 +1473,6 @@ extratags:
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:reviews,favs,follows
|
||||
|
||||
[ficwad.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
[www.fimfiction.net]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
@@ -1296,6 +1491,11 @@ extra_valid_entries:reviews,favs,follows
|
||||
## when updating to enforce accurate chapters.
|
||||
#do_update_hook:false
|
||||
|
||||
## fimfiction.net is reported to misinterprete some BBCode with
|
||||
## blockquotes incorrectly. This fixes those instances and defaults
|
||||
## to on, but can be switched off if it is found to cause problems.
|
||||
fix_fimf_blockquotes:true
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:My Little Pony: Friendship is Magic
|
||||
|
||||
@@ -1633,6 +1833,9 @@ extracharacters:Wolverine,Rogue
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Stargate: Atlantis
|
||||
|
||||
extra_valid_entries:reviews
|
||||
reviews_label:Reviews
|
||||
|
||||
[overrides]
|
||||
## It may sometimes be useful to override all of the specific format,
|
||||
## site and site:format sections in your private configuration. For
|
||||
|
||||
@@ -51,12 +51,6 @@
|
||||
by {{ fic.author }} ({{ fic.format }})
|
||||
{% endif %}
|
||||
{% if fic.failure %}
|
||||
<h3>fanfiction.net / fimfiction.net</h3>
|
||||
<p>
|
||||
As of Jan 13, 2014, fanfiction.net & fimfiction.net
|
||||
are working again. I'd ask that users limit the number of
|
||||
stories they download from those sites, thanks.
|
||||
</p>
|
||||
<span id='error'>{{ fic.failure }}</span>
|
||||
{% endif %}
|
||||
{% if not fic.completed and not fic.failure %}
|
||||
|
||||
Reference in New Issue
Block a user