mirror of
https://github.com/wassname/FanFicFare.git
synced 2026-09-13 12:11:20 +08:00
Compare commits
211
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
4718955ede | ||
|
|
d60ad9892d | ||
|
|
35b5eb3dee | ||
|
|
f9d8e2f3db | ||
|
|
f291625ae3 | ||
|
|
6ef28cee6a | ||
|
|
fc3431098d | ||
|
|
8980eaf21b | ||
|
|
c8a89fa8b2 | ||
|
|
683ad6182d | ||
|
|
3267064576 | ||
|
|
ab128c417c | ||
|
|
dbc9b5e250 | ||
|
|
fffdd89254 | ||
|
|
cbb17aeba1 | ||
|
|
7433d2494f | ||
|
|
d0fa72a002 | ||
|
|
7412acd007 | ||
|
|
196eff7f60 | ||
|
|
8f06a374cb | ||
|
|
4f5738108f | ||
|
|
6f22a3c701 | ||
|
|
589076eed9 | ||
|
|
ee07ac8919 | ||
|
|
dc5def5cc8 | ||
|
|
23ad8700bb | ||
|
|
250121e137 | ||
|
|
56a639508e | ||
|
|
d398b62c1a | ||
|
|
bff3cfbd25 | ||
|
|
34558b1653 | ||
|
|
0ce37974e2 | ||
|
|
929cf36e79 | ||
|
|
b86c15d5a6 | ||
|
|
df622dfbf0 | ||
|
|
2c16b5e148 | ||
|
|
86ce293f78 | ||
|
|
63e8fc8bb2 | ||
|
|
38ab66907b | ||
|
|
1dedde2f9d | ||
|
|
fc5dd20853 | ||
|
|
4e9fe9f86f | ||
|
|
7325be98f5 | ||
|
|
e2545a0da8 | ||
|
|
5cd3dba4a6 | ||
|
|
bc2420d143 | ||
|
|
099e8a62ca | ||
|
|
7f9e22e1b9 | ||
|
|
a46532bc72 | ||
|
|
f7429140f8 | ||
|
|
7872b25034 | ||
|
|
3d468a4dc5 | ||
|
|
413525a881 | ||
|
|
c066a809c7 | ||
|
|
443b2b5ef8 | ||
|
|
3e7f05ba44 | ||
|
|
25e65da239 | ||
|
|
c5f211f49f | ||
|
|
20fff58813 | ||
|
|
8fe250747c | ||
|
|
4d41c28f3c | ||
|
|
f8d1b21007 | ||
|
|
72861e6cd5 | ||
|
|
bd9dba5813 | ||
|
|
1478df572e | ||
|
|
68466af944 | ||
|
|
7c9c84256c | ||
|
|
fcbfe96c64 | ||
|
|
e47748dbbc | ||
|
|
91a6c668d1 |
@@ -1,6 +1,6 @@
|
||||
# ffd-retief-hrd fanfictiondownloader
|
||||
application: fanfictiondownloader
|
||||
version: 4-4-86
|
||||
version: 2-0-00
|
||||
runtime: python27
|
||||
api_version: 1
|
||||
threadsafe: true
|
||||
|
||||
@@ -42,8 +42,8 @@ class FanFictionDownLoaderBase(InterfaceActionBase):
|
||||
description = _('UI plugin to download FanFiction stories from various sites.')
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
author = 'Jim Miller'
|
||||
version = (1, 8, 02)
|
||||
minimum_calibre_version = (0, 8, 57)
|
||||
version = (2, 0, 0)
|
||||
minimum_calibre_version = (1, 13, 0)
|
||||
|
||||
#: This field defines the GUI plugin class that contains all the code
|
||||
#: that actually does something. Its format is module_path:class_name
|
||||
|
||||
@@ -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
|
||||
|
||||
+61
-22
@@ -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
|
||||
@@ -55,12 +76,12 @@ from calibre_plugins.fanfictiondownloader_plugin.dialogs \
|
||||
EditTextDialog, RejectUrlEntry)
|
||||
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.adapters \
|
||||
import (getConfigSections, getNormalStoryURL)
|
||||
import getConfigSections
|
||||
|
||||
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):
|
||||
@@ -209,6 +230,7 @@ class ConfigWidget(QWidget):
|
||||
prefs['suppresstitlesort'] = self.basic_tab.suppresstitlesort.isChecked()
|
||||
prefs['mark'] = self.basic_tab.mark.isChecked()
|
||||
prefs['showmarked'] = self.basic_tab.showmarked.isChecked()
|
||||
prefs['autoconvert'] = self.basic_tab.autoconvert.isChecked()
|
||||
prefs['urlsfromclip'] = self.basic_tab.urlsfromclip.isChecked()
|
||||
prefs['updatedefault'] = self.basic_tab.updatedefault.isChecked()
|
||||
prefs['deleteotherforms'] = self.basic_tab.deleteotherforms.isChecked()
|
||||
@@ -219,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
|
||||
@@ -242,12 +265,13 @@ 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]))
|
||||
prefs['gc_site_settings'] = gc_site_settings
|
||||
prefs['allow_gc_from_ini'] = self.generatecover_tab.allow_gc_from_ini.isChecked()
|
||||
prefs['gc_polish_cover'] = self.generatecover_tab.gc_polish_cover.isChecked()
|
||||
|
||||
# Count Pages tab
|
||||
countpagesstats = []
|
||||
@@ -273,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]))
|
||||
@@ -422,6 +446,11 @@ class BasicTab(QWidget):
|
||||
self.showmarked.setChecked(prefs['showmarked'])
|
||||
self.l.addWidget(self.showmarked)
|
||||
|
||||
self.autoconvert = QCheckBox(_("Automatically Convert new/update books?"),self)
|
||||
self.autoconvert.setToolTip(_("Automatically call calibre's Convert for new/update books.\nConverts to the current output format as chosen in calibre's\nPreferences->Behavior settings."))
|
||||
self.autoconvert.setChecked(prefs['autoconvert'])
|
||||
self.l.addWidget(self.autoconvert)
|
||||
|
||||
gui_gb = groupbox = QGroupBox(_("GUI Options"))
|
||||
self.l = QVBoxLayout()
|
||||
groupbox.setLayout(self.l)
|
||||
@@ -475,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()
|
||||
@@ -642,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'])
|
||||
@@ -658,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'])
|
||||
@@ -720,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)
|
||||
@@ -747,6 +781,11 @@ class GenerateCoverTab(QWidget):
|
||||
self.allow_gc_from_ini.setChecked(prefs['allow_gc_from_ini'])
|
||||
self.l.addWidget(self.allow_gc_from_ini)
|
||||
|
||||
self.gc_polish_cover = QCheckBox(_("Use calibre's Polish feature to inject/update the cover"),self)
|
||||
self.gc_polish_cover.setToolTip(_("Calibre's Polish feature will be used to inject or update the generated cover into the ebook, EPUB only."))
|
||||
self.gc_polish_cover.setChecked(prefs['gc_polish_cover'])
|
||||
self.l.addWidget(self.gc_polish_cover)
|
||||
|
||||
class CountPagesTab(QWidget):
|
||||
|
||||
def __init__(self, parent_dialog, plugin_action):
|
||||
@@ -954,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:
|
||||
@@ -995,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)
|
||||
|
||||
+126
-65
@@ -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,
|
||||
@@ -660,13 +663,13 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
#print("update_dialog()")
|
||||
|
||||
db = self.gui.current_db
|
||||
book_list = map( self.make_book_id_only, id_list )
|
||||
books = map( self.make_book_id_only, id_list )
|
||||
|
||||
for j, book in enumerate(book_list):
|
||||
for j, book in enumerate(books):
|
||||
book['listorder'] = j
|
||||
|
||||
|
||||
LoopProgressDialog(self.gui,
|
||||
book_list,
|
||||
books,
|
||||
partial(self.populate_book_from_calibre_id, db=self.gui.current_db),
|
||||
self.update_dialog_finish,
|
||||
init_label=_("Collecting stories for update..."),
|
||||
@@ -718,6 +721,9 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
url_list = split_text_to_urls(books)
|
||||
books = self.convert_urls_to_books(url_list)
|
||||
|
||||
## for tweak_fg_sleep
|
||||
options['ffnetcount']=len(filter(lambda x : x['site']=='www.fanfiction.net', books))
|
||||
|
||||
options['version'] = self.version
|
||||
logger.debug(self.version)
|
||||
|
||||
@@ -760,7 +766,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>
|
||||
@@ -815,6 +821,18 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
|
||||
options['personal.ini'] = get_ffdl_personalini()
|
||||
adapter = get_ffdl_adapter(url,fileform)
|
||||
# reduce foreground sleep time for ffnet when few books.
|
||||
if 'ffnetcount' in options and \
|
||||
adapter.getConfig('tweak_fg_sleep') and \
|
||||
adapter.getSiteDomain() == 'www.fanfiction.net':
|
||||
minslp = float(adapter.getConfig('min_fg_sleep'))
|
||||
maxslp = float(adapter.getConfig('max_fg_sleep'))
|
||||
dwnlds = float(adapter.getConfig('max_fg_sleep_at_downloads'))
|
||||
m = (maxslp-minslp) / (dwnlds-1)
|
||||
b = minslp - m
|
||||
slp = min(maxslp,m*float(options['ffnetcount'])+b)
|
||||
#print("m:%s b:%s = %s"%(m,b,slp))
|
||||
adapter.set_sleep(slp)
|
||||
|
||||
## three tries, that's enough if both user/pass & is_adult needed,
|
||||
## or a couple tries of one or the other
|
||||
@@ -841,7 +859,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 = 'identifiers:"~ur(i|l):~^%s$"'%re.sub(r'https?\\:','https?(\:|\|)',re.escape(story.getMetadata('seriesUrl')))
|
||||
identicalbooks = db.search_getting_ids(searchstr, None)
|
||||
# print("searchstr:%s"%searchstr)
|
||||
# print("identicalbooks:%s"%identicalbooks)
|
||||
@@ -873,7 +891,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")
|
||||
@@ -925,13 +943,6 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
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)
|
||||
@@ -961,7 +972,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>
|
||||
@@ -1138,6 +1150,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
func, args=args,
|
||||
description=desc)
|
||||
|
||||
self.gui.jobs_pointer.start()
|
||||
self.gui.status_bar.show_message(_('Starting %d FanFictionDownLoads')%len(book_list),3000)
|
||||
|
||||
def update_books_loop(self,book,db=None,
|
||||
@@ -1232,6 +1245,10 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
cp_plugin = self.gui.iactions['Count Pages']
|
||||
cp_plugin.count_statistics(all_ids,prefs['countpagesstats'])
|
||||
|
||||
if prefs['autoconvert'] and options['collision'] != CALIBREONLY:
|
||||
self.gui.status_bar.show_message(_('Starting auto conversion of %d books.')%(len(all_ids)), 3000)
|
||||
self.gui.iactions['Convert Books'].auto_convert_auto_add(all_ids)
|
||||
|
||||
def download_list_completed(self, job, options={},merge=False):
|
||||
if job.failed:
|
||||
self.gui.job_exception(job, dialog_title='Failed to Download Stories')
|
||||
@@ -1335,7 +1352,6 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
bad_list = sorted(bad_list,key=lambda x : x['listorder'])
|
||||
|
||||
self.gui.status_bar.show_message(_('Merging %s books.')%total_good)
|
||||
|
||||
|
||||
existingbook = None
|
||||
if 'mergebook' in options:
|
||||
@@ -1393,7 +1409,10 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
if prefs['mark'] or (prefs['errorcol'] != '' and prefs['errorcol'] in custom_columns):
|
||||
self.previous = self.gui.library_view.currentIndex() # used by update_books_finish.
|
||||
self.gui.status_bar.show_message(_('Adding/Updating %s BAD books.')%len(book_list))
|
||||
label = custom_columns[prefs['errorcol']]['label']
|
||||
if (prefs['errorcol'] != '' and prefs['errorcol'] in custom_columns):
|
||||
label = custom_columns[prefs['errorcol']]['label']
|
||||
else:
|
||||
label = None
|
||||
LoopProgressDialog(self.gui,
|
||||
book_list,
|
||||
partial(self.update_error_column_loop, db=self.gui.current_db, label=label),
|
||||
@@ -1402,12 +1421,10 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
win_title=_("Update calibre for BAD FanFiction stories"),
|
||||
status_prefix=_("Updated"))
|
||||
|
||||
def update_error_column_loop(self,book,db=None,label='errorcol'):
|
||||
if book['calibre_id']:
|
||||
custom_columns = self.gui.library_view.model().custom_columns
|
||||
if (prefs['errorcol'] != '' and prefs['errorcol'] in custom_columns):
|
||||
logger.debug("add/update bad %s %s %s"%(book['title'],book['url'],book['comment']))
|
||||
db.set_custom(book['calibre_id'], book['comment'], label=label, commit=True)
|
||||
def update_error_column_loop(self,book,db=None,label=None):
|
||||
if book['calibre_id'] and label:
|
||||
logger.debug("add/update bad %s %s %s"%(book['title'],book['url'],book['comment']))
|
||||
db.set_custom(book['calibre_id'], book['comment'], label=label, commit=True)
|
||||
|
||||
def add_book_or_update_format(self,book,options,prefs,mi=None):
|
||||
db = self.gui.current_db
|
||||
@@ -1436,8 +1453,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
|
||||
|
||||
@@ -1545,8 +1572,15 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
(custcol,flag) = map( lambda x: x.strip(), custcol.split(",") )
|
||||
|
||||
if meta not in book['all_metadata']:
|
||||
logger.debug("No value for %s, skipping custom column(%s) update."%(meta,custcol))
|
||||
continue
|
||||
# if double quoted, use as a literal value.
|
||||
if meta[0] == '"' and meta[-1] == '"':
|
||||
val = meta[1:-1]
|
||||
logger.debug("No metadata value for %s, setting custom column(%s) literally to %s."%(meta,custcol,val))
|
||||
else:
|
||||
logger.debug("No value for %s, skipping custom column(%s) update."%(meta,custcol))
|
||||
continue
|
||||
else:
|
||||
val = book['all_metadata'][meta]
|
||||
|
||||
if custcol not in custom_columns:
|
||||
continue
|
||||
@@ -1558,12 +1592,19 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
if coldef['datatype'] in ('int','float'): # for favs, etc--site specific metadata.
|
||||
if 'anthology_meta_list' in book and meta in book['anthology_meta_list']:
|
||||
# re-split list, strip commas, convert to floats, sum up.
|
||||
val = sum([ float(x.replace(",","")) for x in book['all_metadata'][meta].split(", ") ])
|
||||
val = sum([ float(x.replace(",","")) for x in val.split(", ") ])
|
||||
else:
|
||||
val = unicode(book['all_metadata'][meta]).replace(",","")
|
||||
val = unicode(val).replace(",","")
|
||||
else:
|
||||
val = book['all_metadata'][meta]
|
||||
val = val
|
||||
if val != '':
|
||||
if coldef['datatype'] == 'bool':
|
||||
if val.lower() in ('t','true','1','yes','y'):
|
||||
val = True
|
||||
elif val.lower() in ('f','false','0','no','n'):
|
||||
val = False
|
||||
else:
|
||||
val = None # for tri-state 'booleans'. Yes/No/Null
|
||||
db.set_custom(book_id, val, label=label, commit=False)
|
||||
|
||||
if flag == 'a':
|
||||
@@ -1577,8 +1618,8 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
except:
|
||||
pass
|
||||
|
||||
if book['all_metadata'][meta]:
|
||||
vallist = [book['all_metadata'][meta]]
|
||||
if val:
|
||||
vallist.append(val)
|
||||
|
||||
db.set_custom(book_id, ", ".join(vallist), label=label, commit=False)
|
||||
|
||||
@@ -1587,30 +1628,23 @@ 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']):
|
||||
|
||||
logger.debug("Do Generate Cover added:%s gcnewonly:%s"%(book['added'],prefs['gcnewonly']))
|
||||
#logger.debug("Do Generate Cover added:%s gcnewonly:%s"%(book['added'],prefs['gcnewonly']))
|
||||
|
||||
# force a refresh if generating cover so complex composite
|
||||
# custom columns are current and correct
|
||||
@@ -1653,6 +1687,28 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
realmi = db.get_metadata(book_id, index_is_id=True)
|
||||
gc_plugin.generate_cover_for_book(realmi,saved_setting_name=setting_name)
|
||||
|
||||
if prefs['gc_polish_cover'] and \
|
||||
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
|
||||
from collections import namedtuple
|
||||
|
||||
# Couldn't find a better way to get the cover path.
|
||||
cover_path = os.path.join(db.library_path, db.path(book_id, index_is_id=True), 'cover.jpg')
|
||||
data = {'cover':cover_path}
|
||||
#print("cover_path:%s"%cover_path)
|
||||
opts = ALL_OPTS.copy()
|
||||
opts.update(data)
|
||||
O = namedtuple('Options', ' '.join(ALL_OPTS.iterkeys()))
|
||||
opts = O(**opts)
|
||||
|
||||
log = Log(level=Log.DEBUG)
|
||||
outfile = db.format_abspath(book_id, formmapping[options['fileform']], index_is_id=True)
|
||||
#print("polish cover outfile:%s"%outfile)
|
||||
polish({outfile:outfile}, opts, log, logger.info)
|
||||
|
||||
|
||||
def get_clean_reading_lists(self,lists):
|
||||
if lists == None or lists.strip() == "" :
|
||||
return []
|
||||
@@ -1741,6 +1797,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
book['end'] = None
|
||||
book['comment'] = '' # note this is a comment on the d/l or update.
|
||||
book['url'] = ''
|
||||
book['site'] = ''
|
||||
book['added'] = False
|
||||
return book
|
||||
|
||||
@@ -1817,13 +1874,15 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
book['status'] = _('Not Found')
|
||||
else:
|
||||
# get normalized url or None.
|
||||
book['url'] = self.is_good_downloader_url(url)
|
||||
if book['url'] == None:
|
||||
urlsitetuple = adapters.getNormalStoryURLSite(url)
|
||||
if urlsitetuple == None:
|
||||
book['url'] = url
|
||||
book['comment'] = _("URL is not a valid story URL.")
|
||||
book['good'] = False
|
||||
book['icon']='dialog_error.png'
|
||||
book['status'] = _('Bad URL')
|
||||
else:
|
||||
(book['url'],book['site'])=urlsitetuple
|
||||
|
||||
def get_story_url(self, db, book_id=None, path=None):
|
||||
if book_id == None:
|
||||
@@ -1928,11 +1987,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.
|
||||
@@ -1947,7 +2007,9 @@ 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']
|
||||
@@ -1966,7 +2028,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'))
|
||||
@@ -1982,7 +2043,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
|
||||
@@ -34,6 +35,7 @@ default_prefs['suppressauthorsort'] = False
|
||||
default_prefs['suppresstitlesort'] = False
|
||||
default_prefs['mark'] = False
|
||||
default_prefs['showmarked'] = False
|
||||
default_prefs['autoconvert'] = False
|
||||
default_prefs['urlsfromclip'] = True
|
||||
default_prefs['updatedefault'] = True
|
||||
default_prefs['fileform'] = 'epub'
|
||||
@@ -56,6 +58,7 @@ default_prefs['addtolistsonread'] = False
|
||||
default_prefs['gcnewonly'] = False
|
||||
default_prefs['gc_site_settings'] = {}
|
||||
default_prefs['allow_gc_from_ini'] = True
|
||||
default_prefs['gc_polish_cover'] = False
|
||||
|
||||
default_prefs['countpagesstats'] = []
|
||||
|
||||
|
||||
+406
-367
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
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+298
-268
File diff suppressed because it is too large
Load Diff
+409
-201
File diff suppressed because it is too large
Load Diff
+17
-18
@@ -23,6 +23,7 @@ import getpass
|
||||
import string
|
||||
import ConfigParser
|
||||
from subprocess import call
|
||||
import pprint
|
||||
|
||||
import logging
|
||||
if sys.version_info >= (2, 7):
|
||||
@@ -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.
@@ -122,6 +122,14 @@ import adapter_scarheadnet
|
||||
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
|
||||
|
||||
## This bit of complexity allows adapters to be added by just adding
|
||||
## importing. It eliminates the long if/else clauses we used to need
|
||||
@@ -145,6 +153,13 @@ for x in imports():
|
||||
__domain_map[site]=cls
|
||||
|
||||
def getNormalStoryURL(url):
|
||||
r = getNormalStoryURLSite(url)
|
||||
if r:
|
||||
return r[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def getNormalStoryURLSite(url):
|
||||
if not getNormalStoryURL.__dummyconfig:
|
||||
getNormalStoryURL.__dummyconfig = Configuration("test1.com","EPUB")
|
||||
# pulling up an adapter is pretty low over-head. If
|
||||
@@ -152,10 +167,11 @@ def getNormalStoryURL(url):
|
||||
try:
|
||||
adapter = getAdapter(getNormalStoryURL.__dummyconfig,url)
|
||||
url = adapter.url
|
||||
site = adapter.getSiteDomain()
|
||||
del adapter
|
||||
return url
|
||||
return (url,site)
|
||||
except:
|
||||
return None;
|
||||
return None
|
||||
|
||||
# kludgey function static/singleton
|
||||
getNormalStoryURL.__dummyconfig = None
|
||||
@@ -192,12 +208,14 @@ def getConfigSectionFor(url):
|
||||
|
||||
def getClassFor(url):
|
||||
## fix up leading protocol.
|
||||
fixedurl = re.sub(r"(?i)^[htps]+[:/]+","http://",url.strip())
|
||||
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 ):
|
||||
@@ -208,11 +226,11 @@ 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)
|
||||
|
||||
@@ -77,14 +77,14 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'archiveofourown.org'
|
||||
|
||||
@classmethod
|
||||
@classmethod
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://"+self.getSiteDomain()+"/works/123456 http://"+self.getSiteDomain()+"/collections/Some_Archive/works/123456 http://"+self.getSiteDomain()+"/works/123456/chapters/78901"
|
||||
|
||||
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)
|
||||
@@ -177,19 +180,28 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
self.story.addToList('authorUrl','http://'+self.host+a['href'])
|
||||
self.story.addToList('author',a.text)
|
||||
|
||||
newestChapter = None
|
||||
self.newestChapterNum = None # save for comparing during update.
|
||||
# Scan all chapters to find the oldest and newest, on AO3 it's
|
||||
# possible for authors to insert new chapters out-of-order or
|
||||
# change the dates of earlier ones by editing them--That WILL
|
||||
# break epub update.
|
||||
# Find the chapters:
|
||||
chapters=soup.findAll('a', href=re.compile(r'/works/'+self.story.getMetadata('storyId')+"/chapters/\d+$"))
|
||||
self.story.setMetadata('numChapters',len(chapters))
|
||||
logger.debug("numChapters: (%s)"%self.story.getMetadata('numChapters'))
|
||||
for x in range(0,len(chapters)):
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
chapter=chapters[x]
|
||||
if len(chapters)==1:
|
||||
self.chapterUrls.append((self.story.getMetadata('title'),'http://'+self.host+chapter['href']+addurl))
|
||||
else:
|
||||
if len(chapters)==1:
|
||||
self.chapterUrls.append((self.story.getMetadata('title'),'http://'+self.host+chapters[0]['href']+addurl))
|
||||
else:
|
||||
for index, chapter in enumerate(chapters):
|
||||
# strip just in case there's tags, like <i> in chapter titles.
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+chapter['href']+addurl))
|
||||
|
||||
|
||||
# (2013-09-21)
|
||||
date = stripHTML(chapter.findNext('span'))[1:-1]
|
||||
chapterDate = makeDate(date,self.dateformat)
|
||||
if newestChapter == None or chapterDate > newestChapter:
|
||||
newestChapter = chapterDate
|
||||
self.newestChapterNum = index
|
||||
|
||||
a = metasoup.find('blockquote',{'class':'userstuff'})
|
||||
if a != None:
|
||||
@@ -299,6 +311,12 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
self.setSeries(series_name, series_index)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
|
||||
def hookForUpdates(self,chaptercount):
|
||||
if self.oldchapters and len(self.oldchapters) > self.newestChapterNum:
|
||||
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)
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
@@ -306,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"})
|
||||
@@ -332,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,192 @@
|
||||
from datetime import timedelta
|
||||
import re
|
||||
import urllib2
|
||||
import urlparse
|
||||
|
||||
from .. import BeautifulSoup
|
||||
|
||||
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 soup.title.string.strip() == 'by':
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
|
||||
for option in soup.find('select', {'name': 'chapter'}):
|
||||
title = option.string.strip()
|
||||
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', title_anchor.string.strip())
|
||||
|
||||
author_anchor = title_anchor.findNextSibling('a')
|
||||
self.story.setMetadata('author', author_anchor.string.strip())
|
||||
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 = list_review.a.string.strip().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', first_listinfo.a.string.strip())
|
||||
|
||||
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)
|
||||
@@ -87,7 +87,7 @@ class BuffyNFaithNetAdapter(BaseSiteAdapter):
|
||||
|
||||
def setHeader(self):
|
||||
"buffynfaith.net wants a Referer for images. Used both above and below(after cookieproc added)"
|
||||
self.opener.addheaders = [('Referer', 'http://'+self.getSiteDomain()+'/')]
|
||||
self.opener.addheaders.append(('Referer', 'http://'+self.getSiteDomain()+'/'))
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(self):
|
||||
|
||||
@@ -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)
|
||||
@@ -57,6 +57,8 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
## accept m(mobile)url, but use www.
|
||||
self.origurl = self.origurl.replace("https://m.","https://www.")
|
||||
|
||||
self.opener.addheaders.append(('Referer',self.origurl))
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return 'www.fanfiction.net'
|
||||
@@ -72,6 +74,13 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
def getSiteURLPattern(self):
|
||||
return r"https?://(www|m)?\.fanfiction\.net/s/\d+(/\d+)?(/|/[^/]+)?/?$"
|
||||
|
||||
def _fetchUrl(self,url):
|
||||
time.sleep(1.0) ## ffnet(and, I assume, fpcom) tends to fail
|
||||
## more if hit too fast. This is in
|
||||
## additional to what ever the
|
||||
## slow_down_sleep_time setting is.
|
||||
return BaseSiteAdapter._fetchUrl(self,url)
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
# fetch the chapter. From that we will get almost all the
|
||||
@@ -98,28 +107,31 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
if "not found. Please check to see you are not using an outdated url." in data:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! 'Chapter not found. Please check to see you are not using an outdated url.'" % url)
|
||||
|
||||
try:
|
||||
# rather nasty way to check for a newer chapter. ffnet has a
|
||||
# tendency to send out update notices in email before all
|
||||
# their servers are showing the update on the first chapter.
|
||||
if self.getConfig('check_next_chapter'):
|
||||
try:
|
||||
chapcount = len(soup.find('select', { 'name' : 'chapter' } ).findAll('option'))
|
||||
# get chapter part of url.
|
||||
## ffnet used to have a tendency to send out update
|
||||
## notices in email before all their servers were
|
||||
## showing the update on the first chapter. It
|
||||
## generates another server request and doesn't seem
|
||||
## to be needed lately, so now default it to off.
|
||||
try:
|
||||
chapcount = len(soup.find('select', { 'name' : 'chapter' } ).findAll('option'))
|
||||
# get chapter part of url.
|
||||
except:
|
||||
chapcount = 1
|
||||
chapter = url.split('/',)[5]
|
||||
tryurl = "https://%s/s/%s/%d/"%(self.getSiteDomain(),
|
||||
self.story.getMetadata('storyId'),
|
||||
chapcount+1)
|
||||
logger.debug('=Trying newer chapter: %s' % tryurl)
|
||||
newdata = self._fetchUrl(tryurl)
|
||||
if "not found. Please check to see you are not using an outdated url." \
|
||||
not in newdata:
|
||||
logger.debug('=======Found newer chapter: %s' % tryurl)
|
||||
soup = bs.BeautifulSoup(newdata)
|
||||
except:
|
||||
chapcount = 1
|
||||
chapter = url.split('/',)[5]
|
||||
tryurl = "https://%s/s/%s/%d/"%(self.getSiteDomain(),
|
||||
self.story.getMetadata('storyId'),
|
||||
chapcount+1)
|
||||
logger.debug('=Trying newer chapter: %s' % tryurl)
|
||||
newdata = self._fetchUrl(tryurl)
|
||||
if "not found. Please check to see you are not using an outdated url." \
|
||||
not in newdata:
|
||||
logger.debug('=======Found newer chapter: %s' % tryurl)
|
||||
soup = bs.BeautifulSoup(newdata)
|
||||
except:
|
||||
pass
|
||||
|
||||
pass
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
a = soup.find('a', href=re.compile(r"^/u/\d+"))
|
||||
self.story.setMetadata('authorId',a['href'].split('/')[2])
|
||||
@@ -157,7 +169,7 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
|
||||
|
||||
a = soup.find('a', href='http://www.fictionratings.com/')
|
||||
a = soup.find('a', href=re.compile(r'https?://www\.fictionratings\.com/'))
|
||||
rating = a.string
|
||||
if 'Fiction' in rating: # if rating has 'Fiction ', strip that out for consistency with past.
|
||||
rating = rating[8:]
|
||||
@@ -275,7 +287,7 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
return
|
||||
|
||||
def getChapterText(self, url):
|
||||
time.sleep(5.0) ## ffnet(and, I assume, fpcom) tends to fail
|
||||
time.sleep(4.0) ## ffnet(and, I assume, fpcom) tends to fail
|
||||
## more if hit too fast. This is in
|
||||
## additional to what ever the
|
||||
## slow_down_sleep_time setting is.
|
||||
|
||||
@@ -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:
|
||||
@@ -101,7 +101,7 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
# if "/images/missing_story.png" in data:
|
||||
# raise exceptions.StoryDoesNotExist(self.url)
|
||||
|
||||
if "This story has been marked as having adult content." in data:
|
||||
if "This story has been marked as having adult content. Please click below to confirm you are of legal age to view adult material in your country." in data:
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
if self.password:
|
||||
@@ -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.
|
||||
@@ -249,18 +249,20 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
value = unicode(value)
|
||||
self.story.setMetadata(metakey, value)
|
||||
|
||||
#Sequel links and group links are each bundled into story_group_list containers.
|
||||
#Rather than mess around examining the header text, which is outside the containers,
|
||||
#one can tell the two link types apart by examining them directly.
|
||||
## Groups and sequels code from FaceDeer
|
||||
allGroupLists = soup.findAll('ul', {'id':'story_group_list'})
|
||||
for groupList in allGroupLists:
|
||||
for groupName in groupList.findAll('a', {'href':re.compile('^/group/')}):
|
||||
self.story.addToList("groupsUrl", 'http://'+self.host+groupName["href"])
|
||||
self.story.addToList("groups",stripHTML(groupName).replace(',', ';'))
|
||||
for sequel in groupList.findAll('a', {'class':'story_link'}):
|
||||
|
||||
sequelStoryHeader = soup.find('h1', {'class':'header-stories'}, text="Sequels")
|
||||
if not sequelStoryHeader == None:
|
||||
sequelContainer = sequelStoryHeader.parent.parent
|
||||
for sequel in sequelContainer.findAll('a', {'class':'story_link'}):
|
||||
self.story.addToList("sequelsUrl", 'http://'+self.host+sequel["href"])
|
||||
self.story.addToList("sequels", stripHTML(sequel).replace(',', ';'))
|
||||
|
||||
|
||||
#The link to the prequel is embedded in the description text, so erring
|
||||
#on the side of caution and wrapping this whole thing in a try block.
|
||||
#If anything goes wrong this probably wasn't a valid prequel link.
|
||||
@@ -281,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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2012 Fanficdownloader team
|
||||
# Copyright 2013 Fanficdownloader team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -37,11 +37,6 @@ class FineStoriesComAdapter(BaseSiteAdapter):
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.decode = ["Windows-1252",
|
||||
"utf8"] # 1252 is a superset of iso-8859-1.
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
self.is_adult=False
|
||||
@@ -51,7 +46,6 @@ class FineStoriesComAdapter(BaseSiteAdapter):
|
||||
if 'storyInfo' in self.story.getMetadata('storyId'):
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/s/storyInfo.php?id='+self.story.getMetadata('storyId'))
|
||||
|
||||
|
||||
@@ -128,23 +128,22 @@ class HPFanficArchiveComAdapter(BaseSiteAdapter):
|
||||
# <span class="label">Rated:</span> NC-17<br /> etc
|
||||
labels = soup.findAll('span',{'class':'label'})
|
||||
for labelspan in labels:
|
||||
value = labelspan.nextSibling
|
||||
val = labelspan.nextSibling
|
||||
value = unicode('')
|
||||
while val and not defaultGetattr(val,'class') == 'label':
|
||||
value += unicode(val)
|
||||
val = val.nextSibling
|
||||
label = labelspan.string
|
||||
#print("label:%s\nvalue:%s"%(label,value))
|
||||
|
||||
if 'Summary' in label:
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
self.setDescription(url,value)
|
||||
|
||||
if 'Rated' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
self.story.setMetadata('rating', stripHTML(value))
|
||||
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value)
|
||||
self.story.setMetadata('numWords', stripHTML(value))
|
||||
|
||||
if 'Categories' in label:
|
||||
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
@@ -161,13 +160,18 @@ 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:
|
||||
self.story.addToList('warnings',warning.string)
|
||||
|
||||
if 'Completed' in label:
|
||||
if 'Yes' in value:
|
||||
if 'Yes' in stripHTML(value):
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2013 Fanficdownloader team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
import urlparse
|
||||
import time
|
||||
|
||||
from .. import BeautifulSoup as bs
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
class LiteroticaSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.decode = ["utf8",
|
||||
"Windows-1252"] # 1252 is a superset of iso-8859-1.
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
|
||||
self.story.setMetadata('siteabbrev','litero')
|
||||
|
||||
# normalize to first chapter. Not sure if they ever have more than 2 digits.
|
||||
storyid = self.parsedUrl.path.split('/',)[2]
|
||||
if re.match(r'-ch\d\d$',storyid):
|
||||
storyid = storyid[:-2]+'01'
|
||||
self.story.setMetadata('storyId',storyid)
|
||||
|
||||
self.origurl = url
|
||||
if "//www.i." in self.origurl:
|
||||
## accept m(mobile)url, but use www.
|
||||
self.origurl = self.origurl.replace("//www.i.","//www.")
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL(url[:url.index('//')+2]+self.getSiteDomain()\
|
||||
+"/s/"+self.story.getMetadata('storyId'))
|
||||
|
||||
# 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'
|
||||
|
||||
@classmethod
|
||||
def getAcceptDomains(cls):
|
||||
return ['www.literotica.com', 'www.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 https://www.literotica.com/s/story-title"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return r"https?://www(\.i)?\.literotica\.com/s/([a-zA-Z0-9_-]+)"
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
if not (self.is_adult or self.getConfig("is_adult")):
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
url1 = self.origurl
|
||||
logger.debug("first page URL: "+url1)
|
||||
|
||||
try:
|
||||
data1 = self._fetchUrl(url1)
|
||||
soup1 = bs.BeautifulSoup(data1)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(url1)
|
||||
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'][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(authorurl)
|
||||
soupAuth = bs.BeautifulSoup(dataAuth)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(authorurl)
|
||||
else:
|
||||
raise e
|
||||
|
||||
## site has started using //domain.name/asdf urls remove https?: from front
|
||||
storyLink = soupAuth.find('a', href=url1[url1.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
|
||||
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
|
||||
else:
|
||||
#If we're already on the last page, copy the soup
|
||||
soup2 = soup1
|
||||
|
||||
# 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.
|
||||
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.
|
||||
chapurl = row.a['href']
|
||||
if chapurl.startswith('//'):
|
||||
chapurl = self.parsedUrl.scheme+':'+chapurl
|
||||
self.chapterUrls.append((row.a.string,chapurl))
|
||||
if not row.nextSibling:
|
||||
break
|
||||
row = row.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)
|
||||
|
||||
# normalize on first chapter URL.
|
||||
self._setURL(self.chapterUrls[0][1])
|
||||
|
||||
# reset storyId to first chapter.
|
||||
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
|
||||
|
||||
self.story.setMetadata('numChapters', len(self.chapterUrls))
|
||||
|
||||
self.story.setMetadata('category', soup1.find('div', 'b-breadcrumbs').findAll('a')[1].string)
|
||||
# deliberately not self.setDescription() because it's never HTML.
|
||||
self.story.setMetadata('description', soup1.find('meta', {'name': 'description'})['content'])
|
||||
|
||||
# li tags inside div class b-s-story-tag-list
|
||||
taglist = soup1.find('div', {'class':'b-s-story-tag-list'})
|
||||
if taglist:
|
||||
for li in taglist.findAll('a'):
|
||||
self.story.addToList('eroticatags',stripHTML(li))
|
||||
|
||||
return
|
||||
|
||||
def getChapterText(self, url):
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
time.sleep(0.5)
|
||||
data1 = self._fetchUrl(url)
|
||||
soup1 = bs.BeautifulSoup(data1)
|
||||
|
||||
#strip comments from soup
|
||||
[comment.extract() for comment in soup1.findAll(text=lambda text:isinstance(text, bs.Comment))]
|
||||
|
||||
# get story text
|
||||
story1 = soup1.find('div', 'b-story-body-x').p
|
||||
story1.name='div'
|
||||
story1.append('<br />')
|
||||
storytext = self.utf8FromSoup(url,story1)
|
||||
|
||||
# find num pages
|
||||
pgs = int(soup1.find("span", "b-pager-caption-t r-d45").string.split(' ')[0])
|
||||
logger.debug("pages: "+str(pgs))
|
||||
|
||||
# get all the pages
|
||||
for i in xrange(2, pgs+1):
|
||||
try:
|
||||
logger.debug("fetching page "+str(i))
|
||||
time.sleep(0.5)
|
||||
data2 = self._fetchUrl(url, {'page': i})
|
||||
soup2 = bs.BeautifulSoup(data2)
|
||||
[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 />')
|
||||
storytext += self.utf8FromSoup(url,story2)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(url)
|
||||
else:
|
||||
raise e
|
||||
return storytext
|
||||
|
||||
|
||||
def getClass():
|
||||
return LiteroticaSiteAdapter
|
||||
|
||||
|
||||
|
||||
@@ -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,206 @@
|
||||
import re
|
||||
import urllib2
|
||||
import urlparse
|
||||
|
||||
from .. import BeautifulSoup
|
||||
|
||||
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', pagetitle_div.a.string.strip())
|
||||
|
||||
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.strip())
|
||||
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.strip())
|
||||
|
||||
listbox_tag = soup.find('div', {'class': 'listbox'})
|
||||
for span_tag in listbox_tag('span'):
|
||||
key = span_tag.string.strip(' :')
|
||||
try:
|
||||
value = span_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(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', sibling.string.strip())
|
||||
|
||||
# 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', sibling.string.strip())
|
||||
|
||||
elif key == 'Genres':
|
||||
for sibling in span_tag.findNextSiblings(['a', 'br']):
|
||||
if sibling.name == 'br':
|
||||
break
|
||||
|
||||
self.story.addToList('genre', sibling.string.strip())
|
||||
|
||||
elif key == 'Warnings':
|
||||
for sibling in span_tag.findNextSiblings(['a', 'br']):
|
||||
if sibling.name == 'br':
|
||||
break
|
||||
self.story.addToList('warnings', sibling.string.strip())
|
||||
|
||||
# Challenges
|
||||
|
||||
elif key == 'Series':
|
||||
a = span_tag.findNextSibling('a')
|
||||
if not a:
|
||||
continue
|
||||
self.story.setMetadata('series', a.string.strip())
|
||||
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 = chapter_anchor.string.strip()
|
||||
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))
|
||||
|
||||
@@ -37,11 +37,6 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.decode = ["Windows-1252",
|
||||
"utf8"] # 1252 is a superset of iso-8859-1.
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
self.is_adult=False
|
||||
@@ -71,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 = {}
|
||||
@@ -119,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
|
||||
|
||||
@@ -147,6 +146,10 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
a = soup.find('h1')
|
||||
self.story.setMetadata('title',stripHTML(a))
|
||||
|
||||
notice = soup.find('div', {'class' : 'notice'})
|
||||
if notice:
|
||||
self.story.setMetadata('notice',unicode(notice))
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
a = soup.find('a', href=re.compile(r"/a/\w+"))
|
||||
self.story.setMetadata('authorId',a['href'].split('/')[2])
|
||||
@@ -172,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:
|
||||
@@ -185,23 +188,74 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('size', lc2.findNext('td', {'class' : 'num'}).text)
|
||||
|
||||
lc4 = lc2.findNext('td', {'class' : 'lc4'})
|
||||
|
||||
desc = lc4.contents[0]
|
||||
|
||||
try:
|
||||
a = lc4.find('a', href=re.compile(r"/library/show_series.php\?id=\d+"))
|
||||
i = a.parent.text.split('(')[1].split(')')[0]
|
||||
self.setSeries(stripHTML(a), i)
|
||||
self.story.setMetadata('seriesUrl','http://'+self.host+a['href'])
|
||||
if a:
|
||||
# if there's a number after the series name, series_contents is a two element list:
|
||||
# [<a href="...">Title</a>, u' (2)']
|
||||
series_contents = a.parent.contents
|
||||
i = 0 if len(series_contents) == 1 else series_contents[1].strip(' ()')
|
||||
seriesUrl = 'http://'+self.host+a['href']
|
||||
self.story.setMetadata('seriesUrl',seriesUrl)
|
||||
series_name = stripHTML(a)
|
||||
logger.debug("Series name= %s" % series_name)
|
||||
series_soup = bs.BeautifulSoup(self._fetchUrl(seriesUrl))
|
||||
if series_soup:
|
||||
logger.debug("Retrieving Series - looking for name")
|
||||
series_name = series_soup.find('span', {'id' : 'ptitle'}).text.partition(' — ')[0]
|
||||
logger.debug("Series name: '{0}'".format(series_name))
|
||||
self.setSeries(series_name, i)
|
||||
desc = lc4.contents[2]
|
||||
# Check if series is in a universe
|
||||
universes_soup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl') + "&type=uni"))
|
||||
# logger.debug("Universe page=", universes_soup)
|
||||
if universes_soup:
|
||||
universes = universes_soup.findAll('div', {'class' : 'ser-box'})
|
||||
logger.debug("Number of Universes: %d" % len(universes))
|
||||
for universe in universes:
|
||||
logger.debug("universe.find('a')={0}".format(universe.find('a')))
|
||||
# The universe id is in an "a" tag that has an id but nothing else. It is the first tag.
|
||||
# The id is prefixed with the letter "u".
|
||||
universe_id = universe.find('a')['id'][1:]
|
||||
logger.debug("universe_id='%s'" % universe_id)
|
||||
universe_name = universe.find('div', {'class' : 'ser-name'}).text.partition(' ')[2]
|
||||
logger.debug("universe_name='%s'" % universe_name)
|
||||
# If there is link to the story, we have the right universe
|
||||
story_a = universe.find('a', {'href' : '/s/'+self.story.getMetadata('storyId')})
|
||||
if story_a:
|
||||
logger.debug("Story is in a series that is in a universe! The universe is '%s'" % universe_name)
|
||||
self.story.setMetadata("universe", universe_name)
|
||||
self.story.setMetadata('universeUrl','http://'+self.host+ '/library/universe.php?id=' + universe_id)
|
||||
break
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
a = lc4.find('a', href=re.compile(r"/library/universe.php\?id=\d+"))
|
||||
if a:
|
||||
self.story.setMetadata("universe",stripHTML(a))
|
||||
desc = lc4.contents[2]
|
||||
# Assumed only one universe, but it does have a URL--use universeHTML
|
||||
universe_name = stripHTML(a)
|
||||
universeUrl = 'http://'+self.host+a['href']
|
||||
logger.debug("Retrieving Universe - about to get page")
|
||||
universe_soup = bs.BeautifulSoup(self._fetchUrl(universeUrl))
|
||||
logger.debug("Retrieving Universe - have page")
|
||||
if universe_soup:
|
||||
logger.debug("Retrieving Universe - looking for name")
|
||||
universe_name = universe_soup.find('span', {'id' : 'ptitle'}).text.partition(' —')[0]
|
||||
logger.debug("Universes name: '{0}'".format(universe_name))
|
||||
|
||||
self.story.setMetadata('universeUrl',universeUrl)
|
||||
logger.debug("Setting universe name: '{0}'".format(universe_name))
|
||||
self.story.setMetadata('universe',universe_name)
|
||||
if self.getConfig("universe_as_series"):
|
||||
self.setSeries(universe_name, 0)
|
||||
self.story.setMetadata('seriesUrl',universeUrl)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
desc = lc4.contents[0]
|
||||
self.setDescription('http://'+self.host+'/s/'+self.story.getMetadata('storyId'),desc)
|
||||
|
||||
for b in lc4.findAll('b'):
|
||||
@@ -255,8 +309,18 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
# some big chapters are split over several pages
|
||||
pager = div.find('span', {'class' : 'pager'})
|
||||
if pager != None:
|
||||
a = pager.previousSibling
|
||||
while a != None:
|
||||
logger.debug("before pager: {0}".format(a))
|
||||
b = a.previousSibling
|
||||
a.extract()
|
||||
a = b
|
||||
|
||||
urls=pager.findAll('a')
|
||||
urls=urls[:len(urls)-1]
|
||||
pager.extract()
|
||||
div.contents = div.contents[2:]
|
||||
# logger.debug(div)
|
||||
|
||||
for ur in urls:
|
||||
soup = bs.BeautifulSoup(self._fetchUrl("http://"+self.getSiteDomain()+ur['href']),
|
||||
@@ -264,38 +328,76 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
|
||||
div1 = soup.find('div', {'id' : 'story'})
|
||||
|
||||
# appending next section
|
||||
last=div.findAll('p')
|
||||
next=div1.find('span', {'class' : 'conTag'}).nextSibling
|
||||
|
||||
last[len(last)-1]=last[len(last)-1].append(next)
|
||||
div.append(div1)
|
||||
|
||||
# removing all the left-over stuff
|
||||
for a in div.findAll('span'):
|
||||
a.extract()
|
||||
|
||||
for a in div.findAll('h1'):
|
||||
a.extract()
|
||||
for a in div.findAll('h2'):
|
||||
a.extract()
|
||||
for a in div.findAll('h3'):
|
||||
a.extract()
|
||||
for a in div.findAll('h4'):
|
||||
a.extract()
|
||||
for a in div.findAll('br'):
|
||||
a.extract()
|
||||
for a in div.findAll('div', {'class' : 'date'}):
|
||||
a.extract()
|
||||
|
||||
a = div.find('form')
|
||||
if a != None:
|
||||
b = a.nextSibling
|
||||
while b != None:
|
||||
# Find the "Continues" marker on the current page and remove everything after that.
|
||||
continues = div.find('span', {'class' : 'conTag'})
|
||||
if continues != None:
|
||||
while continues != None:
|
||||
# logger.debug("removing end: {0}".format(continues))
|
||||
b = continues.nextSibling
|
||||
continues.extract()
|
||||
continues = b
|
||||
|
||||
# Find the "Continued" marker and delete everything before that
|
||||
continued = div1.find('span', {'class' : 'conTag'})
|
||||
if continued != None:
|
||||
a = continued.previousSibling
|
||||
while a != None:
|
||||
# logger.debug("before conTag: {0}".format(a))
|
||||
b = a.previousSibling
|
||||
a.extract()
|
||||
a = b
|
||||
# Remove the pager from the end if this is the last page
|
||||
endPager = div1.find('span', {'class' : 'pager'})
|
||||
if endPager != None:
|
||||
b = endPager.nextSibling
|
||||
while endPager != None:
|
||||
logger.debug("removing end: {0}".format(endPager))
|
||||
b = endPager.nextSibling
|
||||
endPager.extract()
|
||||
endPager = b
|
||||
div1.contents = div1.contents[:len(div1) - 2]
|
||||
# logger.debug("after removing pager: {0}".format(div1))
|
||||
for tag in div1.contents[2:]:
|
||||
div.append(tag)
|
||||
|
||||
# If it is a chapter, there are dates at the start for when it was posted or modified. These plus
|
||||
# everything before them can be discarded.
|
||||
postedDates = div.findAll('div', {'class' : 'date'})
|
||||
if postedDates:
|
||||
a = postedDates[0].previousSibling
|
||||
while a != None:
|
||||
# logger.debug("before dates: {0}".format(a))
|
||||
b = a.previousSibling
|
||||
a.extract()
|
||||
a=b
|
||||
b=b.nextSibling
|
||||
|
||||
a = b
|
||||
for a in div.findAll('div', {'class' : 'date'}):
|
||||
a.extract()
|
||||
|
||||
# For single chapter stories, there is a copyright statement. Remove this and everything
|
||||
# before it.
|
||||
copy = div.find('h4', {'class': 'copy'})
|
||||
while copy != None:
|
||||
# logger.debug("before copyright: {0}".format(copy))
|
||||
b = copy.previousSibling
|
||||
copy.extract()
|
||||
copy = b
|
||||
|
||||
# For a story or the last chapter, remove voting form and the in library box
|
||||
a = div.find('div', {'id' : 'vote-form'})
|
||||
if a != None:
|
||||
a.extract()
|
||||
a = div.find('div', {'id' : 'b-man-div'})
|
||||
if a != None:
|
||||
a.extract()
|
||||
|
||||
# Kill the "The End" header and everything after it.
|
||||
a = div.find(['h2', 'h3'], {'class' : 'end'})
|
||||
logger.debug("Chapter end= '{0}'".format(a))
|
||||
while a != None:
|
||||
b = a.nextSibling
|
||||
a.extract()
|
||||
a=b
|
||||
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
@@ -50,6 +50,7 @@ class TestSiteAdapter(BaseSiteAdapter):
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
idstr = self.story.getMetadata('storyId')
|
||||
idnum = int(idstr)
|
||||
self.do_sleep()
|
||||
|
||||
if idnum >= 1000:
|
||||
logger.warn("storyId:%s - Custom INI data will be used."%idstr)
|
||||
@@ -218,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')
|
||||
@@ -296,6 +300,7 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
|
||||
|
||||
def getChapterText(self, url):
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
self.do_sleep()
|
||||
if self.story.getMetadata('storyId').startswith('670') or \
|
||||
self.story.getMetadata('storyId').startswith('672'):
|
||||
time.sleep(1.0)
|
||||
|
||||
@@ -65,12 +65,16 @@ class TrekiverseOrgAdapter(BaseSiteAdapter):
|
||||
# The site domain.
|
||||
return 'trekiverse.org'
|
||||
|
||||
@classmethod
|
||||
def getAcceptDomains(cls):
|
||||
return ['trekiverse.org','efiction.trekiverse.org']
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://"+self.getSiteDomain()+"/efiction/viewstory.php?sid=1234"
|
||||
return "http://"+self.getSiteDomain()+"/efiction/viewstory.php?sid=1234 http://efiction."+self.getSiteDomain()+"/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return r'http://trekiverse\.org/efiction/viewstory\.php\?sid=\d+'
|
||||
return r'(http://trekiverse\.org/efiction/viewstory\.php\?sid=\d+|http://efiction\.trekiverse\.org/viewstory\.php\?sid=\d+)'
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -76,6 +76,8 @@ class BaseSiteAdapter(Configurable):
|
||||
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
self.is_adult=False
|
||||
|
||||
self.override_sleep = None
|
||||
|
||||
self.opener = u2.build_opener(u2.HTTPCookieProcessor(),GZipProcessor())
|
||||
## Specific UA because too many sites are blocking the default python UA.
|
||||
@@ -147,8 +149,7 @@ class BaseSiteAdapter(Configurable):
|
||||
|
||||
# Assumes application/x-www-form-urlencoded. parameters, headers are dict()s
|
||||
def _postUrl(self, url, parameters={}, headers={}):
|
||||
if self.getConfig('slow_down_sleep_time'):
|
||||
time.sleep(float(self.getConfig('slow_down_sleep_time')))
|
||||
self.do_sleep()
|
||||
|
||||
## u2.Request assumes POST when data!=None. Also assumes data
|
||||
## is application/x-www-form-urlencoded.
|
||||
@@ -159,24 +160,38 @@ 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)
|
||||
self.override_sleep = val
|
||||
|
||||
def do_sleep(self):
|
||||
if self.override_sleep:
|
||||
time.sleep(float(self.override_sleep))
|
||||
elif self.getConfig('slow_down_sleep_time'):
|
||||
time.sleep(float(self.getConfig('slow_down_sleep_time')))
|
||||
|
||||
# parameters is a dict()
|
||||
def _fetchUrl(self, url, parameters=None):
|
||||
if self.getConfig('slow_down_sleep_time'):
|
||||
time.sleep(float(self.getConfig('slow_down_sleep_time')))
|
||||
self.do_sleep()
|
||||
|
||||
excpt=None
|
||||
for sleeptime in [0, 0.5, 4, 9]:
|
||||
time.sleep(sleeptime)
|
||||
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)))
|
||||
break # break out on 404
|
||||
except Exception, e:
|
||||
excpt=e
|
||||
logger.warn("Caught an exception reading URL: %s Exception %s."%(unicode(url),unicode(e)))
|
||||
@@ -313,7 +328,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...)
|
||||
@@ -367,7 +383,7 @@ class BaseSiteAdapter(Configurable):
|
||||
|
||||
# Don't want body tags in chapter html--writers add them.
|
||||
# This is primarily for epub updates.
|
||||
retval = re.sub(r"</?body>\r?\n?","",retval)
|
||||
retval = re.sub(r"</?body[^>]*>\r?\n?","",retval)
|
||||
|
||||
if self.getConfig("replace_br_with_p"):
|
||||
# Apply heuristic processing to replace <br> paragraph
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -137,32 +137,35 @@ def replace_br_with_p(body):
|
||||
contentLinesSum += lineLen
|
||||
if lineLen > longestLineLength:
|
||||
longestLineLength = lineLen
|
||||
|
||||
if contentLines == 0:
|
||||
contentLines = 1
|
||||
|
||||
averageLineLength = contentLinesSum/contentLines
|
||||
|
||||
logger.debug(u'---')
|
||||
logger.debug(u'Lines.............: ' + str(len(lines)))
|
||||
logger.debug(u'contentLines......: ' + str(contentLines))
|
||||
logger.debug(u'contentLinesSum...: ' + str(contentLinesSum))
|
||||
logger.debug(u'longestLineLength.: ' + str(longestLineLength))
|
||||
logger.debug(u'averageLineLength.: ' + str(averageLineLength))
|
||||
logger.debug(u'Lines.............: ' + unicode(len(lines)))
|
||||
logger.debug(u'contentLines......: ' + unicode(contentLines))
|
||||
logger.debug(u'contentLinesSum...: ' + unicode(contentLinesSum))
|
||||
logger.debug(u'longestLineLength.: ' + unicode(longestLineLength))
|
||||
logger.debug(u'averageLineLength.: ' + unicode(averageLineLength))
|
||||
|
||||
if breaksMaxIndex == len(breaksCount)-1 and breaksMax < 2:
|
||||
breaksMaxIndex = 0
|
||||
breaksMax = breaksCount[0]
|
||||
|
||||
logger.debug(u'---')
|
||||
logger.debug(u'breaks 1: ' + str(breaksCount[0]))
|
||||
logger.debug(u'breaks 2: ' + str(breaksCount[1]))
|
||||
logger.debug(u'breaks 3: ' + str(breaksCount[2]))
|
||||
logger.debug(u'breaks 4: ' + str(breaksCount[3]))
|
||||
logger.debug(u'breaks 5: ' + str(breaksCount[4]))
|
||||
logger.debug(u'breaks 6: ' + str(breaksCount[5]))
|
||||
logger.debug(u'breaks 7: ' + str(breaksCount[6]))
|
||||
logger.debug(u'breaks 8: ' + str(breaksCount[7]))
|
||||
logger.debug(u'breaks 1: ' + unicode(breaksCount[0]))
|
||||
logger.debug(u'breaks 2: ' + unicode(breaksCount[1]))
|
||||
logger.debug(u'breaks 3: ' + unicode(breaksCount[2]))
|
||||
logger.debug(u'breaks 4: ' + unicode(breaksCount[3]))
|
||||
logger.debug(u'breaks 5: ' + unicode(breaksCount[4]))
|
||||
logger.debug(u'breaks 6: ' + unicode(breaksCount[5]))
|
||||
logger.debug(u'breaks 7: ' + unicode(breaksCount[6]))
|
||||
logger.debug(u'breaks 8: ' + unicode(breaksCount[7]))
|
||||
logger.debug(u'----')
|
||||
logger.debug(u'max found: ' + str(breaksMax))
|
||||
logger.debug(u'max Index: ' + str(breaksMaxIndex))
|
||||
logger.debug(u'max found: ' + unicode(breaksMax))
|
||||
logger.debug(u'max Index: ' + unicode(breaksMaxIndex))
|
||||
logger.debug(u'----')
|
||||
|
||||
if breaksMaxIndex > 0 and breaksCount[0] > breaksMax and averageLineLength < 90:
|
||||
@@ -173,13 +176,13 @@ def replace_br_with_p(body):
|
||||
for i in range(len(breaksCount)):
|
||||
# if i > 0 or breaksMaxIndex == 0:
|
||||
if i <= breaksMaxIndex:
|
||||
logger.debug(str(i) + u' <= breaksMaxIndex (' + str(breaksMaxIndex) + u')')
|
||||
logger.debug(unicode(i) + u' <= breaksMaxIndex (' + unicode(breaksMaxIndex) + u')')
|
||||
body = breaksRegexp[i].sub(r'\1</p>\n<p>\3', body)
|
||||
elif i == breaksMaxIndex+1:
|
||||
logger.debug(str(i) + u' == breaksMaxIndex+1 (' + str(breaksMaxIndex+1) + u')')
|
||||
logger.debug(unicode(i) + u' == breaksMaxIndex+1 (' + unicode(breaksMaxIndex+1) + u')')
|
||||
body = breaksRegexp[i].sub(r'\1</p>\n<p><br/></p>\n<p>\3', body)
|
||||
else:
|
||||
logger.debug(str(i) + u' > breaksMaxIndex+1 (' + str(breaksMaxIndex+1) + u')')
|
||||
logger.debug(unicode(i) + u' > breaksMaxIndex+1 (' + unicode(breaksMaxIndex+1) + u')')
|
||||
body = breaksRegexp[i].sub(r'\1</p>\n<hr />\n<p>\3', body)
|
||||
|
||||
body = breaksRegexp[8].sub(r'</p>\n<hr />\n<p>', body)
|
||||
@@ -230,7 +233,7 @@ def replace_br_with_p(body):
|
||||
return tag_sanitizer(body)
|
||||
|
||||
def is_valid_block(block):
|
||||
return str(block).find('<') == 0 and str(block).find('<!') != 0
|
||||
return unicode(block).find('<') == 0 and unicode(block).find('<!') != 0
|
||||
|
||||
def soup_up_div(body):
|
||||
blockTags = ['address', 'blockquote', 'del', 'div', 'dl', 'fieldset', 'form', 'ins', 'noscript', 'ol', 'p', 'pre', 'table', 'ul']
|
||||
@@ -247,8 +250,8 @@ def soup_up_div(body):
|
||||
lastElement = 1 # 1 = block, 2 = nested, 3 = invalid
|
||||
|
||||
for i in soup.contents[0]:
|
||||
if str(i).strip().__len__() > 0:
|
||||
s = str(i)
|
||||
if unicode(i).strip().__len__() > 0:
|
||||
s = unicode(i)
|
||||
if type(i) == bs.Tag:
|
||||
if i.name in blockTags:
|
||||
if lastElement > 1:
|
||||
@@ -308,7 +311,7 @@ def tag_sanitizer(html):
|
||||
is_closed = is_closed_tag(rTag[0]) or is_comment_tag(rTag[0])
|
||||
|
||||
# is_comment = is_comment_tag(rTag[0])
|
||||
# logger.debug(u'%s > isEnd: %s > isClosed: %s > isComment: %s'%(name, str(is_end), str(is_closed), str(is_comment)))
|
||||
# logger.debug(u'%s > isEnd: %s > isClosed: %s > isComment: %s'%(name, unicode(is_end), unicode(is_closed), unicode(is_comment)))
|
||||
# logger.debug(u'> %s%s\n'%(rTag[0], rTag[1]))
|
||||
|
||||
if name in blockTags:
|
||||
|
||||
+222
-37
@@ -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.
|
||||
@@ -297,11 +428,19 @@ 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
|
||||
(metakeys,regexp,replacement,condkey,condregexp) = replaceline
|
||||
if (metakeys == None or key in metakeys) \
|
||||
and isinstance(value,basestring) \
|
||||
and regexp.search(value):
|
||||
@@ -311,8 +450,32 @@ 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:
|
||||
retlist = [regexp.sub(replacement,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 +489,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 +521,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,21 +542,26 @@ 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'))
|
||||
for k in self.getConfigList('make_linkhtml_entries'):
|
||||
# Assuming list, because it has to be site specific and
|
||||
# they are all lists.
|
||||
# they are all lists. Bail if kUrl list not the same
|
||||
# length.
|
||||
# logger.debug("\nk:%s\nlist:%s\nlistURL:%s"%(k,self.getList(k),self.getList(k+'Url')))
|
||||
if len(self.getList(k+'Url')) != len(self.getList(k)):
|
||||
continue
|
||||
htmllist=[]
|
||||
for i, v in enumerate(self.getList(k)):
|
||||
url = self.getList(k+'Url')[i]
|
||||
@@ -403,9 +573,9 @@ class Story(Configurable):
|
||||
url=removeAllEntities(url)
|
||||
v=removeAllEntities(v)
|
||||
|
||||
htmllist.append(linkhtml%('author',url,v))
|
||||
join_string = self.getConfig("join_string_"+k+"HTML",u", ").replace('\s',' ')
|
||||
self.setMetadata(k+'HTML',join_string.join(htmllist))
|
||||
htmllist.append(linkhtml%(k,url,v))
|
||||
# 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:
|
||||
@@ -420,11 +590,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.
|
||||
@@ -436,7 +607,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) )
|
||||
|
||||
@@ -465,16 +636,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:
|
||||
@@ -568,7 +743,13 @@ class Story(Configurable):
|
||||
imgurl = url
|
||||
else:
|
||||
parsedUrl = urlparse.urlparse(parenturl)
|
||||
if url.startswith("/") :
|
||||
if url.startswith("//") :
|
||||
imgurl = urlparse.urlunparse(
|
||||
(parsedUrl.scheme,
|
||||
'',
|
||||
url,
|
||||
'','',''))
|
||||
elif url.startswith("/") :
|
||||
imgurl = urlparse.urlunparse(
|
||||
(parsedUrl.scheme,
|
||||
parsedUrl.netloc,
|
||||
@@ -587,6 +768,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)
|
||||
@@ -618,7 +803,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]
|
||||
@@ -637,7 +822,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})
|
||||
|
||||
+7
-16
@@ -46,17 +46,6 @@
|
||||
{{yourfile}}
|
||||
<!-- </div> -->
|
||||
|
||||
<h3>fanfiction.net / fimfiction.net</h3>
|
||||
<p>
|
||||
Fanfiction.net appears to be blocking access from Google
|
||||
App Engine, which prevents this web service. There's
|
||||
nothing I can do about it. At the time of writing, the
|
||||
latest CLI and calibre plugin versions worked.
|
||||
</p>
|
||||
<p>It appears that FimFiction.net is also blocking access from Google
|
||||
App Engine now.
|
||||
</p>
|
||||
|
||||
{% if authorized %}
|
||||
<form action="/fdown" method="post">
|
||||
<div id='urlbox'>
|
||||
@@ -66,14 +55,16 @@
|
||||
</div>
|
||||
<!-- 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>New Site: trekiverse.org/efiction/</li>
|
||||
<li>Fix for higher rated stories with not logged in on tthfanfic.org.</li>
|
||||
<li>Add prequels/sequels & fix groups site specific metadata for fimfiction.net.</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>
|
||||
<li>Reset version number to align with caliber plugin version.</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Questions? Check out our
|
||||
<a href="http://code.google.com/p/fanficdownloader/wiki/FanFictionDownloaderFAQs">FAQs</a>.
|
||||
@@ -82,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-85.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
|
||||
<a href="http://4-5-07.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.
|
||||
|
||||
+428
-202
File diff suppressed because it is too large
Load Diff
-10
@@ -51,16 +51,6 @@
|
||||
by {{ fic.author }} ({{ fic.format }})
|
||||
{% endif %}
|
||||
{% if fic.failure %}
|
||||
<h3>fanfiction.net / fimfiction.net</h3>
|
||||
<p>
|
||||
FYI, fanfiction.net appears to be blocking access from Google
|
||||
App Engine, which prevents this web service. There's
|
||||
nothing I can do about it. At the time of writing, the
|
||||
latest CLI and calibre plugin versions worked.
|
||||
</p>
|
||||
<p>It appears that FimFiction.net is also blocking access from Google
|
||||
App Engine now.
|
||||
</p>
|
||||
<span id='error'>{{ fic.failure }}</span>
|
||||
{% endif %}
|
||||
{% if not fic.completed and not fic.failure %}
|
||||
|
||||
Reference in New Issue
Block a user