mirror of
https://github.com/wassname/FanFicFare.git
synced 2026-09-13 12:11:20 +08:00
Compare commits
268
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d52d2f2438 | ||
|
|
e47e4bf29a | ||
|
|
445d676d24 | ||
|
|
ef71577b73 | ||
|
|
951fd68ce6 | ||
|
|
e740166ba4 | ||
|
|
54616e9892 | ||
|
|
3fbfa5b56c | ||
|
|
a606db85d1 | ||
|
|
15abab181f | ||
|
|
d81b365aba | ||
|
|
6e897c78f1 | ||
|
|
52ccebf16e | ||
|
|
a0e9123c58 | ||
|
|
6e93ded2a3 | ||
|
|
d95b96b9c4 | ||
|
|
d14b100d7e | ||
|
|
66f9d4f7e0 | ||
|
|
cd342bb352 | ||
|
|
ae7ffcfc32 | ||
|
|
863ee5c44b | ||
|
|
8d094fc26e | ||
|
|
ee85c13e75 | ||
|
|
367bea316b | ||
|
|
e5168d1d98 | ||
|
|
ff4559a8ad | ||
|
|
3cfa3179c9 | ||
|
|
e3970a64de | ||
|
|
2320e118b3 | ||
|
|
53d76052a8 | ||
|
|
9471a74527 | ||
|
|
7ba9290c7d | ||
|
|
64f60b4540 | ||
|
|
2adbcdc23e | ||
|
|
d3ab5e2024 | ||
|
|
635170f664 | ||
|
|
5ac90d3cdb | ||
|
|
394b21ab0e | ||
|
|
df6599a9cc | ||
|
|
d380f8b05c | ||
|
|
cf1ecee8e9 | ||
|
|
5a85524629 | ||
|
|
65e6bce0bc | ||
|
|
12161a8224 | ||
|
|
19d181a90f | ||
|
|
159d33f287 | ||
|
|
bbd806ab95 | ||
|
|
a4f82bf841 | ||
|
|
389b658135 | ||
|
|
7bcd4143e5 | ||
|
|
e9f010a162 | ||
|
|
babfc35f7b | ||
|
|
cefcb9ab96 | ||
|
|
7a763a8516 | ||
|
|
a191521649 | ||
|
|
e108c2d828 | ||
|
|
7534c03a37 | ||
|
|
bf2e71e17f | ||
|
|
110960169a | ||
|
|
fb9d128687 | ||
|
|
e99c3d6ea6 | ||
|
|
1172322446 | ||
|
|
2f4ce5c40e | ||
|
|
73c7ffeff6 | ||
|
|
0a78a0c044 | ||
|
|
dc5adb7f4a | ||
|
|
4abfbdf462 | ||
|
|
d1b73b9a6a | ||
|
|
e477a9870d | ||
|
|
2dbbb0f13d | ||
|
|
3d1d3f4e26 | ||
|
|
7a5d77975a | ||
|
|
c3911a279b | ||
|
|
8cf6f210e6 | ||
|
|
539426b41d | ||
|
|
78efbb3e1e | ||
|
|
4878837805 | ||
|
|
8347f4490e | ||
|
|
360d37746d | ||
|
|
8af36f298e | ||
|
|
831370134b | ||
|
|
de37c4aa1d | ||
|
|
2cb139147a | ||
|
|
9ce6117688 | ||
|
|
2e38ef1122 | ||
|
|
4c4576f331 | ||
|
|
9aa75905c6 | ||
|
|
76823dccfb | ||
|
|
0d0778fea5 | ||
|
|
5823d335a4 | ||
|
|
db3878668b | ||
|
|
02289c0af1 | ||
|
|
7a840043f0 | ||
|
|
0c3ccb4e7c | ||
|
|
e8904ec061 | ||
|
|
a589cf4280 | ||
|
|
cf11959970 | ||
|
|
49777c299e | ||
|
|
4acffb88f6 | ||
|
|
6e38557454 | ||
|
|
f24c363d3b | ||
|
|
e7ea699bc9 | ||
|
|
9de65d94f3 | ||
|
|
da7498d202 | ||
|
|
05ec7bce2b | ||
|
|
241c4d8d52 | ||
|
|
ccf4c8cc4e | ||
|
|
7498a9aa93 | ||
|
|
25c63c3a47 | ||
|
|
c5e5a9bb84 | ||
|
|
9bc36c3652 | ||
|
|
0736c35be0 | ||
|
|
cd4f2c2717 | ||
|
|
e6bb8c557b | ||
|
|
f4da7dc1bd | ||
|
|
eba312e777 | ||
|
|
0cd615d950 | ||
|
|
4b120bb2d3 | ||
|
|
c9bcf9175e | ||
|
|
c76facd40c | ||
|
|
f1d52834d1 | ||
|
|
e8ac7f8a89 | ||
|
|
3e0f92d8ce | ||
|
|
2682d0fe36 | ||
|
|
125e29091f | ||
|
|
d0c73d5444 | ||
|
|
bd8e54edcf | ||
|
|
d680a86f0c | ||
|
|
f075ae582d | ||
|
|
928ebb9751 | ||
|
|
ce262be162 | ||
|
|
6faa6850af | ||
|
|
f392c6dd77 | ||
|
|
af0dff28b4 | ||
|
|
02a75a821f | ||
|
|
c06028b498 | ||
|
|
de4b95af9b | ||
|
|
5366355d96 | ||
|
|
264853d768 | ||
|
|
d82b399738 | ||
|
|
59446d23dc | ||
|
|
6fcbdd8a8d | ||
|
|
1348525d25 | ||
|
|
40bac62c5e | ||
|
|
362f15f9fa | ||
|
|
b833041dc4 | ||
|
|
fa67220b86 | ||
|
|
4f412eb89f | ||
|
|
6a4aa4340e | ||
|
|
dc785b911e | ||
|
|
d403f916a9 | ||
|
|
29fe1a6e24 | ||
|
|
131a08c0dc | ||
|
|
fbd26c16e0 | ||
|
|
34ebba40d0 | ||
|
|
7ce8436208 | ||
|
|
b247e4fc7b | ||
|
|
bf509bb6ee | ||
|
|
8b4f1953fe | ||
|
|
cf7e913e60 | ||
|
|
402366a3e5 | ||
|
|
bc0ef37fe6 | ||
|
|
3b77a6b1c1 | ||
|
|
f603762261 | ||
|
|
094f864252 | ||
|
|
34e67ebb26 | ||
|
|
7dae1140a7 | ||
|
|
c4b1810887 | ||
|
|
e9d3ef2b9a | ||
|
|
04895411b1 | ||
|
|
e8706b19fd | ||
|
|
dc86c90b9e | ||
|
|
b90399d674 | ||
|
|
7c13b5aed9 | ||
|
|
8d87564158 | ||
|
|
1663f13950 | ||
|
|
10aedfbce6 | ||
|
|
d5dd328a05 | ||
|
|
d08bb7ac21 | ||
|
|
6687601074 | ||
|
|
4847a2b1b4 | ||
|
|
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 | ||
|
|
4875265fd3 | ||
|
|
bf651df38f | ||
|
|
77e2d7beff | ||
|
|
9c85563ffd | ||
|
|
e217c6fe23 | ||
|
|
17b6d090db | ||
|
|
4e1e04ef06 | ||
|
|
c81ee20151 | ||
|
|
eb5b447b09 | ||
|
|
857ae5b7fa | ||
|
|
1037fff11f | ||
|
|
f611a1e26f | ||
|
|
f3cfa72051 | ||
|
|
f13a698808 | ||
|
|
18064c269b | ||
|
|
ccb4af4f63 | ||
|
|
0ee30ddeac |
@@ -1,6 +1,6 @@
|
||||
# ffd-retief-hrd fanfictiondownloader
|
||||
application: fanfictiondownloader
|
||||
version: 4-4-84
|
||||
version: 2-0-03
|
||||
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, 00)
|
||||
minimum_calibre_version = (0, 8, 57)
|
||||
version = (2, 0, 3)
|
||||
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
|
||||
|
||||
+66
-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,12 +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))
|
||||
self.gc_dropdowns[site] = dropdown
|
||||
dropdown.addItem(setting,setting)
|
||||
if site == _("Default"):
|
||||
self.gc_dropdowns["Default"] = dropdown
|
||||
if 'Default' in prefs['gc_site_settings']:
|
||||
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)
|
||||
@@ -742,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):
|
||||
@@ -949,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:
|
||||
@@ -990,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)
|
||||
|
||||
|
||||
+43
-41
@@ -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
|
||||
@@ -136,7 +160,8 @@ class RejectUrlEntry:
|
||||
def fullnote(self):
|
||||
retval = ""
|
||||
if self.title and self.auth:
|
||||
retval = retval + _("%s by %s")%(self.title,self.auth)
|
||||
# don't translate--ends up being saved and confuses regex above.
|
||||
retval = retval + "%s by %s"%(self.title,self.auth)
|
||||
if self.note:
|
||||
retval = retval + " - "
|
||||
|
||||
@@ -145,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
|
||||
@@ -195,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):
|
||||
@@ -558,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
|
||||
@@ -828,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'])))
|
||||
@@ -847,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
|
||||
@@ -913,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)
|
||||
@@ -991,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)
|
||||
@@ -1036,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()
|
||||
@@ -1046,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
|
||||
@@ -1088,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)
|
||||
|
||||
+159
-75
@@ -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,16 +13,18 @@ logger = logging.getLogger(__name__)
|
||||
import time, os, copy, threading, re, platform, sys
|
||||
from StringIO import StringIO
|
||||
from functools import partial
|
||||
from datetime import datetime
|
||||
from datetime import datetime, time
|
||||
from string import Template
|
||||
import urllib
|
||||
import email
|
||||
import traceback
|
||||
|
||||
from PyQt4.Qt import (QApplication, QMenu, QToolButton, QTimer)
|
||||
|
||||
from PyQt4.Qt import QPixmap, Qt
|
||||
from PyQt4.QtCore import QBuffer
|
||||
try:
|
||||
from PyQt5.Qt import (QApplication, QMenu, QTimer)
|
||||
from PyQt5.QtCore import QBuffer
|
||||
except ImportError as e:
|
||||
from PyQt4.Qt import (QApplication, QMenu, QTimer)
|
||||
from PyQt4.QtCore import QBuffer
|
||||
|
||||
from calibre.constants import numeric_version as calibre_version
|
||||
|
||||
@@ -32,6 +34,7 @@ from calibre.ebooks.metadata.meta import get_metadata
|
||||
from calibre.gui2 import error_dialog, warning_dialog, question_dialog, info_dialog
|
||||
from calibre.gui2.dialogs.message_box import ViewLog
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
from calibre.utils.config import prefs as calibre_prefs
|
||||
from calibre.utils.date import local_tz
|
||||
from calibre.library.comments import sanitize_comments_html
|
||||
from calibre.constants import config_dir as calibre_config_dir
|
||||
@@ -206,7 +209,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
#print("text/plain:%s"%event.mimeData().data(mimetype))
|
||||
urllist.extend(get_urls_from_text(event.mimeData().data(mimetype)))
|
||||
|
||||
#print("urllist:%s\ndropped_ids:%s"%(urllist,dropped_ids))
|
||||
# print("urllist:%s\ndropped_ids:%s"%(urllist,dropped_ids))
|
||||
if urllist or dropped_ids:
|
||||
QTimer.singleShot(1, partial(self.do_drop,
|
||||
dropped_ids=dropped_ids,
|
||||
@@ -384,6 +387,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
|
||||
def get_urls_from_page_menu(self):
|
||||
|
||||
urltxt = ""
|
||||
if prefs['urlsfromclip']:
|
||||
try:
|
||||
urltxt = self.get_urls_clip(storyurls=False)[0]
|
||||
@@ -660,13 +664,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..."),
|
||||
@@ -711,6 +715,11 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
# No need to do anything with perfs here, but we could.
|
||||
prefs
|
||||
|
||||
def make_id_searchstr(self,url):
|
||||
# older idents can be uri vs url and have | instead of : after
|
||||
# http, plus many sites are now switching to https.
|
||||
return 'identifiers:"~ur(i|l):~^%s$"'%re.sub(r'https?\\\:','https?(\:|\|)',re.escape(url))
|
||||
|
||||
def prep_downloads(self, options, books, merge=False, extrapayload=None):
|
||||
'''Fetch metadata for stories from servers, launch BG job when done.'''
|
||||
|
||||
@@ -718,6 +727,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 +772,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
if not merge: # skip reject list when merging.
|
||||
if rejecturllist.check(url):
|
||||
rejnote = rejecturllist.get_full_note(url)
|
||||
if question_dialog(self.gui, _('Reject URL?'),'''
|
||||
if prefs['reject_always'] or question_dialog(self.gui, _('Reject URL?'),'''
|
||||
<h3>%s</h3>
|
||||
<p>%s</p>
|
||||
<p>"<b>%s</b>"</p>
|
||||
@@ -815,6 +827,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 +865,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
series = story.getMetadata('series')
|
||||
if not merge and series and prefs['checkforseriesurlid']:
|
||||
# try to find *series anthology* by *seriesUrl* identifier url or uri first.
|
||||
searchstr = 'identifiers:"~ur(i|l):=%s"'%story.getMetadata('seriesUrl').replace(":","|")
|
||||
searchstr = self.make_id_searchstr(story.getMetadata('seriesUrl'))
|
||||
identicalbooks = db.search_getting_ids(searchstr, None)
|
||||
# print("searchstr:%s"%searchstr)
|
||||
# print("identicalbooks:%s"%identicalbooks)
|
||||
@@ -873,7 +897,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
|
||||
# all_metadata duplicates some data, but also includes extra_entries, etc.
|
||||
book['all_metadata'] = story.getAllMetadata(removeallentities=True)
|
||||
|
||||
|
||||
book['title'] = story.getMetadata("title", removeallentities=True)
|
||||
book['author_sort'] = book['author'] = story.getList("author", removeallentities=True)
|
||||
book['publisher'] = story.getMetadata("site")
|
||||
@@ -918,18 +942,13 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
logger.debug("from URL(%s)"%url)
|
||||
|
||||
# try to find by identifier url or uri first.
|
||||
searchstr = 'identifiers:"~ur(i|l):=%s"'%url.replace(":","|")
|
||||
searchstr = self.make_id_searchstr(url)
|
||||
identicalbooks = db.search_getting_ids(searchstr, None)
|
||||
# print("searchstr:%s"%searchstr)
|
||||
# print("identicalbooks:%s"%identicalbooks)
|
||||
if len(identicalbooks) < 1:
|
||||
# find dups
|
||||
authlist = story.getList("author", removeallentities=True)
|
||||
if len(authlist) > 100 and calibre_version < (0, 8, 61):
|
||||
## should be fixed from 0.8.61 on. In the
|
||||
## meantime, if it matches the title *and* first
|
||||
## 100 authors, I'm prepared to assume it's a
|
||||
## match.
|
||||
logger.debug("reduce author list to 100 only when calibre < 0.8.61")
|
||||
authlist = authlist[:100]
|
||||
mi = MetaInformation(story.getMetadata("title", removeallentities=True),
|
||||
authlist)
|
||||
identicalbooks = db.find_identical_books(mi)
|
||||
@@ -943,7 +962,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
raise NotGoingToDownload(_("Skipping duplicate story."),"list_remove.png")
|
||||
|
||||
if len(identicalbooks) > 1:
|
||||
raise NotGoingToDownload("More than one identical book by Identifer URL or title/author(s)--can't tell which book to update/overwrite.","minusminus.png")
|
||||
raise NotGoingToDownload(_("More than one identical book by Identifer URL or title/author(s)--can't tell which book to update/overwrite."),"minusminus.png")
|
||||
|
||||
## changed: add new book when CALIBREONLY if none found.
|
||||
if collision == CALIBREONLY and not identicalbooks:
|
||||
@@ -959,7 +978,8 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
if book_id and mi: # book_id and mi only set if matched by title/author.
|
||||
liburl = self.get_story_url(db,book_id)
|
||||
if book['url'] != liburl and prefs['checkforurlchange'] and \
|
||||
not (book['url'].replace('https','http') == liburl and 'fanfiction.net' in liburl):
|
||||
not (book['url'].replace('https','http') == liburl): # several sites have been changing to
|
||||
# https now. Don't flag when that's the only change.
|
||||
# special case for ffnet urls change to https.
|
||||
if not question_dialog(self.gui, _('Change Story URL?'),'''
|
||||
<h3>%s</h3>
|
||||
@@ -1014,7 +1034,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
(epuburl,chaptercount) = \
|
||||
get_dcsource_chaptercount(StringIO(db.format(book_id,'EPUB',
|
||||
index_is_id=True)))
|
||||
urlchaptercount = int(story.getMetadata('numChapters'))
|
||||
urlchaptercount = int(story.getMetadata('numChapters').replace(',',''))
|
||||
if chaptercount == urlchaptercount:
|
||||
if collision == UPDATE:
|
||||
raise NotGoingToDownload(_("Already contains %d chapters.")%chaptercount,'edit-undo.png')
|
||||
@@ -1029,10 +1049,16 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
if collision == OVERWRITE and \
|
||||
db.has_format(book_id,formmapping[fileform],index_is_id=True):
|
||||
# check make sure incoming is newer.
|
||||
lastupdated=story.getMetadataRaw('dateUpdated').date()
|
||||
fileupdated=datetime.fromtimestamp(os.stat(db.format_abspath(book_id, formmapping[fileform], index_is_id=True))[8]).date()
|
||||
if fileupdated > lastupdated:
|
||||
lastupdated=story.getMetadataRaw('dateUpdated')
|
||||
fileupdated=datetime.fromtimestamp(os.stat(db.format_abspath(book_id, formmapping[fileform], index_is_id=True))[8])
|
||||
|
||||
# updated doesn't have time (or is midnight), use dates only.
|
||||
# updated does have time, use full timestamps.
|
||||
if (lastupdated.time() == time.min and fileupdated.date() > lastupdated.date()) or \
|
||||
(lastupdated.time() != time.min and fileupdated > lastupdated):
|
||||
raise NotGoingToDownload(_("Not Overwriting, web site is not newer."),'edit-undo.png')
|
||||
|
||||
|
||||
|
||||
# For update, provide a tmp file copy of the existing epub so
|
||||
# it can't change underneath us. Now also overwrite for logpage preserve.
|
||||
@@ -1130,6 +1156,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,
|
||||
@@ -1224,6 +1251,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')
|
||||
@@ -1327,7 +1358,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:
|
||||
@@ -1385,7 +1415,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),
|
||||
@@ -1394,12 +1427,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
|
||||
@@ -1428,8 +1459,18 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
fmts = db.formats(book['calibre_id'], index_is_id=True).split(',')
|
||||
for fmt in fmts:
|
||||
if fmt != formmapping[options['fileform']]:
|
||||
logger.debug("remove f:"+fmt)
|
||||
logger.debug("deleteotherforms remove f:"+fmt)
|
||||
db.remove_format(book['calibre_id'], fmt, index_is_id=True)#, notify=False
|
||||
elif prefs['autoconvert']:
|
||||
## 'Convert Book'.auto_convert_auto_add doesn't convert if
|
||||
## the format is already there.
|
||||
fmt = calibre_prefs['output_format']
|
||||
# delete if there, but not if the format we just made.
|
||||
if fmt != formmapping[options['fileform']] and \
|
||||
db.has_format(book_id,fmt,index_is_id=True):
|
||||
logger.debug("autoconvert remove f:"+fmt)
|
||||
db.remove_format(book['calibre_id'], fmt, index_is_id=True)#, notify=False
|
||||
|
||||
|
||||
return book_id
|
||||
|
||||
@@ -1537,8 +1578,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
|
||||
@@ -1550,12 +1598,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':
|
||||
@@ -1569,8 +1624,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)
|
||||
|
||||
@@ -1579,28 +1634,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']))
|
||||
|
||||
# force a refresh if generating cover so complex composite
|
||||
# custom columns are current and correct
|
||||
@@ -1632,15 +1682,39 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
|
||||
if not setting_name and book['all_metadata']['site'] in prefs['gc_site_settings']:
|
||||
setting_name = prefs['gc_site_settings'][book['all_metadata']['site']]
|
||||
logger.debug("Generate Cover Setting from site(%s)"%setting_name)
|
||||
|
||||
if not setting_name and 'Default' in prefs['gc_site_settings']:
|
||||
setting_name = prefs['gc_site_settings']['Default']
|
||||
logger.debug("Generate Cover Setting from Default(%s)"%setting_name)
|
||||
|
||||
if setting_name:
|
||||
logger.debug("Running Generate Cover with settings %s."%setting_name)
|
||||
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 []
|
||||
@@ -1729,7 +1803,9 @@ 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
|
||||
book['pubdate'] = None
|
||||
return book
|
||||
|
||||
def convert_urls_to_books(self, urls):
|
||||
@@ -1805,13 +1881,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:
|
||||
@@ -1893,9 +1971,9 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
|
||||
# fill from first of each if not already present:
|
||||
for k in ('pubdate', 'timestamp', 'updatedate'):
|
||||
if k not in b: # not in this book? Skip it.
|
||||
if k not in b or not b[k]: # not in this book? Skip it.
|
||||
continue
|
||||
if k not in book: # first is good enough for publisher.
|
||||
if k not in book or not book[k]: # first is good enough for publisher.
|
||||
book[k]=b[k]
|
||||
|
||||
# Do these even on first to get the all_metadata settings.
|
||||
@@ -1916,11 +1994,12 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
for (k,v) in b['all_metadata'].iteritems():
|
||||
#print("merge_meta_books v:%s k:%s"%(v,k))
|
||||
if k in ('numChapters','numWords'):
|
||||
if k not in book['all_metadata']:
|
||||
book['all_metadata'][k] = b['all_metadata'][k]
|
||||
else:
|
||||
# lot of work for a simple add.
|
||||
book['all_metadata'][k] = unicode(int(book['all_metadata'][k].replace(',',''))+int(b['all_metadata'][k].replace(',','')))
|
||||
if k in b['all_metadata'] and b['all_metadata'][k]:
|
||||
if k not in book['all_metadata']:
|
||||
book['all_metadata'][k] = b['all_metadata'][k]
|
||||
else:
|
||||
# lot of work for a simple add.
|
||||
book['all_metadata'][k] = unicode(int(book['all_metadata'][k].replace(',',''))+int(b['all_metadata'][k].replace(',','')))
|
||||
elif k in ('dateUpdated','datePublished','dateCreated',
|
||||
'series','status','title'):
|
||||
pass # handled above, below or skip these for now, not going to do anything with them.
|
||||
@@ -1935,14 +2014,20 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
# cust cols can convert back to numbers and
|
||||
# add.
|
||||
book['anthology_meta_list'][k]=True
|
||||
|
||||
|
||||
print("book['url']:%s"%book['url'])
|
||||
configuration = get_ffdl_config(book['url'],fileform)
|
||||
if existingbook:
|
||||
book['title'] = deftitle = existingbook['title']
|
||||
book['comments'] = existingbook['comments']
|
||||
else:
|
||||
book['title'] = deftitle = book_list[0]['title']
|
||||
book['comments'] = _("Anthology containing:")+"\n" + \
|
||||
"\n".join([ _("%s by %s")%(b['title'],', '.join(b['author'])) for b in book_list ])
|
||||
if len(book['author']) > 1:
|
||||
book['comments'] = _("Anthology containing:")+"\n" + \
|
||||
"\n".join([ _("%s by %s")%(b['title'],', '.join(b['author'])) for b in book_list ])
|
||||
else:
|
||||
book['comments'] = _("Anthology containing:")+"\n" + \
|
||||
"\n".join([ b['title'] for b in book_list ])
|
||||
# book['all_metadata']['description']
|
||||
|
||||
# if all same series, use series for name. But only if all and not previous named
|
||||
@@ -1954,7 +2039,6 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
book['title'] = deftitle
|
||||
break
|
||||
|
||||
configuration = get_ffdl_config(book['url'],fileform)
|
||||
logger.debug("anthology_title_pattern:%s"%configuration.getConfig('anthology_title_pattern'))
|
||||
if configuration.getConfig('anthology_title_pattern'):
|
||||
tmplt = Template(configuration.getConfig('anthology_title_pattern'))
|
||||
@@ -1970,7 +2054,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
|
||||
for v in ['Completed','In-Progress']:
|
||||
if v in book['tags']:
|
||||
book['tags'].remove(v)
|
||||
book['tags'].append('Anthology')
|
||||
book['tags'].extend(configuration.getConfigList('anthology_tags'))
|
||||
book['all_metadata']['anthology'] = "true"
|
||||
|
||||
return book
|
||||
|
||||
+8
-11
@@ -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()
|
||||
@@ -182,7 +179,7 @@ def do_download_for_worker(book,options,notification=lambda x,y:x):
|
||||
|
||||
# update now handled by pre-populating the old images and
|
||||
# chapters in the adapter rather than merging epubs.
|
||||
urlchaptercount = int(story.getMetadata('numChapters'))
|
||||
urlchaptercount = int(story.getMetadata('numChapters').replace(',',''))
|
||||
(url,
|
||||
chaptercount,
|
||||
adapter.oldchapters,
|
||||
|
||||
@@ -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'] = []
|
||||
|
||||
|
||||
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
+420
-374
File diff suppressed because it is too large
Load Diff
+474
-205
File diff suppressed because it is too large
Load Diff
+20
-21
@@ -23,6 +23,7 @@ import getpass
|
||||
import string
|
||||
import ConfigParser
|
||||
from subprocess import call
|
||||
import pprint
|
||||
|
||||
import logging
|
||||
if sys.version_info >= (2, 7):
|
||||
@@ -226,9 +227,9 @@ def main(argv,
|
||||
except:
|
||||
options.update = False
|
||||
pass
|
||||
|
||||
## Check for include_images and absence of PIL, give warning.
|
||||
if adapter.getConfig('include_images'):
|
||||
|
||||
## Check for include_images without no_image_processing. In absence of PIL, give warning.
|
||||
if adapter.getConfig('include_images') and not adapter.getConfig('no_image_processing'):
|
||||
try:
|
||||
from calibre.utils.magick import Image
|
||||
logging.debug("Using calibre.utils.magick")
|
||||
@@ -270,31 +271,29 @@ def main(argv,
|
||||
elif chaptercount == 0:
|
||||
print "%s doesn't contain any recognizable chapters, probably from a different source. Not updating." % (output_filename)
|
||||
else:
|
||||
if not options.metaonly:
|
||||
# update now handled by pre-populating the old
|
||||
# images and chapters in the adapter rather than
|
||||
# merging epubs.
|
||||
(url,
|
||||
chaptercount,
|
||||
adapter.oldchapters,
|
||||
adapter.oldimgs,
|
||||
adapter.oldcover,
|
||||
adapter.calibrebookmark,
|
||||
adapter.logfile) = get_update_data(output_filename)
|
||||
|
||||
# update now handled by pre-populating the old
|
||||
# images and chapters in the adapter rather than
|
||||
# merging epubs.
|
||||
(url,
|
||||
chaptercount,
|
||||
adapter.oldchapters,
|
||||
adapter.oldimgs,
|
||||
adapter.oldcover,
|
||||
adapter.calibrebookmark,
|
||||
adapter.logfile) = get_update_data(output_filename)
|
||||
print "Do update - epub(%d) vs url(%d)" % (chaptercount, urlchaptercount)
|
||||
|
||||
print "Do update - epub(%d) vs url(%d)" % (chaptercount, urlchaptercount)
|
||||
if not (options.update and chaptercount == urlchaptercount) \
|
||||
and adapter.getConfig("do_update_hook"):
|
||||
chaptercount = adapter.hookForUpdates(chaptercount)
|
||||
|
||||
if not (options.update and chaptercount == urlchaptercount) \
|
||||
and adapter.getConfig("do_update_hook"):
|
||||
chaptercount = adapter.hookForUpdates(chaptercount)
|
||||
|
||||
writeStory(configuration,adapter,"epub")
|
||||
writeStory(configuration,adapter,"epub")
|
||||
|
||||
else:
|
||||
# regular download
|
||||
if options.metaonly:
|
||||
print adapter.getStoryMetadataOnly()
|
||||
pprint.pprint(adapter.getStoryMetadataOnly().getAllMetadata())
|
||||
|
||||
output_filename=writeStory(configuration,adapter,options.format,options.metaonly)
|
||||
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,57 @@
|
||||
# coding: utf-8
|
||||
|
||||
import re
|
||||
import codecs
|
||||
|
||||
stack = []
|
||||
|
||||
def get_end_tag(tag):
|
||||
if len(tag) > 0 and tag.find(u'<') > -1 and tag.rfind(u'>') > -1:
|
||||
return re.sub(r'.*<([^\ >]+).*', r'</\1>', tag)
|
||||
return u''
|
||||
|
||||
def get_tag_name(tag):
|
||||
if len(tag) > 0 and tag.find(u'<') > -1 and tag.rfind(u'>') > -1:
|
||||
return re.sub(r'</*([^\ >]+).*', r'\1', tag)
|
||||
return u''
|
||||
|
||||
def push(tag):
|
||||
if len(tag) > 0 and tag.find(u'<') > -1 and tag.rfind(u'>') > -1:
|
||||
stack.append(tag)
|
||||
|
||||
def pop():
|
||||
if len(stack) > 0:
|
||||
return stack.pop()
|
||||
return u''
|
||||
|
||||
def pop_end_tag():
|
||||
return unicode(get_end_tag(pop()))
|
||||
|
||||
def spool_end():
|
||||
html = u''
|
||||
for tag in reversed(stack):
|
||||
html += get_end_tag(tag)
|
||||
return html
|
||||
|
||||
def spool_start():
|
||||
html = u''
|
||||
for item in stack:
|
||||
html += item
|
||||
return html
|
||||
|
||||
def has_elements():
|
||||
return len(stack) > 0
|
||||
|
||||
def get_last():
|
||||
# t = pop()
|
||||
# push(t)
|
||||
# return t
|
||||
if len(stack) > 0:
|
||||
return stack[len(stack)-1]
|
||||
return u''
|
||||
|
||||
def flush():
|
||||
del stack[:]
|
||||
|
||||
def get_stack():
|
||||
return stack
|
||||
@@ -121,11 +121,23 @@ import adapter_simplyundeniablecom
|
||||
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
|
||||
import adapter_bdsmgeschichten
|
||||
import adapter_tolkienfanfiction
|
||||
import adapter_themaplebookshelf
|
||||
|
||||
## This bit of complexity allows adapters to be added by just adding
|
||||
## importing. It eliminates the long if/else clauses we used to need
|
||||
## to pick out the adapter.
|
||||
|
||||
|
||||
## List of registered site adapters.
|
||||
__class_list = []
|
||||
__domain_map = {}
|
||||
@@ -144,6 +156,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
|
||||
@@ -151,10 +170,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
|
||||
@@ -185,18 +205,20 @@ def getConfigSectionFor(url):
|
||||
(cls,fixedurl) = getClassFor(url)
|
||||
if cls:
|
||||
return cls.getConfigSection()
|
||||
|
||||
|
||||
# No adapter found.
|
||||
raise exceptions.UnknownSite( url, [cls.getSiteDomain() for cls in __class_list] )
|
||||
|
||||
def getClassFor(url):
|
||||
## fix up leading protocol.
|
||||
fixedurl = re.sub(r"(?i)^[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 ):
|
||||
@@ -207,17 +229,17 @@ def getClassFor(url):
|
||||
domain = domain.replace("www.","")
|
||||
#logger.debug("trying site:without www: "+domain)
|
||||
cls = getClassFromList(domain)
|
||||
fixedurl = fixedurl.replace("http://www.","http://")
|
||||
fixedurl = re.sub(r"^http(s?)://www\.",r"http\1://",fixedurl)
|
||||
if not cls:
|
||||
#logger.debug("trying site:www."+domain)
|
||||
cls = getClassFromList("www."+domain)
|
||||
fixedurl = fixedurl.replace("http://","http://www.")
|
||||
fixedurl = re.sub(r"^http(s?)://",r"http\1://www.",fixedurl)
|
||||
|
||||
if cls:
|
||||
fixedurl = cls.stripURLParameters(fixedurl)
|
||||
|
||||
|
||||
return (cls,fixedurl)
|
||||
|
||||
|
||||
def getClassFromList(domain):
|
||||
try:
|
||||
return __domain_map[domain]
|
||||
|
||||
@@ -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)
|
||||
@@ -162,34 +165,43 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
## Title
|
||||
a = soup.find('a', href=re.compile(r"^/works/\d+$"))
|
||||
a = soup.find('a', href=re.compile(r"/works/\d+$"))
|
||||
self.story.setMetadata('title',stripHTML(a))
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
alist = soup.findAll('a', href=re.compile(r"^/users/\w+/pseuds/\w+"))
|
||||
alist = soup.findAll('a', href=re.compile(r"/users/\w+/pseuds/\w+"))
|
||||
if len(alist) < 1: # ao3 allows for author 'Anonymous' with no author link.
|
||||
self.story.setMetadata('author','Anonymous')
|
||||
self.story.setMetadata('authorUrl','http://archiveofourown.org/')
|
||||
self.story.setMetadata('authorId','0')
|
||||
else:
|
||||
for a in alist:
|
||||
self.story.addToList('authorId',a['href'].split('/')[2])
|
||||
self.story.addToList('authorUrl','http://'+self.host+a['href'])
|
||||
self.story.addToList('authorId',a['href'].split('/')[-1])
|
||||
self.story.addToList('authorUrl',a['href'])
|
||||
self.story.addToList('author',a.text)
|
||||
|
||||
newestChapter = None
|
||||
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,346 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2014 Fanficdownloader team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
import urlparse
|
||||
import time
|
||||
|
||||
from .. import BeautifulSoup as bs
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
def _translate_date_german_english(date):
|
||||
fullmon = {"Januar":"01",
|
||||
"Februar":"02",
|
||||
u"März":"03",
|
||||
"April":"04",
|
||||
"Mai":"05",
|
||||
"Juni":"06",
|
||||
"Juli":"07",
|
||||
"August":"08",
|
||||
"September":"09",
|
||||
"Oktober":"10",
|
||||
"November":"11",
|
||||
"Dezember":"12"}
|
||||
for (name,num) in fullmon.items():
|
||||
date = date.replace(name,num)
|
||||
return date
|
||||
|
||||
_REGEX_TRAILING_DIGIT = re.compile("(\d+)$")
|
||||
_REGEX_DASH_TO_END = re.compile("-[^-]+$")
|
||||
_REGEX_CHAPTER_TITLE = re.compile(ur"""
|
||||
\s*
|
||||
[\u2013-]?
|
||||
\s*
|
||||
([\dIVX-]+)?
|
||||
\.?
|
||||
\s*
|
||||
[\[\(]?
|
||||
\s*
|
||||
(Teil|Kapitel|Tag)?
|
||||
\s*
|
||||
([\dIVX-]+)?
|
||||
\s*
|
||||
[\]\)]?
|
||||
\s*
|
||||
$
|
||||
""", re.VERBOSE)
|
||||
_INITIAL_STEP = 5
|
||||
|
||||
class BdsmGeschichtenAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.decode = ["utf8", "Windows-1252"]
|
||||
|
||||
self.story.setMetadata('siteabbrev','bdsmgesch')
|
||||
|
||||
# Replace possible chapter numbering
|
||||
chapterMatch = _REGEX_TRAILING_DIGIT.search(url)
|
||||
if chapterMatch is None:
|
||||
self.maxChapter = 1
|
||||
else:
|
||||
self.maxChapter = int(chapterMatch.group(1))
|
||||
# url = re.sub(_REGEX_TRAILING_DIGIT, "1", url)
|
||||
|
||||
# set storyId
|
||||
self.story.setMetadata('storyId', re.compile(self.getSiteURLPattern()).match(url).group('storyId'))
|
||||
|
||||
# normalize URL
|
||||
self._setURL('http://%s/%s' % (self.getSiteDomain(), self.story.getMetadata('storyId')))
|
||||
|
||||
self.dateformat = '%d. %m %Y - %H:%M'
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return 'bdsm-geschichten.net'
|
||||
|
||||
@classmethod
|
||||
def getAcceptDomains(cls):
|
||||
return ['www.bdsm-geschichten.net', 'www.bdsm-geschichten.net']
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://www.bdsm-geschichten.net/title-of-story-1 http://bdsm-geschichten.net/title-of-story-1"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return r"http://(www\.)?bdsm-geschichten.net/(?P<storyId>[a-zA-Z0-9_-]+)"
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
if not (self.is_adult or self.getConfig("is_adult")):
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
try:
|
||||
data1 = self._fetchUrl(self.url)
|
||||
soup = bs.BeautifulSoup(data1)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
#strip comments from soup
|
||||
[comment.extract() for comment in soup.findAll(text=lambda text:isinstance(text, bs.Comment))]
|
||||
|
||||
# Cache the soups so we won't have to redownload in getChapterText later
|
||||
self.soupsCache = {}
|
||||
self.soupsCache[self.url] = soup
|
||||
|
||||
# author
|
||||
authorDiv = soup.find("div", "author-pane-line author-name")
|
||||
authorId = authorDiv.string.strip()
|
||||
self.story.setMetadata('authorId', authorId)
|
||||
self.story.setMetadata('author', authorId)
|
||||
# TODO not really true need to be loggedin for this to work or fetch userid
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/'+authorId)
|
||||
|
||||
# TODO better metadata
|
||||
date = soup.find("div", {"class": "submitted"}).string.strip()
|
||||
date = re.sub(" —.*", "", date)
|
||||
date = _translate_date_german_english(date)
|
||||
self.story.setMetadata('datePublished', makeDate(date, self.dateformat))
|
||||
title1 = soup.find("h1", {'class': 'title'}).string
|
||||
|
||||
|
||||
for tagLink in soup.find("ul", "taxonomy").findAll("a"):
|
||||
self.story.addToList('category', tagLink.string)
|
||||
|
||||
## Retrieve chapter soups
|
||||
if self.getConfig('find_chapters') == 'guess':
|
||||
self.chapterUrls = []
|
||||
self._find_chapters_by_guessing(title1)
|
||||
else:
|
||||
self._find_chapters_by_parsing(soup)
|
||||
|
||||
firstChapterUrl = self.chapterUrls[0][1]
|
||||
if firstChapterUrl in self.soupsCache:
|
||||
firstChapterSoup = self.soupsCache[firstChapterUrl]
|
||||
h1 = firstChapterSoup.find("h1").text
|
||||
else:
|
||||
h1 = soup.find("h1").text
|
||||
|
||||
h1 = re.sub(_REGEX_CHAPTER_TITLE, "", h1)
|
||||
self.story.setMetadata('title', h1)
|
||||
self.story.setMetadata('numChapters', len(self.chapterUrls))
|
||||
return
|
||||
|
||||
def _find_chapters_by_parsing(self, soup):
|
||||
|
||||
# store original soup
|
||||
origSoup = soup
|
||||
|
||||
#
|
||||
# find first chapter
|
||||
#
|
||||
firstLink = None
|
||||
firstLinkDiv = soup.find("div", "field-field-erster-teil")
|
||||
if firstLinkDiv is not None:
|
||||
firstLink = "http://%s%s" % (self.getSiteDomain(), firstLinkDiv.findNext("a")['href'])
|
||||
logger.debug("Found first chapter right away <%s>" % firstLink)
|
||||
try:
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(firstLink))
|
||||
self.soupsCache[firstLink] = soup
|
||||
self.chapterUrls.insert(0, (soup.find("h1").text, firstLink))
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise exceptions.StoryDoesNotExist(firstLink)
|
||||
else:
|
||||
logger.debug("DIDN'T find first chapter right away")
|
||||
# parse previous Link until first
|
||||
while True:
|
||||
prevLink = None
|
||||
prevLinkDiv = soup.find("div", "field-field-vorheriger-teil")
|
||||
if prevLinkDiv is not None:
|
||||
prevLink = prevLinkDiv.find("a")
|
||||
if prevLink is None:
|
||||
prevLink = soup.find("a", text=re.compile("<<<")) # <<<
|
||||
if prevLink is None:
|
||||
logger.debug("Couldn't find prev part")
|
||||
break
|
||||
else:
|
||||
logger.debug("Previous Chapter <%s>" % prevLink)
|
||||
if type(prevLink) != bs.Tag or prevLink.name != "a":
|
||||
prevLink = prevLink.findParent("a")
|
||||
if prevLink is None or '#' in prevLink['href']:
|
||||
logger.debug("Couldn't find prev part (false positive) <%s>" % prevLink)
|
||||
break
|
||||
prevLink = prevLink['href']
|
||||
try:
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(prevLink))
|
||||
self.soupsCache[prevLink] = soup
|
||||
prevTtitle = soup.find("h1", {'class': 'title'}).string
|
||||
self.chapterUrls.insert(0, (prevTtitle, prevLink))
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(nextLink)
|
||||
else:
|
||||
raise e
|
||||
firstLink = prevLink
|
||||
|
||||
# if first chapter couldn't be determined, assume the URL originally
|
||||
# passed is the first chapter
|
||||
if firstLink is None:
|
||||
logger.debug("Couldn't set first chapter")
|
||||
firstLink = self.url
|
||||
self.chapterUrls.insert(0, (soup.find("h1").text, firstLink))
|
||||
|
||||
# set first URL
|
||||
logger.debug("Set first link: %s" % firstLink)
|
||||
self._setURL(firstLink)
|
||||
self.story.setMetadata('storyId', re.compile(self.getSiteURLPattern()).match(firstLink).group('storyId'))
|
||||
|
||||
#
|
||||
# Parse next chapters
|
||||
#
|
||||
while True:
|
||||
nextLink = None
|
||||
nextLinkDiv = soup.find("div", "field-field-naechster-teil")
|
||||
if nextLinkDiv is not None:
|
||||
nextLink = nextLinkDiv.find("a")
|
||||
if nextLink is None:
|
||||
nextLink = soup.find("a", text=re.compile(">>>"))
|
||||
if nextLink is None:
|
||||
nextLink = soup.find("a", text=re.compile("Fortsetzung"))
|
||||
|
||||
if nextLink is None:
|
||||
logger.debug("Couldn't find next part")
|
||||
break
|
||||
else:
|
||||
if type(nextLink) != bs.Tag or nextLink.name != "a":
|
||||
nextLink = nextLink.findParent("a")
|
||||
if nextLink is None or '#' in nextLink['href']:
|
||||
logger.debug("Couldn't find next part (false positive) <%s>" % nextLink)
|
||||
break
|
||||
nextLink = nextLink['href']
|
||||
|
||||
if not nextLink.startswith('http:'):
|
||||
nextLink = 'http://' + self.getSiteDomain() + nextLink
|
||||
|
||||
for loadedChapter in self.chapterUrls:
|
||||
if loadedChapter[0] == nextLink:
|
||||
logger.debug("ERROR: Repeating chapter <%s> Try to fix it" % nextLink)
|
||||
nextLinkMatch = _REGEX_TRAILING_DIGIT.match(nextLink)
|
||||
if nextLinkMatch is not None:
|
||||
curChap = nextLinkMatch.group(1)
|
||||
nextLink = re.sub(_REGEX_TRAILING_DIGIT, str(int(curChap) + 1), nextLink)
|
||||
else:
|
||||
break
|
||||
try:
|
||||
data = self._fetchUrl(nextLink)
|
||||
soup = bs.BeautifulSoup(data)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(nextLink)
|
||||
else:
|
||||
raise e
|
||||
title2 = soup.find("h1", {'class': 'title'}).string
|
||||
self.chapterUrls.append((title2, nextLink))
|
||||
logger.debug("Grabbing next chapter URL " + nextLink)
|
||||
self.soupsCache[nextLink] = soup
|
||||
# [comment.extract() for comment in soup.findAll(text=lambda text:isinstance(text, bs.Comment))]
|
||||
logger.debug("Chapters: %s" % self.chapterUrls)
|
||||
|
||||
|
||||
def _find_chapters_by_guessing(self, title1):
|
||||
step = _INITIAL_STEP
|
||||
curMax = self.maxChapter + step
|
||||
lastHit = True
|
||||
while True:
|
||||
nextChapterUrl = re.sub(_REGEX_TRAILING_DIGIT, str(curMax), self.url)
|
||||
if nextChapterUrl == self.url:
|
||||
logger.debug("Unable to guess next chapter because URL doesn't end in numbers")
|
||||
break;
|
||||
try:
|
||||
logger.debug("Trying chapter URL " + nextChapterUrl)
|
||||
data = self._fetchUrl(nextChapterUrl)
|
||||
hit = True
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
hit = False
|
||||
else:
|
||||
raise e
|
||||
if hit:
|
||||
logger.debug("Found chapter URL " + nextChapterUrl)
|
||||
self.maxChapter = curMax
|
||||
self.soupsCache[nextChapterUrl] = bs.BeautifulSoup(data)
|
||||
if not lastHit:
|
||||
break
|
||||
lastHit = curMax
|
||||
curMax += step
|
||||
else:
|
||||
lastHit = False
|
||||
curMax -= 1
|
||||
logger.debug(curMax)
|
||||
|
||||
for i in xrange(1, self.maxChapter):
|
||||
nextChapterUrl = re.sub(_REGEX_TRAILING_DIGIT, str(i), self.url)
|
||||
nextChapterTitle = re.sub("1", str(i), title1)
|
||||
self.chapterUrls.append((nextChapterTitle, nextChapterUrl))
|
||||
|
||||
def getChapterText(self, url):
|
||||
|
||||
if url in self.soupsCache:
|
||||
logger.debug('Getting chapter <%s> from cache' % url)
|
||||
soup = self.soupsCache[url]
|
||||
else:
|
||||
logger.debug('Downloading chapter <%s>' % url)
|
||||
data1 = self._fetchUrl(url)
|
||||
soup = bs.BeautifulSoup(data1)
|
||||
#strip comments from soup
|
||||
[comment.extract() for comment in soup.findAll(text=lambda text:isinstance(text, bs.Comment))]
|
||||
|
||||
# get story text
|
||||
storyDiv1 = bs.Tag(soup, "div")
|
||||
for para in soup.find("div", "full-node").find('div', 'content').findAll("p"):
|
||||
storyDiv1.append(para)
|
||||
storyDiv1.append('<br />')
|
||||
storytext = self.utf8FromSoup(url,storyDiv1)
|
||||
|
||||
return storytext
|
||||
|
||||
|
||||
def getClass():
|
||||
return BdsmGeschichtenAdapter
|
||||
@@ -0,0 +1,193 @@
|
||||
from datetime import timedelta
|
||||
import re
|
||||
import urllib2
|
||||
import urlparse
|
||||
|
||||
from .. import BeautifulSoup
|
||||
from ..htmlcleanup import stripHTML
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
from .. import exceptions
|
||||
|
||||
|
||||
def getClass():
|
||||
return BloodshedverseComAdapter
|
||||
|
||||
|
||||
def _get_query_data(url):
|
||||
components = urlparse.urlparse(url)
|
||||
query_data = urlparse.parse_qs(components.query)
|
||||
return dict((key, data[0]) for key, data in query_data.items())
|
||||
|
||||
|
||||
class BloodshedverseComAdapter(BaseSiteAdapter):
|
||||
SITE_ABBREVIATION = 'bvc'
|
||||
SITE_DOMAIN = 'bloodshedverse.com'
|
||||
|
||||
BASE_URL = 'http://' + SITE_DOMAIN + '/'
|
||||
READ_URL_TEMPLATE = BASE_URL + 'stories.php?go=read&no=%s'
|
||||
|
||||
STARTED_DATETIME_FORMAT = '%m/%d/%Y'
|
||||
UPDATED_DATETIME_FORMAT = '%m/%d/%Y %I:%M'
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
query_data = urlparse.parse_qs(self.parsedUrl.query)
|
||||
story_no = query_data['no'][0]
|
||||
|
||||
self.story.setMetadata('storyId', story_no)
|
||||
self._setURL(self.READ_URL_TEMPLATE % story_no)
|
||||
self.story.setMetadata('siteabbrev', self.SITE_ABBREVIATION)
|
||||
|
||||
def _customized_fetch_url(self, url, exception=None, parameters=None):
|
||||
if exception:
|
||||
try:
|
||||
data = self._fetchUrl(url, parameters)
|
||||
except urllib2.HTTPError:
|
||||
raise exception(self.url)
|
||||
# Just let self._fetchUrl throw the exception, don't catch and
|
||||
# customize it.
|
||||
else:
|
||||
data = self._fetchUrl(url, parameters)
|
||||
|
||||
return BeautifulSoup.BeautifulSoup(data)
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return BloodshedverseComAdapter.SITE_DOMAIN
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return cls.READ_URL_TEMPLATE % 1234
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape(self.BASE_URL + 'stories.php?go=') + r'(read|chapters)\&no=\d+$'
|
||||
|
||||
# Override stripURLParameters so the "no" parameter won't get stripped
|
||||
@classmethod
|
||||
def stripURLParameters(cls, url):
|
||||
return url
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
soup = self._customized_fetch_url(self.url)
|
||||
|
||||
# Since no 404 error code we have to raise the exception ourselves.
|
||||
# A title that is just 'by' indicates that there is no author name
|
||||
# and no story title available.
|
||||
if stripHTML(soup.title) == 'by':
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
|
||||
for option in soup.find('select', {'name': 'chapter'}):
|
||||
title = stripHTML(option)
|
||||
url = self.READ_URL_TEMPLATE % option['value']
|
||||
self.chapterUrls.append((title, url))
|
||||
|
||||
# Get the URL to the author's page and find the correct story entry to
|
||||
# scrape the metadata
|
||||
author_url = urlparse.urljoin(self.url, soup.find('a', {'class': 'headline'})['href'])
|
||||
soup = self._customized_fetch_url(author_url)
|
||||
|
||||
story_no = self.story.getMetadata('storyId')
|
||||
# Ignore first list_box div, it only contains the author information
|
||||
for list_box in soup('div', {'class': 'list_box'})[1:]:
|
||||
url = list_box.find('a', {'class': 'fictitle'})['href']
|
||||
query_data = _get_query_data(url)
|
||||
|
||||
# Found the div containing the story's metadata; break the loop and
|
||||
# parse the element
|
||||
if query_data['no'] == story_no:
|
||||
break
|
||||
else:
|
||||
raise exceptions.FailedToDownload(self.url)
|
||||
|
||||
title_anchor = list_box.find('a', {'class': 'fictitle'})
|
||||
self.story.setMetadata('title', stripHTML(title_anchor))
|
||||
|
||||
author_anchor = title_anchor.findNextSibling('a')
|
||||
self.story.setMetadata('author', stripHTML(author_anchor))
|
||||
self.story.setMetadata('authorId', _get_query_data(author_anchor['href'])['who'])
|
||||
self.story.setMetadata('authorUrl', urlparse.urljoin(self.url, author_anchor['href']))
|
||||
|
||||
list_review = list_box.find('div', {'class': 'list_review'})
|
||||
reviews = stripHTML(list_review.a).split(' ', 1)[0]
|
||||
self.story.setMetadata('reviews', reviews)
|
||||
|
||||
summary_div = list_box.find('div', {'class': 'list_summary'})
|
||||
if not self.getConfig('keep_summary_html'):
|
||||
summary = ''.join(summary_div(text=True))
|
||||
else:
|
||||
summary = self.utf8FromSoup(author_url, summary_div)
|
||||
|
||||
self.story.setMetadata('description', summary)
|
||||
|
||||
# I'm assuming this to be the category, not sure what else it could be
|
||||
first_listinfo = list_box.find('div', {'class': 'list_info'})
|
||||
self.story.addToList('category', stripHTML(first_listinfo.a))
|
||||
|
||||
for list_info in first_listinfo.findNextSiblings('div', {'class': 'list_info'}):
|
||||
for b_tag in list_info('b'):
|
||||
key = b_tag.string.strip(': ')
|
||||
# Strip colons from the beginning, superfluous spaces and minus
|
||||
# characters from the end, and possibly trailing commas from
|
||||
# the warnings if only one is present
|
||||
value = b_tag.nextSibling.string.strip(': -,')
|
||||
|
||||
if key == 'Genre':
|
||||
for genre in value.split(', '):
|
||||
# Ignore the "none" genre
|
||||
if not genre == 'none':
|
||||
self.story.addToList('genre', genre)
|
||||
|
||||
elif key == 'Rating':
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
elif key == 'Complete':
|
||||
self.story.setMetadata('status', 'Completed' if value == 'Yes' else 'In-Progress')
|
||||
|
||||
elif key == 'Warning':
|
||||
for warning in value.split(', '):
|
||||
# The string here starts with ", " before the actual list
|
||||
# of values sometimes, so check for an empty warning
|
||||
# and ignore the "none" warning.
|
||||
if not warning or warning == 'none':
|
||||
continue
|
||||
|
||||
self.story.addToList('warnings', warning)
|
||||
|
||||
elif key == 'Chapters':
|
||||
self.story.setMetadata('numChapters', int(value))
|
||||
|
||||
elif key == 'Words':
|
||||
# Apparently only numChapters need to be an integer for
|
||||
# some strange reason. Remove possible ',' characters as to
|
||||
# not confuse the codebase down the line
|
||||
self.story.setMetadata('numWords', value.replace(',', ''))
|
||||
|
||||
elif key == 'Started':
|
||||
self.story.setMetadata('datePublished', makeDate(value, self.STARTED_DATETIME_FORMAT))
|
||||
|
||||
elif key == 'Updated':
|
||||
date_string, period = value.rsplit(' ', 1)
|
||||
date = makeDate(date_string, self.UPDATED_DATETIME_FORMAT)
|
||||
|
||||
# Rather ugly hack to work around Calibre's changing of
|
||||
# Python's locale setting, causing am/pm to not be properly
|
||||
# parsed by strptime() when using a non-english locale
|
||||
if period == 'pm':
|
||||
date += timedelta(hours=12)
|
||||
self.story.setMetadata('dateUpdated', date)
|
||||
|
||||
if self.story.getMetadata('rating') == 'NC-17' and not (self.is_adult or self.getConfig('is_adult')):
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
def getChapterText(self, url):
|
||||
soup = self._customized_fetch_url(url)
|
||||
storytext_div = soup.find('div', {'class': 'storytext'})
|
||||
|
||||
if self.getConfig('strip_text_links'):
|
||||
for anchor in storytext_div('a', {'class': 'FAtxtL'}):
|
||||
navigable_string = BeautifulSoup.NavigableString(anchor.string)
|
||||
anchor.replaceWith(navigable_string)
|
||||
|
||||
return self.utf8FromSoup(url, storytext_div)
|
||||
@@ -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,22 +249,55 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
value = unicode(value)
|
||||
self.story.setMetadata(metakey, value)
|
||||
|
||||
rawGroupList = soup.find('ul', {'id':'story_group_list'})
|
||||
if rawGroupList is not None:
|
||||
for groupName in rawGroupList.findAll('a', {'href':re.compile('^/group/')}):
|
||||
## 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(',', ';'))
|
||||
|
||||
|
||||
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.
|
||||
try:
|
||||
description = soup.find('div', {'class':'description'})
|
||||
firstHR = description.find("hr")
|
||||
nextSib = firstHR.nextSibling
|
||||
if "This story is a sequel to" in nextSib.string:
|
||||
link = nextSib.nextSibling
|
||||
if link.name == "a":
|
||||
self.story.setMetadata("prequelUrl", 'http://'+self.host+link["href"])
|
||||
self.story.setMetadata("prequel", stripHTML(link))
|
||||
except:
|
||||
pass
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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,257 @@
|
||||
# -*- 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]
|
||||
# replace later chapters with first chapter but don't remove numbers
|
||||
# from the URL that disambiguate stories with the same title.
|
||||
storyId = re.sub("-ch-?\d\d", "", storyId)
|
||||
self.story.setMetadata('storyId', storyId)
|
||||
|
||||
## accept m(mobile)url, but use www.
|
||||
url = re.sub("^(www|german|spanish|french|dutch|italian|romanian|portuguese|other)\.i",
|
||||
"\1",
|
||||
url)
|
||||
|
||||
## strip ?page=...
|
||||
url = re.sub("\?page=.*$", "", url)
|
||||
|
||||
## set url
|
||||
self._setURL(url)
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = '%m/%d/%y'
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return 'literotica.com'
|
||||
|
||||
@classmethod
|
||||
def getAcceptDomains(cls):
|
||||
return ['www.literotica.com',
|
||||
'www.i.literotica.com',
|
||||
'german.literotica.com',
|
||||
'german.i.literotica.com',
|
||||
'spanish.literotica.com',
|
||||
'spanish.i.literotica.com',
|
||||
'french.literotica.com',
|
||||
'french.i.literotica.com',
|
||||
'dutch.literotica.com',
|
||||
'dutch.i.literotica.com',
|
||||
'italian.literotica.com',
|
||||
'italian.i.literotica.com',
|
||||
'romanian.literotica.com',
|
||||
'romanian.i.literotica.com',
|
||||
'portuguese.literotica.com',
|
||||
'portuguese.i.literotica.com',
|
||||
'other.literotica.com',
|
||||
'other.i.literotica.com']
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://www.literotica.com/s/story-title https://www.literotica.com/s/story-title http://portuguese.literotica.com/s/story-title http://german.literotica.com/s/story-title"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return r"https?://(www|german|spanish|french|dutch|italian|romanian|portuguese|other)(\.i)?\.literotica\.com/s/([a-zA-Z0-9_-]+)"
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
"""
|
||||
NOTE: Some stories can have versions,
|
||||
e.g. /my-story-ch-05-version-10
|
||||
NOTE: If two stories share the same title, a running index is added,
|
||||
e.g.: /my-story-ch-02-1
|
||||
Strategy:
|
||||
* Go to author's page, search for the current story link,
|
||||
* If it's in a tr.root-story => One-part story
|
||||
* , get metadata and be done
|
||||
* If it's in a tr.sl => Chapter in series
|
||||
* Search up from there until we find a tr.ser-ttl (this is the
|
||||
story)
|
||||
* Gather metadata
|
||||
* Search down from there for all tr.sl until the next
|
||||
tr.ser-ttl, foreach
|
||||
* Chapter link is there
|
||||
"""
|
||||
|
||||
if not (self.is_adult or self.getConfig("is_adult")):
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
logger.debug("Chapter/Story URL: <%s> " % self.url)
|
||||
try:
|
||||
data1 = self._fetchUrl(self.url)
|
||||
soup1 = bs.BeautifulSoup(data1)
|
||||
#strip comments from soup
|
||||
[comment.extract() for comment in soup1.findAll(text=lambda text:isinstance(text, bs.Comment))]
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
# 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)
|
||||
#strip comments from soup
|
||||
[comment.extract() for comment in soupAuth.findAll(text=lambda text:isinstance(text, bs.Comment))]
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(authorurl)
|
||||
else:
|
||||
raise e
|
||||
|
||||
## Find link to url in author's page
|
||||
## site has started using //domain.name/asdf urls remove https?: from front
|
||||
storyLink = soupAuth.find('a', href=self.url[self.url.index(':')+1:])
|
||||
|
||||
if storyLink is not None:
|
||||
urlTr = storyLink.parent.parent
|
||||
if urlTr['class'] == "sl":
|
||||
isSingleStory = False
|
||||
else:
|
||||
isSingleStory = True
|
||||
else:
|
||||
raise exceptions.FailedToDownload("Couldn't find story <%s> on author's page <%s>" % (url, authorurl))
|
||||
|
||||
if isSingleStory:
|
||||
self.story.setMetadata('title', storyLink.text)
|
||||
self.story.setMetadata('description', urlTr.findAll("td")[1].text)
|
||||
self.story.addToList('eroticatags', urlTr.findAll("td")[2].text)
|
||||
date = urlTr.findAll('td')[-1].text
|
||||
self.story.setMetadata('datePublished', makeDate(date, self.dateformat))
|
||||
self.story.setMetadata('dateUpdated',makeDate(date, self.dateformat))
|
||||
self.chapterUrls = [(storyLink.text, self.url)]
|
||||
else:
|
||||
seriesTr = urlTr.previousSibling
|
||||
while seriesTr['class'] != 'ser-ttl':
|
||||
seriesTr = seriesTr.previousSibling
|
||||
m = re.match("^(?P<title>.*?):\s(?P<numChapters>\d+)\sPart\sSeries$", seriesTr.find("strong").text)
|
||||
self.story.setMetadata('title', m.group('title'))
|
||||
self.story.setMetadata('numChapters', int(m.group('numChapters')))
|
||||
|
||||
## Walk the chapters
|
||||
chapterTr = seriesTr.nextSibling
|
||||
self.chapterUrls = []
|
||||
dates = []
|
||||
descriptions = []
|
||||
while chapterTr is not None and chapterTr['class'] == 'sl':
|
||||
descriptions.append(chapterTr.findAll("td")[1].text)
|
||||
chapterLink = chapterTr.find("td", "fc").find("a")
|
||||
self.chapterUrls.append((chapterLink.text, "http:" + chapterLink["href"]))
|
||||
self.story.addToList('eroticatags', chapterTr.findAll("td")[2].text)
|
||||
dates.append(makeDate(chapterTr.findAll('td')[-1].text, self.dateformat))
|
||||
chapterTr = chapterTr.nextSibling
|
||||
|
||||
## Set description to joint chapter descriptions
|
||||
self.story.setMetadata('description', " / ".join(descriptions))
|
||||
|
||||
## Set the oldest date as publication date, the newest as update date
|
||||
dates.sort()
|
||||
self.story.setMetadata('datePublished', dates[0])
|
||||
self.story.setMetadata('dateUpdated', dates[-1])
|
||||
|
||||
# normalize on first chapter URL.
|
||||
self._setURL(self.chapterUrls[0][1])
|
||||
|
||||
# set storyId to 'title-author' to avoid duplicates
|
||||
# self.story.setMetadata('storyId',
|
||||
# re.sub("[^a-z0-9]", "", self.story.getMetadata('title').lower())
|
||||
# + "-"
|
||||
# + re.sub("[^a-z0-9]", "", self.story.getMetadata('author').lower()))
|
||||
|
||||
return
|
||||
|
||||
def getChapterText(self, url):
|
||||
logger.debug('Getting chapter text from <%s>' % url)
|
||||
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,207 @@
|
||||
import re
|
||||
import urllib2
|
||||
import urlparse
|
||||
|
||||
from .. import BeautifulSoup
|
||||
from ..htmlcleanup import stripHTML
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
from .. import exceptions
|
||||
|
||||
|
||||
def getClass():
|
||||
return SpikeluverComAdapter
|
||||
|
||||
|
||||
# yields Tag _and_ NavigableString siblings from the given tag. The
|
||||
# BeautifulSoup findNextSiblings() method for some reasons only returns either
|
||||
# NavigableStrings _or_ Tag objects, not both.
|
||||
def _yield_next_siblings(tag):
|
||||
sibling = tag.nextSibling
|
||||
while sibling:
|
||||
yield sibling
|
||||
sibling = sibling.nextSibling
|
||||
|
||||
|
||||
class SpikeluverComAdapter(BaseSiteAdapter):
|
||||
SITE_ABBREVIATION = 'slc'
|
||||
SITE_DOMAIN = 'spikeluver.com'
|
||||
|
||||
BASE_URL = 'http://' + SITE_DOMAIN + '/SpuffyRealm/'
|
||||
LOGIN_URL = BASE_URL + 'user.php?action=login'
|
||||
VIEW_STORY_URL_TEMPLATE = BASE_URL + 'viewstory.php?sid=%d'
|
||||
METADATA_URL_SUFFIX = '&index=1'
|
||||
AGE_CONSENT_URL_SUFFIX = '&ageconsent=ok&warning=5'
|
||||
|
||||
DATETIME_FORMAT = '%m/%d/%Y'
|
||||
STORY_DOES_NOT_EXIST_ERROR_TEXT = 'That story does not exist on this archive. You may search for it or return to the home page.'
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
query_data = urlparse.parse_qs(self.parsedUrl.query)
|
||||
story_id = query_data['sid'][0]
|
||||
|
||||
self.story.setMetadata('storyId', story_id)
|
||||
self._setURL(self.VIEW_STORY_URL_TEMPLATE % int(story_id))
|
||||
self.story.setMetadata('siteabbrev', self.SITE_ABBREVIATION)
|
||||
|
||||
def _customized_fetch_url(self, url, exception=None, parameters=None):
|
||||
if exception:
|
||||
try:
|
||||
data = self._fetchUrl(url, parameters)
|
||||
except urllib2.HTTPError:
|
||||
raise exception(self.url)
|
||||
# Just let self._fetchUrl throw the exception, don't catch and
|
||||
# customize it.
|
||||
else:
|
||||
data = self._fetchUrl(url, parameters)
|
||||
|
||||
return BeautifulSoup.BeautifulSoup(data)
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return SpikeluverComAdapter.SITE_DOMAIN
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return cls.VIEW_STORY_URL_TEMPLATE % 1234
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape(self.VIEW_STORY_URL_TEMPLATE[:-2]) + r'\d+$'
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
soup = self._customized_fetch_url(self.url + self.METADATA_URL_SUFFIX)
|
||||
|
||||
errortext_div = soup.find('div', {'class': 'errortext'})
|
||||
if errortext_div:
|
||||
error_text = ''.join(errortext_div(text=True)).strip()
|
||||
if error_text == self.STORY_DOES_NOT_EXIST_ERROR_TEXT:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
|
||||
# No additional login is required, just check for adult
|
||||
pagetitle_div = soup.find('div', id='pagetitle')
|
||||
if pagetitle_div.a['href'].startswith('javascript:'):
|
||||
if not(self.is_adult or self.getConfig('is_adult')):
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
url = ''.join([self.url, self.METADATA_URL_SUFFIX, self.AGE_CONSENT_URL_SUFFIX])
|
||||
soup = self._customized_fetch_url(url)
|
||||
|
||||
pagetitle_div = soup.find('div', id='pagetitle')
|
||||
self.story.setMetadata('title', stripHTML(pagetitle_div.a))
|
||||
|
||||
author_anchor = pagetitle_div.a.findNextSibling('a')
|
||||
url = urlparse.urljoin(self.BASE_URL, author_anchor['href'])
|
||||
components = urlparse.urlparse(url)
|
||||
query_data = urlparse.parse_qs(components.query)
|
||||
|
||||
self.story.setMetadata('author', stripHTML(author_anchor))
|
||||
self.story.setMetadata('authorId', query_data['uid'])
|
||||
self.story.setMetadata('authorUrl', url)
|
||||
|
||||
sort_div = soup.find('div', id='sort')
|
||||
self.story.setMetadata('reviews', stripHTML(sort_div('a')[1]))
|
||||
|
||||
listbox_tag = soup.find('div', {'class': 'listbox'})
|
||||
for span_tag in listbox_tag('span'):
|
||||
key = span_tag.string.strip(' :')
|
||||
try:
|
||||
value = stripHTML(span_tag.nextSibling)
|
||||
# This can happen with some fancy markup in the summary. Just
|
||||
# ignore this error and set value to None, the summary parsing
|
||||
# takes care of this
|
||||
except AttributeError:
|
||||
value = None
|
||||
|
||||
if key == 'Summary':
|
||||
contents = []
|
||||
keep_summary_html = self.getConfig('keep_summary_html')
|
||||
|
||||
for sibling in _yield_next_siblings(span_tag):
|
||||
if isinstance(sibling, BeautifulSoup.Tag):
|
||||
# Encountered next label, break. Not as bad as other
|
||||
# e-fiction sites, let's hope this is enough for proper
|
||||
# parsing.
|
||||
if sibling.name == 'span' and sibling.get('class', None) == 'label':
|
||||
break
|
||||
|
||||
if keep_summary_html:
|
||||
contents.append(self.utf8FromSoup(self.url, sibling))
|
||||
else:
|
||||
contents.append(''.join(sibling(text=True)))
|
||||
else:
|
||||
contents.append(sibling)
|
||||
|
||||
# Remove the preceding break line tag and other crud
|
||||
contents.pop()
|
||||
contents.pop()
|
||||
self.story.setMetadata('description', ''.join(contents))
|
||||
|
||||
elif key == 'Rated':
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
elif key == 'Categories':
|
||||
for sibling in span_tag.findNextSiblings(['a', 'br']):
|
||||
if sibling.name == 'br':
|
||||
break
|
||||
|
||||
self.story.addToList('category', stripHTML(sibling))
|
||||
|
||||
# Seems to be always "None" for some reason
|
||||
elif key == 'Characters':
|
||||
for sibling in span_tag.findNextSiblings(['a', 'br']):
|
||||
if sibling.name == 'br':
|
||||
break
|
||||
self.story.addToList('characters', stripHTML(sibling))
|
||||
|
||||
elif key == 'Genres':
|
||||
for sibling in span_tag.findNextSiblings(['a', 'br']):
|
||||
if sibling.name == 'br':
|
||||
break
|
||||
|
||||
self.story.addToList('genre', stripHTML(sibling))
|
||||
|
||||
elif key == 'Warnings':
|
||||
for sibling in span_tag.findNextSiblings(['a', 'br']):
|
||||
if sibling.name == 'br':
|
||||
break
|
||||
self.story.addToList('warnings', stripHTML(sibling))
|
||||
|
||||
# Challenges
|
||||
|
||||
elif key == 'Series':
|
||||
a = span_tag.findNextSibling('a')
|
||||
if not a:
|
||||
continue
|
||||
self.story.setMetadata('series', stripHTML(a))
|
||||
self.story.setMetadata('seriesUrl', urlparse.urljoin(self.BASE_URL, a['href']))
|
||||
|
||||
elif key == 'Chapters':
|
||||
self.story.setMetadata('numChapters', int(value))
|
||||
|
||||
elif key == 'Completed':
|
||||
self.story.setMetadata('status', 'Completed' if value == 'Yes' else 'In-Progress')
|
||||
|
||||
elif key == 'Word count':
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
elif key == 'Published':
|
||||
self.story.setMetadata('datePublished', makeDate(value, self.DATETIME_FORMAT))
|
||||
|
||||
elif key == 'Updated':
|
||||
self.story.setMetadata('dateUpdated', makeDate(value, self.DATETIME_FORMAT))
|
||||
|
||||
for p_tag in listbox_tag.findNextSiblings('p'):
|
||||
chapter_anchor = p_tag.find('a', href=lambda href: href and href.startswith('viewstory.php?sid='))
|
||||
if not chapter_anchor:
|
||||
continue
|
||||
|
||||
title = stripHTML(chapter_anchor)
|
||||
url = urlparse.urljoin(self.BASE_URL, chapter_anchor['href'])
|
||||
self.chapterUrls.append((title, url))
|
||||
|
||||
def getChapterText(self, url):
|
||||
url += self.AGE_CONSENT_URL_SUFFIX
|
||||
soup = self._customized_fetch_url(url)
|
||||
return self.utf8FromSoup(url, soup.find('div', id='story'))
|
||||
@@ -62,7 +62,7 @@ class SquidgeOrgPejaAdapter(BaseSiteAdapter):
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/peja/cgi-bin/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
self._setURL('https://' + self.getSiteDomain() + '/peja/cgi-bin/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','wwomb')
|
||||
@@ -83,10 +83,10 @@ class SquidgeOrgPejaAdapter(BaseSiteAdapter):
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://"+self.getSiteDomain()+"/peja/cgi-bin/viewstory.php?sid=1234"
|
||||
return "https://"+self.getSiteDomain()+"/peja/cgi-bin/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain()+"/")+"~?"+re.escape("peja/cgi-bin/viewstory.php?sid=")+r"\d+$"
|
||||
return r"https?"+re.escape("://"+self.getSiteDomain()+"/")+r"~?"+re.escape("peja/cgi-bin/viewstory.php?sid=")+r"\d+$"
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
@@ -116,7 +116,7 @@ class SquidgeOrgPejaAdapter(BaseSiteAdapter):
|
||||
# Find authorid and URL from... author url.
|
||||
author = soup.find('div', {'id':"pagetitle"}).find('a')
|
||||
self.story.setMetadata('authorId',author['href'].split('=')[1])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/peja/cgi-bin/'+author['href'])
|
||||
self.story.setMetadata('authorUrl','https://'+self.host+'/peja/cgi-bin/'+author['href'])
|
||||
self.story.setMetadata('author',author.string)
|
||||
|
||||
authorSoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')))
|
||||
@@ -131,7 +131,7 @@ class SquidgeOrgPejaAdapter(BaseSiteAdapter):
|
||||
chapterselect=soup.find('select',{'name':'chapter'})
|
||||
if chapterselect:
|
||||
for ch in chapterselect.findAll('option'):
|
||||
self.chapterUrls.append((stripHTML(ch),'http://'+self.host+'/peja/cgi-bin/viewstory.php?sid='+self.story.getMetadata('storyId')+'&chapter='+ch['value']))
|
||||
self.chapterUrls.append((stripHTML(ch),'https://'+self.host+'/peja/cgi-bin/viewstory.php?sid='+self.story.getMetadata('storyId')+'&chapter='+ch['value']))
|
||||
else:
|
||||
self.chapterUrls.append((title,url))
|
||||
|
||||
@@ -207,7 +207,7 @@ class SquidgeOrgPejaAdapter(BaseSiteAdapter):
|
||||
# http://www.squidge.org/peja/cgi-bin/series.php?seriesid=254
|
||||
a = titleblock.find('a', href=re.compile(r"series.php\?seriesid=\d+"))
|
||||
series_name = a.string
|
||||
series_url = 'http://'+self.host+'/peja/cgi-bin/'+a['href']
|
||||
series_url = 'https://'+self.host+'/peja/cgi-bin/'+a['href']
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2014 Fanficdownloader team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
from .. import BeautifulSoup as bs
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
|
||||
class TheMapleBookshelfComSiteAdapter(BaseSiteAdapter):
|
||||
"""
|
||||
Use Printable version which is easier to parse and has everything in one
|
||||
page and cache between extractChapterUrlsAndMetadata and getChapterText
|
||||
"""
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
self.story.setMetadata('siteabbrev','maplebook')
|
||||
self.decode = ["Windows-1252", "utf8"]
|
||||
self.story.setMetadata('storyId', re.compile(self.getSiteURLPattern()).match(url).group('storyId'))
|
||||
self._setURL("http://%s/Literati/viewstory.php?sid=%s" % (self.getSiteDomain(), self.story.getMetadata('storyId')))
|
||||
self.dateformat = "%b %d, %Y"
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return 'themaplebookshelf.com'
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://www.themaplebookshelf.com/Literati/viewstory.php?sid=227 http://themaplebookshelf.com/Literati/viewstory.php?sid=227&chapter=2"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return r"http://themaplebookshelf.com/Literati/viewstory.php\?sid=(?P<storyId>\d+)"
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
logger.debug(self.url)
|
||||
self._setURL(self.url + "&action=printable")
|
||||
try:
|
||||
html = self._fetchUrl(self.url)
|
||||
soup = bs.BeautifulSoup(html)
|
||||
# #strip comments from soup
|
||||
# [comment.extract() for comment in soup1.findAll(text=lambda text:isinstance(text, bs.Comment))]
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
## title + author
|
||||
pagetitleDiv = soup.find("div", {"id": "pagetitle"})
|
||||
self.story.setMetadata('title', pagetitleDiv.find("a").text)
|
||||
authorLink = pagetitleDiv.findAll("a")[1]
|
||||
self.story.setMetadata('author', authorLink.text)
|
||||
self.story.setMetadata('authorUrl', "http://" + self.getSiteDomain() + "/" + authorLink['href'])
|
||||
self.story.setMetadata('authorId', re.search("\d+", authorLink['href']).group(0))
|
||||
|
||||
## Description
|
||||
description = ""
|
||||
summaryEnd = soup.find("div", "content").find("span", "label").nextSibling
|
||||
while summaryEnd is not None:
|
||||
description += stripHTML(summaryEnd)
|
||||
summaryEnd = summaryEnd.nextSibling
|
||||
if type(summaryEnd) != bs.NavigableString and summaryEnd.name == 'br':
|
||||
break
|
||||
self.story.setMetadata('description', description)
|
||||
|
||||
## General Metadata
|
||||
for kSpan in soup.findAll("span", "label"):
|
||||
k = kSpan.text.strip().replace(':', '')
|
||||
vSpan = kSpan.nextSibling
|
||||
if k == 'Summary:' or not vSpan or not vSpan.string:
|
||||
continue
|
||||
v = vSpan.string.strip()
|
||||
if v == 'None':
|
||||
continue
|
||||
logger.debug("%s '%s'" %(k, v))
|
||||
if k == 'Genre':
|
||||
for genre in v.split(", "):
|
||||
self.story.addToList('genre', genre)
|
||||
elif k == 'Chapters':
|
||||
self.story.setMetadata('numChapters', int(v))
|
||||
elif k == 'Word count':
|
||||
self.story.setMetadata('numWords', v)
|
||||
elif k == 'Published':
|
||||
self.story.setMetadata('datePublished', makeDate(v, self.dateformat))
|
||||
elif k == 'Updated':
|
||||
self.story.setMetadata('dateUpdated', makeDate(v, self.dateformat))
|
||||
# TODO: Series, Warnings
|
||||
|
||||
## Chapter URLs (fragment identifiers in the document, so we don' need to fetch so much)
|
||||
for chapterNumB in soup.findAll("b", text=re.compile("^\d+\.$")):
|
||||
self.chapterUrls.append((
|
||||
chapterNumB.parent.parent.find("a").text,
|
||||
self.url + chapterNumB.parent.parent.find("a")["href"]
|
||||
))
|
||||
|
||||
## fix all local image 'src' to absolute
|
||||
for img in soup.findAll("img", {"src": re.compile("^(?!http)")}):
|
||||
img['src'] = re.sub("viewstory.php\?.*", "", self.url) + img['src']
|
||||
|
||||
self.html = soup
|
||||
|
||||
def getChapterText(self, url):
|
||||
logger.debug('Getting chapter text from <%s>' % url)
|
||||
anchor = url.replace(self.url, "")
|
||||
anchor = anchor.replace("#", "")
|
||||
chapterDiv = self.html.find("a", {"name": anchor}).parent.findNext("div", "chapter")
|
||||
return self.utf8FromSoup(self.url, chapterDiv)
|
||||
|
||||
def getClass():
|
||||
return TheMapleBookshelfComSiteAdapter
|
||||
@@ -0,0 +1,238 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
FFDL Adapter for TolkienFanFiction.com.
|
||||
|
||||
Chapter URL: http://www.tolkienfanfiction.com/Story_Read_Chapter.php?CHid=1234
|
||||
Metadata
|
||||
Link to Story URL [Index]
|
||||
chapterTitle
|
||||
storyTitle
|
||||
Story URL: http://www.tolkienfanfiction.com/Story_Read_Head.php?STid=1034
|
||||
Metadata
|
||||
Links to Chapter URLs
|
||||
storyTitle
|
||||
chapterTitle[s]
|
||||
author
|
||||
authorId
|
||||
authorUrl
|
||||
numChapters
|
||||
wordCount
|
||||
description/summary
|
||||
rating TODO
|
||||
genre TODO
|
||||
Characters
|
||||
Ages (specific) TODO
|
||||
Search: http://www.tolkienfanfiction.com/Story_Chapter_Search.php?text=From+Wilderness+to+Cities+White&field=1&type=3&search=Search
|
||||
Strategy
|
||||
Search by exact phrase for styo
|
||||
Metadata
|
||||
dateUpdated
|
||||
Parameters
|
||||
field (field to search)
|
||||
1: title
|
||||
2: description
|
||||
3: chapter text
|
||||
type (any, all or exact phrase)
|
||||
1: any
|
||||
2: all
|
||||
3: exact phrase
|
||||
|
||||
"""
|
||||
# Copyright 2014 Fanficdownloader team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib
|
||||
import urllib2
|
||||
import urlparse
|
||||
import string
|
||||
|
||||
from .. import BeautifulSoup as bs
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
def _is_story_url(url):
|
||||
return "Story_Read_Head.php" in url
|
||||
|
||||
def _latinize(text):
|
||||
"""
|
||||
See http://stackoverflow.com/a/19114706/201318
|
||||
"""
|
||||
src = u"áâäÉéêëíóôöúû"
|
||||
tgt = u"aaaEeeeiooouu"
|
||||
src_ord = [ord(char) for char in src]
|
||||
translate_table = dict(zip(src_ord, tgt))
|
||||
return text.translate(translate_table)
|
||||
|
||||
def _fix_broken_markup(html):
|
||||
"""Replaces invalid comment tags"""
|
||||
if html.startswith("<CENTER>"):
|
||||
logger.error("TolkienFanFiction.com couldn't handle this request: '%s'" % html)
|
||||
html = re.sub("<!-.+?->", "", html)
|
||||
return html
|
||||
|
||||
|
||||
class TolkienFanfictionAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.decode = ["ISO-8859-1", "Windows-1252"]
|
||||
|
||||
self.story.setMetadata('siteabbrev','tolkien')
|
||||
|
||||
self.dateformat = '%B %d, %Y'
|
||||
|
||||
self._normalizeURL(url)
|
||||
|
||||
def _normalizeURL(self, url):
|
||||
if _is_story_url(url):
|
||||
self.story.setMetadata('storyId', re.compile(self.getSiteURLPattern()).match(url).group('storyId'))
|
||||
self._setURL('http://' + self.getSiteDomain() + '/Story_Read_Head.php?STid=' + self.story.getMetadata('storyId'))
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return 'tolkienfanfiction.com'
|
||||
|
||||
@classmethod
|
||||
def getAcceptDomains(cls):
|
||||
return ['tolkienfanfiction.com', 'www.tolkienfanfiction.com']
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(self):
|
||||
return 'http://www.tolkienfanfiction.com/Story_Read_Head.php?STid=1034 http://www.tolkienfanfiction.com/Story_Read_Chapter.php?CHid=4945'
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return r"http://(?:www.)?tolkienfanfiction.com/(?:Story_Read_Chapter\.php\?CH|Story_Read_Head\.php\?ST)id=(?P<storyId>[0-9]+)"
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
if not _is_story_url(self.url):
|
||||
# Get the link to the index page
|
||||
try:
|
||||
chapterHtml = _fix_broken_markup(self._fetchUrl(self.url))
|
||||
chapterSoup = bs.BeautifulSoup(chapterHtml)
|
||||
indexLink = chapterSoup.find("a", text="[Index]").parent
|
||||
self._normalizeURL('http://' + self.getSiteDomain() + '/' + indexLink.get('href'))
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
logger.debug("Determined index page: <%s>" % self.url)
|
||||
|
||||
try:
|
||||
indexHtml = _fix_broken_markup(self._fetchUrl(self.url))
|
||||
soup = bs.BeautifulSoup(indexHtml)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
# chapterUrls
|
||||
for pfLink in soup.findAll("a", text='[PF] '):
|
||||
chapterLink = pfLink.parent.findNext("a")
|
||||
chapterTitle = chapterLink.string
|
||||
if self.getConfig('strip_chapter_numeral'):
|
||||
chapterTitle = re.sub("^\d+:", "", chapterTitle)
|
||||
chapterUrl = 'http://' + self.host + '/' + chapterLink['href']
|
||||
self.chapterUrls.append((chapterTitle, chapterUrl))
|
||||
numChapters = len(self.chapterUrls)
|
||||
self.story.setMetadata('numChapters', numChapters)
|
||||
logger.debug('Number of Chapters: %s' % numChapters)
|
||||
|
||||
# title
|
||||
title = soup.find("table", "headertitle").find("tr").contents[1].string
|
||||
logger.debug("Title: '%s'" % title)
|
||||
self.story.setMetadata('title', title)
|
||||
|
||||
# author
|
||||
authorLink = soup.find("a", {"href":lambda x: x.startswith("Author_Profile.php")})
|
||||
authorName = authorLink.find("b").string
|
||||
authorHref = authorLink['href']
|
||||
authorUrl = 'http:' + self.host + '/' + authorHref
|
||||
authorId = authorHref[authorHref.index('=')+1:]
|
||||
self.story.setMetadata('author', authorName)
|
||||
self.story.setMetadata('authorId', authorId)
|
||||
self.story.setMetadata('authorUrl', authorUrl)
|
||||
logger.debug("Author: %s [%s] @ <%s>" % (authorId, authorName, authorUrl))
|
||||
|
||||
# numWords
|
||||
numWordsMatch = re.search("Word Count: (\d+)<BR>", indexHtml)
|
||||
if numWordsMatch:
|
||||
numWords = numWordsMatch.group(1)
|
||||
logger.debug('Number of words: %s' % numWords)
|
||||
self.story.setMetadata('numWords', numWords)
|
||||
|
||||
# description
|
||||
description = soup.find("b", text="Description:").parent.nextSibling.nextSibling
|
||||
self.story.setDescription(description)
|
||||
logger.debug("Summary: '%s'" % description)
|
||||
|
||||
# characters
|
||||
characters = soup.find("b", text="Characters").parent.nextSibling.nextSibling.nextSibling
|
||||
for character in characters.split(", "):
|
||||
self.story.addToList('characters', character)
|
||||
logger.debug("Characters: %s" % self.story.getMetadata('characters'))
|
||||
|
||||
logger.debug('Title as `str`: ' + str(title))
|
||||
# For publication date we need to search
|
||||
try:
|
||||
queryString = urllib.urlencode((
|
||||
('type', 3),
|
||||
('field', 1),
|
||||
# need translate here for the weird accented letters
|
||||
('text', _latinize(title)),
|
||||
('search', 'Search'),
|
||||
))
|
||||
searchUrl = 'http://%s/Story_Chapter_Search.php?%s' % (self.host, queryString)
|
||||
logger.debug("Search URL: <%s>" % searchUrl)
|
||||
searchHtml = _fix_broken_markup(self._fetchUrl(searchUrl))
|
||||
searchSoup = bs.BeautifulSoup(searchHtml)
|
||||
date = searchSoup.find(text="Updated:").nextSibling.string
|
||||
logger.debug("Last Updated: '%s'" % date)
|
||||
self.story.setMetadata('dateUpdated', makeDate(date, self.dateformat))
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
def getChapterText(self, url):
|
||||
|
||||
logger.debug('Downloading chapter <%s>' % url)
|
||||
|
||||
time.sleep(0.5)
|
||||
htmldata = _fix_broken_markup(self._fetchUrl(url))
|
||||
soup = bs.BeautifulSoup(htmldata)
|
||||
|
||||
#strip comments from soup
|
||||
[comment.extract() for comment in soup.findAll(text=lambda text:isinstance(text, bs.Comment))]
|
||||
|
||||
# Strip redundant headings
|
||||
[font.parent.extract() for font in soup.findAll("font", {"size": "4"})]
|
||||
|
||||
# get story text
|
||||
textDiv = soup.find("div", "text")
|
||||
return self.utf8FromSoup(url, textDiv)
|
||||
|
||||
def getClass():
|
||||
return TolkienFanfictionAdapter
|
||||
@@ -0,0 +1,325 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2013 Fanficdownloader team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
from .. import BeautifulSoup as bs
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
def getClass():
|
||||
return TrekiverseOrgAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class TrekiverseOrgAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.decode = ["iso-8859-1",
|
||||
"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.username = "NoneGiven" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
self.is_adult=False
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL("http://"+self.getSiteDomain()\
|
||||
+"/efiction/viewstory.php?sid="+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','trkvs')
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%d/%m/%Y"
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain.
|
||||
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 http://efiction."+self.getSiteDomain()+"/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
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):
|
||||
if 'Registered Users Only' in data \
|
||||
or 'There is no such account on our website' in data \
|
||||
or "That password doesn't match the one in our database" in data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def performLogin(self, url):
|
||||
params = {}
|
||||
|
||||
if self.password:
|
||||
params['penname'] = self.username
|
||||
params['password'] = self.password
|
||||
else:
|
||||
params['penname'] = self.getConfig("username")
|
||||
params['password'] = self.getConfig("password")
|
||||
params['cookiecheck'] = '1'
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/efiction/user.php?action=login'
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Member Account" not in d : #Member Account
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
# Weirdly, different sites use different warning numbers.
|
||||
# If the title search below fails, there's a good chance
|
||||
# you need a different number. print data at that point
|
||||
# and see what the 'click here to continue' url says.
|
||||
addurl = "&index=1&ageconsent=ok&warning=5"
|
||||
else:
|
||||
addurl="&index=1"
|
||||
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+addurl
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
if self.needToLoginCheck(data):
|
||||
# need to log in for this one.
|
||||
self.performLogin(url)
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data)
|
||||
if m != None:
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
# We tried the default and still got a warning, so
|
||||
# let's pull the warning number from the 'continue'
|
||||
# link and reload data.
|
||||
addurl = m.group(1)
|
||||
# correct stupid & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
url = self.url+'&index=1'+addurl
|
||||
logger.debug("URL 2nd try: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
else:
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = bs.BeautifulSoup(data)
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
## Title and author
|
||||
a = soup.find('div', {'id' : 'pagetitle'})
|
||||
aut = a.find('a', href=re.compile(r"^viewuser\.php\?uid="))
|
||||
self.story.setMetadata('authorId',aut['href'].split('=')[1])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/efiction/'+aut['href'])
|
||||
self.story.setMetadata('author',aut.string)
|
||||
|
||||
ttl = a.find('a', href=re.compile(r'^viewstory.php\?sid=%s$'%self.story.getMetadata('storyId')))
|
||||
self.story.setMetadata('title',ttl.string)
|
||||
|
||||
# Find the chapters:
|
||||
outputdiv = soup.find('div', {'id':'output'})
|
||||
# (amp;)? because it should be &, but is escaped to & in URL.
|
||||
# viewstory.php?sid=35&chapter=3
|
||||
chapters=outputdiv.findAll('a', href=re.compile(r'^viewstory.php\?sid=%s&(amp;)?chapter=\d+$'%self.story.getMetadata('storyId')))
|
||||
if len(chapters)==0:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: No php/html chapters found.")
|
||||
if len(chapters)==1:
|
||||
self.chapterUrls.append((self.story.getMetadata('title'),'http://'+self.host+'/efiction/'+chapters[0]['href']))
|
||||
else:
|
||||
for chapter in chapters:
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/efiction/'+chapter['href']))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
# eFiction sites don't help us out a lot with their meta data
|
||||
# formating, so it's a little ugly.
|
||||
|
||||
def defaultGetattr(d,k):
|
||||
try:
|
||||
return d[k]
|
||||
except:
|
||||
return ""
|
||||
|
||||
# <span class="label">Rated:</span> NC-17<br /> etc
|
||||
labels = soup.findAll('span',{'class':'label'})
|
||||
for labelspan in labels:
|
||||
value = labelspan.nextSibling
|
||||
label = labelspan.string
|
||||
|
||||
if 'Summary' in label:
|
||||
## Everything until the next span class='label'
|
||||
svalue = ''
|
||||
while value and not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
value = value.nextSibling
|
||||
# sometimes poorly formated desc (<p> w/o </p>) leads
|
||||
# to all labels being included.
|
||||
svalue=svalue[:svalue.find('<span class="label">')]
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
if 'Rated' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
if 'Categories' in label:
|
||||
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
catstext = [cat.string for cat in cats]
|
||||
for cat in catstext:
|
||||
self.story.addToList('category',cat.string)
|
||||
|
||||
if 'Characters' in label:
|
||||
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
|
||||
charstext = [char.string for char in chars]
|
||||
for char in charstext:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
if 'Genre' in label:
|
||||
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=9'))
|
||||
genrestext = [genre.string for genre in genres]
|
||||
self.genre = ', '.join(genrestext)
|
||||
for genre in genrestext:
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
if 'Awards' in label:
|
||||
awards = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=12'))
|
||||
awardstext = [award.string for award in awards]
|
||||
self.award = ', '.join(awardstext)
|
||||
for award in awardstext:
|
||||
self.story.addToList('awards',award.string)
|
||||
|
||||
if 'Pairing' in label:
|
||||
ships = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=10'))
|
||||
shipstext = [ship.string for ship in ships]
|
||||
self.ship = ', '.join(shipstext)
|
||||
for ship in shipstext:
|
||||
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=11'))
|
||||
warningstext = [warning.string for warning in warnings]
|
||||
self.warning = ', '.join(warningstext)
|
||||
for warning in warningstext:
|
||||
self.story.addToList('warnings',warning.string)
|
||||
|
||||
if 'Completed' in label:
|
||||
if 'Yes' in value:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
if 'Published' in label:
|
||||
self.story.setMetadata('datePublished', makeDate(value.strip(), "%d %b %Y"))
|
||||
|
||||
if 'Updated' in label:
|
||||
self.story.setMetadata('dateUpdated', makeDate(value.strip(), "%d %b %Y"))
|
||||
|
||||
try:
|
||||
# Find Series name from series URL.
|
||||
a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+"))
|
||||
series_name = a.string
|
||||
series_url = 'http://'+self.host+'/efiction/'+a['href']
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url))
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$'))
|
||||
i=1
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
except:
|
||||
# I find it hard to care if the series parsing fails
|
||||
pass
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(url)) # otherwise soup eats the br/hr tags.
|
||||
|
||||
div = soup.find('div', {'id' : 'story'})
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
notesdiv = soup.find('div', {'class':'noteinfo'})
|
||||
if notesdiv != None:
|
||||
div.insert(0,"<hr>")
|
||||
div.insert(0,notesdiv)
|
||||
div.insert(0,"<hr>")
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
@@ -134,18 +134,18 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
|
||||
if "<h2>Story Not Found</h2>" in data:
|
||||
raise exceptions.StoryDoesNotExist(url)
|
||||
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
form = soup.find('form', {'id':'sitemaxratingform'})
|
||||
params={'ctkn':form.find('input', {'name':'ctkn'})['value'],
|
||||
'sitemaxrating':'5'}
|
||||
logger.info("Attempting to get rating cookie for %s" % url)
|
||||
data = self._postUrl("http://"+self.getSiteDomain()+'/setmaxrating.php',params)
|
||||
# refetch story page.
|
||||
data = self._fetchUrl(url)
|
||||
soup = bs.BeautifulSoup(data)
|
||||
|
||||
if "NOTE: This story is rated FR21 which is above your chosen filter level." in data:
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
form = soup.find('form', {'id':'sitemaxratingform'})
|
||||
params={'ctkn':form.find('input', {'name':'ctkn'})['value'],
|
||||
'sitemaxrating':'5'}
|
||||
logger.info("Attempting to get rating cookie for %s" % url)
|
||||
data = self._postUrl("http://"+self.getSiteDomain()+'/setmaxrating.php',params)
|
||||
# refetch story page.
|
||||
data = self._fetchUrl(url)
|
||||
soup = bs.BeautifulSoup(data)
|
||||
else:
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
# http://www.tthfanfic.org/AuthorStories-3449/Greywizard.htm
|
||||
# Find authorid and URL from... author url.
|
||||
@@ -163,7 +163,14 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
|
||||
authorsoup = bs.BeautifulSoup(authordata)
|
||||
# author can have several pages, scan until we find it.
|
||||
while( not authorsoup.find('a', href=re.compile(r"^/Story-"+self.story.getMetadata('storyId'))) ):
|
||||
nextpage = 'http://'+self.host+authorsoup.find('a', {'class':'arrowf'})['href']
|
||||
nextarrow = authorsoup.find('a', {'class':'arrowf'})
|
||||
if not nextarrow:
|
||||
## if rating is set lower than story, it won't be
|
||||
## visible on author lists unless. The *story* is
|
||||
## visible via the url, just not the entry on
|
||||
## author list.
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
nextpage = 'http://'+self.host+nextarrow['href']
|
||||
logger.debug("**AUTHOR** nextpage URL: "+nextpage)
|
||||
authordata = self._fetchUrl(nextpage)
|
||||
descurl=nextpage
|
||||
|
||||
@@ -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)))
|
||||
@@ -282,9 +297,10 @@ class BaseSiteAdapter(Configurable):
|
||||
@classmethod
|
||||
def getSiteExampleURLs(self):
|
||||
"""
|
||||
Return a string of space separated example URLs.
|
||||
Needs to be overriden in each adapter class. It's the adapter
|
||||
writer's responsibility to make sure the example(s) pass the
|
||||
URL validate.
|
||||
validateURL method.
|
||||
"""
|
||||
return 'no such example'
|
||||
|
||||
@@ -313,7 +329,8 @@ class BaseSiteAdapter(Configurable):
|
||||
|
||||
def setCoverImage(self,storyurl,imgurl):
|
||||
if self.getConfig('include_images'):
|
||||
self.story.addImgUrl(storyurl,imgurl,self._fetchUrlRaw,cover=True)
|
||||
self.story.addImgUrl(storyurl,imgurl,self._fetchUrlRaw,cover=True,
|
||||
coverexclusion=self.getConfig('cover_exclusion_regexp'))
|
||||
|
||||
# This gives us a unicode object, not just a string containing bytes.
|
||||
# (I gave soup a unicode string, you'd think it could give it back...)
|
||||
@@ -358,7 +375,7 @@ class BaseSiteAdapter(Configurable):
|
||||
|
||||
retval = soup.__str__('utf8').decode('utf-8')
|
||||
|
||||
if self.getConfig('nook_img_fix'):
|
||||
if self.getConfig('nook_img_fix') and not self.getConfig('replace_br_with_p'):
|
||||
# if the <img> tag doesn't have a div or a p around it,
|
||||
# nook gets confused and displays it on every page after
|
||||
# that under the text for the rest of the chapter.
|
||||
@@ -367,7 +384,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
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import codecs
|
||||
import BeautifulSoup as bs
|
||||
import HtmlTagStack as stack
|
||||
|
||||
from . import exceptions as exceptions
|
||||
|
||||
@@ -28,7 +30,7 @@ def replace_br_with_p(body):
|
||||
# However, Python Regex does not recognize it as a whitespace, so we'll be changing it to a reagular space.
|
||||
body = body.replace(u'\xa0', u' ')
|
||||
|
||||
if body.find('>') == -1 or body.rfind("<") == -1:
|
||||
if body.find('>') == -1 or body.rfind('<') == -1:
|
||||
return body
|
||||
|
||||
# logger.debug(u'BODY start.: ' + body[:250])
|
||||
@@ -42,11 +44,11 @@ def replace_br_with_p(body):
|
||||
# tag in all cases now should be div, to just strip the first and
|
||||
# last tags.
|
||||
if is_valid_block(body) and body.find('<div') == 0:
|
||||
body = body[body.index('>')+1:body.rindex("<")]
|
||||
body = body[body.index('>')+1:body.rindex('<')]
|
||||
|
||||
body = soup_up_div(u'<div>' + body + u'</div>')
|
||||
|
||||
body = body[body.index('>')+1:body.rindex("<")]
|
||||
body = body[body.index('>')+1:body.rindex('<')]
|
||||
|
||||
# Find all bexisting blocks with p, pre and blockquote tags, we need to shields break tags inside those.
|
||||
# This is for "lenient" mode, however it is also used to clear break tags before and after the block elements.
|
||||
@@ -135,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:
|
||||
@@ -169,14 +174,15 @@ def replace_br_with_p(body):
|
||||
# Find all instances of consecutive breaks less than otr equal to the max count use most often
|
||||
# replase those tags to inverted p tag pairs, those with more connsecutive breaks are replaced them with a horisontal line
|
||||
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)
|
||||
@@ -223,10 +229,11 @@ def replace_br_with_p(body):
|
||||
# re-wrap in div tag.
|
||||
body = u'<div>\n' + body + u'</div>\n'
|
||||
|
||||
return body
|
||||
# return 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']
|
||||
@@ -243,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:
|
||||
@@ -281,3 +288,61 @@ def soup_up_div(body):
|
||||
body = body.replace(u'[br /]', u'<br />')
|
||||
|
||||
return tag + body + tagend
|
||||
|
||||
|
||||
def is_end_tag(tag):
|
||||
return re.match(r'</([^\ >]+)>', tag) != None
|
||||
|
||||
def is_comment_tag(tag):
|
||||
return re.match(r'<\!\-\-([^>]+)>', tag) != None
|
||||
|
||||
def is_closed_tag(tag):
|
||||
return re.match(r'<(.+?)/>', tag) != None
|
||||
|
||||
def tag_sanitizer(html):
|
||||
blockTags = ['address', 'blockquote', 'del', 'div', 'dl', 'fieldset', 'form', 'ins', 'noscript', 'ol', 'pre', 'table', 'ul']
|
||||
|
||||
body = u''
|
||||
tags = re.findall(r'(<[^>]+>)([^<]*)', html)
|
||||
|
||||
for rTag in tags:
|
||||
name = stack.get_tag_name(rTag[0])
|
||||
is_end = is_end_tag(rTag[0])
|
||||
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, unicode(is_end), unicode(is_closed), unicode(is_comment)))
|
||||
# logger.debug(u'> %s%s\n'%(rTag[0], rTag[1]))
|
||||
|
||||
if name in blockTags:
|
||||
body += rTag[0]
|
||||
body += rTag[1]
|
||||
elif name == u'p':
|
||||
if is_end:
|
||||
body += stack.spool_end()
|
||||
body += rTag[0]
|
||||
body += rTag[1]
|
||||
elif is_closed:
|
||||
body += rTag[0]
|
||||
body += rTag[1]
|
||||
else:
|
||||
body += rTag[0]
|
||||
body += stack.spool_start()
|
||||
body += rTag[1]
|
||||
else:
|
||||
if is_end:
|
||||
t = stack.get_last()
|
||||
tn = stack.get_tag_name(t)
|
||||
rTn = stack.get_tag_name(rTag[0])
|
||||
if tn == rTn:
|
||||
body += rTag[0]
|
||||
stack.pop()
|
||||
elif not is_closed:
|
||||
stack.push(rTag[0])
|
||||
body += rTag[0]
|
||||
else:
|
||||
body += rTag[0]
|
||||
|
||||
body += rTag[1]
|
||||
stack.flush()
|
||||
return body
|
||||
|
||||
+284
-77
@@ -28,9 +28,12 @@ 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
|
||||
|
||||
|
||||
imagetypes = {
|
||||
'jpg':'image/jpeg',
|
||||
'jpeg':'image/jpeg',
|
||||
@@ -48,14 +51,14 @@ try:
|
||||
export = False
|
||||
img = Image()
|
||||
img.load(data)
|
||||
|
||||
|
||||
owidth, oheight = img.size
|
||||
nwidth, nheight = sizes
|
||||
scaled, nwidth, nheight = fit_image(owidth, oheight, nwidth, nheight)
|
||||
if scaled:
|
||||
img.size = (nwidth, nheight)
|
||||
export = True
|
||||
|
||||
|
||||
if normalize_format_name(img.format) != imgtype:
|
||||
export = True
|
||||
|
||||
@@ -65,7 +68,7 @@ try:
|
||||
canvas.compose(img)
|
||||
img = canvas
|
||||
export = True
|
||||
|
||||
|
||||
if grayscale and img.type != "GrayscaleType":
|
||||
img.type = "GrayscaleType"
|
||||
export = True
|
||||
@@ -75,7 +78,7 @@ try:
|
||||
else:
|
||||
logger.debug("image used unchanged")
|
||||
return (data,imgtype,imagetypes[imgtype])
|
||||
|
||||
|
||||
except:
|
||||
|
||||
# No calibre routines, try for PIL for CLI.
|
||||
@@ -87,14 +90,14 @@ except:
|
||||
removetrans,imgtype="jpg",background='#ffffff'):
|
||||
export = False
|
||||
img = Image.open(StringIO(data))
|
||||
|
||||
|
||||
owidth, oheight = img.size
|
||||
nwidth, nheight = sizes
|
||||
scaled, nwidth, nheight = fit_image(owidth, oheight, nwidth, nheight)
|
||||
if scaled:
|
||||
img = img.resize((nwidth, nheight),Image.ANTIALIAS)
|
||||
export = True
|
||||
|
||||
|
||||
if normalize_format_name(img.format) != imgtype:
|
||||
if img.mode == "P":
|
||||
# convert pallete gifs to RGB so jpg save doesn't fail.
|
||||
@@ -119,7 +122,7 @@ except:
|
||||
else:
|
||||
logger.debug("image used unchanged")
|
||||
return (data,imgtype,imagetypes[imgtype])
|
||||
|
||||
|
||||
except:
|
||||
# No calibre or PIL, simple pass through with mimetype.
|
||||
def convert_image(url,data,sizes,grayscale,
|
||||
@@ -129,16 +132,16 @@ except:
|
||||
## also used for explicit no image processing.
|
||||
def no_convert_image(url,data):
|
||||
parsedUrl = up.urlparse(url)
|
||||
|
||||
|
||||
ext=parsedUrl.path[parsedUrl.path.rfind('.')+1:].lower()
|
||||
|
||||
|
||||
if ext not in imagetypes:
|
||||
logger.debug("no_convert_image url:%s - no known extension"%url)
|
||||
# doesn't have extension? use jpg.
|
||||
ext='jpg'
|
||||
|
||||
|
||||
return (data,ext,imagetypes[ext])
|
||||
|
||||
|
||||
def normalize_format_name(fmt):
|
||||
if fmt:
|
||||
fmt = fmt.lower()
|
||||
@@ -221,8 +224,67 @@ 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):
|
||||
Configurable.__init__(self, configuration)
|
||||
try:
|
||||
@@ -231,18 +293,18 @@ 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 = []
|
||||
|
||||
|
||||
self.cover=None # *href* of new cover image--need to create html.
|
||||
self.oldcover=None # (oldcoverhtmlhref,oldcoverhtmltype,oldcoverhtmldata,oldcoverimghref,oldcoverimgtype,oldcoverimgdata)
|
||||
self.calibrebookmark=None # cheesy way to carry calibre bookmark file forward across update.
|
||||
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"),
|
||||
@@ -251,24 +313,93 @@ class Story(Configurable):
|
||||
self.addToList(metadata,val)
|
||||
|
||||
self.setReplace(self.getConfig('replace_metadata'))
|
||||
|
||||
|
||||
in_ex_clude_list = ['include_metadata_pre','exclude_metadata_pre',
|
||||
'include_metadata_post','exclude_metadata_post']
|
||||
for ie in in_ex_clude_list:
|
||||
ies = self.getConfig(ie)
|
||||
# print("%s %s"%(ie,ies))
|
||||
if ies:
|
||||
iel = []
|
||||
self.in_ex_cludes[ie] = self.set_in_ex_clude(ies)
|
||||
|
||||
def join_list(self, key, vallist):
|
||||
return self.getConfig("join_string_"+key,u", ").replace(SPACE_REPLACE,' ').join(map(unicode, vallist))
|
||||
|
||||
def setMetadata(self, key, value, condremoveentities=True):
|
||||
## still keeps < < and &
|
||||
if condremoveentities:
|
||||
self.metadata[key]=conditionalRemoveEntities(value)
|
||||
|
||||
# keep as list type, but set as only value.
|
||||
if self.isList(key):
|
||||
self.addToList(key,value,condremoveentities=condremoveentities,clear=True)
|
||||
else:
|
||||
self.metadata[key]=value
|
||||
## still keeps < < and &
|
||||
if condremoveentities:
|
||||
self.metadata[key]=conditionalRemoveEntities(value)
|
||||
else:
|
||||
self.metadata[key]=value
|
||||
|
||||
if key == "language":
|
||||
try:
|
||||
# getMetadata not just self.metadata[] to do replace_metadata.
|
||||
self.metadata['langcode'] = langs[self.getMetadata(key)]
|
||||
self.setMetadata('langcode',langs[self.getMetadata(key)])
|
||||
except:
|
||||
self.metadata['langcode'] = 'en'
|
||||
if key == 'dateUpdated':
|
||||
self.setMetadata('langcode','en')
|
||||
|
||||
if key == 'dateUpdated' and value:
|
||||
# Last Update tags for Bill.
|
||||
self.addToList('lastupdate',value.strftime("Last Update Year/Month: %Y/%m"))
|
||||
self.addToList('lastupdate',value.strftime("Last Update: %Y/%m/%d"))
|
||||
|
||||
|
||||
## metakey[,metakey]=~pattern
|
||||
## metakey[,metakey]==string
|
||||
## *for* part lines. Effect only when trailing conditional key=~regexp matches
|
||||
## metakey[,metakey]=~pattern[&&metakey=~regexp]
|
||||
## metakey[,metakey]==string[&&metakey=~regexp]
|
||||
## metakey[,metakey]=~pattern[&&metakey==string]
|
||||
## metakey[,metakey]==string[&&metakey==string]
|
||||
def set_in_ex_clude(self,setting):
|
||||
dest = []
|
||||
# print("set_in_ex_clude:"+setting)
|
||||
for line in setting.splitlines():
|
||||
if line:
|
||||
(match,condmatch)=(None,None)
|
||||
if "&&" in line:
|
||||
(line,conditional) = line.split("&&")
|
||||
condmatch = InExMatch(conditional)
|
||||
match = InExMatch(line)
|
||||
dest.append([match,condmatch])
|
||||
return dest
|
||||
|
||||
def do_in_ex_clude(self,which,value,key):
|
||||
if value and which in self.in_ex_cludes:
|
||||
include = 'include' in which
|
||||
keyfound = False
|
||||
found = False
|
||||
for (match,condmatch) in self.in_ex_cludes[which]:
|
||||
keyfndnow = False
|
||||
if match.in_keys(key):
|
||||
# key in keys and either no conditional, or conditional matched
|
||||
if condmatch == None or condmatch.is_key(key):
|
||||
keyfndnow = True
|
||||
else:
|
||||
condval = self.getMetadata(condmatch.key())
|
||||
keyfndnow = condmatch.is_match(condval)
|
||||
keyfound |= keyfndnow
|
||||
# print("match:%s %s\ncondmatch:%s %s\n\tkeyfound:%s\n\tfound:%s"%(
|
||||
# match,value,condmatch,condval,keyfound,found))
|
||||
if keyfndnow:
|
||||
found = isinstance(value,basestring) and match.is_match(value)
|
||||
if found:
|
||||
# print("match:%s %s\n\tkeyfndnow:%s\n\tfound:%s"%(
|
||||
# match,value,keyfndnow,found))
|
||||
if not include:
|
||||
value = None
|
||||
break
|
||||
if include and keyfound and not found:
|
||||
value = None
|
||||
return value
|
||||
|
||||
|
||||
## Two or three part lines. Two part effect everything.
|
||||
## Three part effect only those key(s) lists.
|
||||
@@ -278,6 +409,7 @@ class Story(Configurable):
|
||||
## metakey[,metakey]=>pattern=>replacement[&&metakey=>regexp]
|
||||
def setReplace(self,replace):
|
||||
for line in replace.splitlines():
|
||||
# print("replacement line:%s"%line)
|
||||
(metakeys,regexp,replacement,condkey,condregexp)=(None,None,None,None,None)
|
||||
if "&&" in line:
|
||||
(line,conditional) = line.split("&&")
|
||||
@@ -297,11 +429,20 @@ class Story(Configurable):
|
||||
# A way to explicitly include spaces in the
|
||||
# replacement string. The .ini parser eats any
|
||||
# trailing spaces.
|
||||
replacement=replacement.replace('\s',' ')
|
||||
replacement=replacement.replace(SPACE_REPLACE,' ')
|
||||
self.replacements.append([metakeys,regexp,replacement,condkey,condregexp])
|
||||
|
||||
def doReplacements(self,value,key):
|
||||
for (metakeys,regexp,replacement,condkey,condregexp) in self.replacements:
|
||||
|
||||
def doReplacements(self,value,key,return_list=False,seen_list=[]):
|
||||
value = self.do_in_ex_clude('include_metadata_pre',value,key)
|
||||
value = self.do_in_ex_clude('exclude_metadata_pre',value,key)
|
||||
|
||||
retlist = [value]
|
||||
for replaceline in self.replacements:
|
||||
if replaceline in seen_list: # recursion on pattern, bail
|
||||
# print("bailing on %s"%replaceline)
|
||||
continue
|
||||
#print("replacement tuple:%s"%replaceline)
|
||||
(metakeys,regexp,replacement,condkey,condregexp) = replaceline
|
||||
if (metakeys == None or key in metakeys) \
|
||||
and isinstance(value,basestring) \
|
||||
and regexp.search(value):
|
||||
@@ -309,11 +450,37 @@ class Story(Configurable):
|
||||
if condkey and condkey != key: # prevent infinite recursion.
|
||||
condval = self.getMetadata(condkey)
|
||||
doreplace = condval != None and condregexp.search(condval)
|
||||
|
||||
|
||||
if doreplace:
|
||||
value = regexp.sub(replacement,value)
|
||||
return value
|
||||
|
||||
# split into more than one list entry if
|
||||
# SPLIT_META present in replacement string. Split
|
||||
# first, then regex sub, then recurse call replace
|
||||
# on each. Break out of loop, each split element
|
||||
# handled individually by recursion call.
|
||||
if SPLIT_META in replacement:
|
||||
retlist = []
|
||||
for splitrepl in replacement.split(SPLIT_META):
|
||||
retlist.extend(self.doReplacements(regexp.sub(splitrepl,value),
|
||||
key,
|
||||
return_list=True,
|
||||
seen_list=seen_list+[replaceline]))
|
||||
break
|
||||
else:
|
||||
# print("replacement,value:%s,%s->%s"%(replacement,value,regexp.sub(replacement,value)))
|
||||
value = regexp.sub(replacement,value)
|
||||
retlist = [value]
|
||||
|
||||
for val in retlist:
|
||||
retlist = map(partial(self.do_in_ex_clude,'include_metadata_post',key=key),retlist)
|
||||
retlist = map(partial(self.do_in_ex_clude,'exclude_metadata_post',key=key),retlist)
|
||||
# value = self.do_in_ex_clude('include_metadata_post',value,key)
|
||||
# value = self.do_in_ex_clude('exclude_metadata_post',value,key)
|
||||
|
||||
if return_list:
|
||||
return retlist
|
||||
else:
|
||||
return self.join_list(key,retlist)
|
||||
|
||||
def getMetadataRaw(self,key):
|
||||
if self.isValidMetaEntry(key) and self.metadata.has_key(key):
|
||||
return self.metadata[key]
|
||||
@@ -326,8 +493,9 @@ class Story(Configurable):
|
||||
return value
|
||||
|
||||
if self.isList(key):
|
||||
join_string = self.getConfig("join_string_"+key,u", ").replace('\s',' ')
|
||||
value = join_string.join(self.getList(key, removeallentities, doreplacements=True))
|
||||
# join_string = self.getConfig("join_string_"+key,u", ").replace(SPACE_REPLACE,' ')
|
||||
# value = join_string.join(self.getList(key, removeallentities, doreplacements=True))
|
||||
value = self.join_list(key,self.getList(key, removeallentities, doreplacements=True))
|
||||
if doreplacements:
|
||||
value = self.doReplacements(value,key+"_LIST")
|
||||
return value
|
||||
@@ -351,22 +519,23 @@ class Story(Configurable):
|
||||
return value
|
||||
else: #if self.getConfig("default_value_"+key):
|
||||
return self.getConfig("default_value_"+key)
|
||||
|
||||
|
||||
def getAllMetadata(self,
|
||||
removeallentities=False,
|
||||
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 = {}
|
||||
|
||||
|
||||
# special handling for authors/authorUrls
|
||||
linkhtml="<a class='%slink' href='%s'>%s</a>"
|
||||
if self.isList('author'): # more than one author, assume multiple authorUrl too.
|
||||
htmllist=[]
|
||||
for i, v in enumerate(self.getList('author')):
|
||||
aurl = self.getList('authorUrl')[i]
|
||||
aurl = self.getList('authorUrl')[i]
|
||||
auth = v
|
||||
# make sure doreplacements & removeallentities are honored.
|
||||
if doreplacements:
|
||||
@@ -375,38 +544,62 @@ class Story(Configurable):
|
||||
if removeallentities:
|
||||
aurl=removeAllEntities(aurl)
|
||||
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. 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]
|
||||
# make sure doreplacements & removeallentities are honored.
|
||||
if doreplacements:
|
||||
url=self.doReplacements(url,k+'Url')
|
||||
v=self.doReplacements(v,k)
|
||||
if removeallentities:
|
||||
url=removeAllEntities(url)
|
||||
v=removeAllEntities(v)
|
||||
|
||||
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:
|
||||
allmetadata[k] = self.getList(k, removeallentities, doreplacements)
|
||||
else:
|
||||
allmetadata[k] = self.getMetadata(k, removeallentities, doreplacements)
|
||||
|
||||
|
||||
return allmetadata
|
||||
|
||||
# just for less clutter in adapters.
|
||||
def extendList(self,listname,l):
|
||||
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.
|
||||
@@ -418,27 +611,27 @@ 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) )
|
||||
|
||||
|
||||
def getList(self,listname,
|
||||
removeallentities=False,
|
||||
doreplacements=True,
|
||||
includelist=[]):
|
||||
#print("getList(%s,%s)"%(listname,includelist))
|
||||
retlist = []
|
||||
|
||||
|
||||
if not self.isValidMetaEntry(listname):
|
||||
return retlist
|
||||
|
||||
|
||||
# includelist prevents infinite recursion of include_in_'s
|
||||
if self.hasConfig("include_in_"+listname) and listname not in includelist:
|
||||
for k in self.getConfigList("include_in_"+listname):
|
||||
retlist.extend(self.getList(k,removeallentities=False,
|
||||
doreplacements=doreplacements,includelist=includelist+[listname]))
|
||||
else:
|
||||
|
||||
|
||||
if not self.isList(listname):
|
||||
retlist = [self.getMetadata(listname,removeallentities=False,
|
||||
doreplacements=doreplacements)]
|
||||
@@ -447,18 +640,22 @@ 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:
|
||||
if listname in ('author','authorUrl','authorId') or self.getConfig('keep_in_order_'+listname):
|
||||
# need to retain order for author & authorUrl so the
|
||||
@@ -473,9 +670,9 @@ class Story(Configurable):
|
||||
def getSubjectTags(self, removeallentities=False):
|
||||
# set to avoid duplicates subject tags.
|
||||
subjectset = set()
|
||||
|
||||
|
||||
tags_list = self.getConfigList("include_subject_tags") + self.getConfigList("extra_subject_tags")
|
||||
|
||||
|
||||
# metadata all go into dc:subject tags, but only if they are configured.
|
||||
for (name,value) in self.getAllMetadata(removeallentities=removeallentities,keeplists=True).iteritems():
|
||||
if name in tags_list:
|
||||
@@ -491,7 +688,7 @@ class Story(Configurable):
|
||||
subjectset.remove('')
|
||||
|
||||
return list(subjectset | set(self.getConfigList("extratags")))
|
||||
|
||||
|
||||
def addChapter(self, url, title, html):
|
||||
if self.getConfig('strip_chapter_numbers') and \
|
||||
self.getConfig('chapter_title_strip_pattern'):
|
||||
@@ -512,7 +709,7 @@ class Story(Configurable):
|
||||
html) )
|
||||
else:
|
||||
retval = self.chapters
|
||||
|
||||
|
||||
return retval
|
||||
|
||||
def formatFileName(self,template,allowunsafefilename=True):
|
||||
@@ -526,7 +723,7 @@ class Story(Configurable):
|
||||
pattern = re.compile(self.getConfig("output_filename_safepattern",r"[^a-zA-Z0-9_\. \[\]\(\)&'-]+"))
|
||||
for k in origvalues.keys():
|
||||
values[k]=re.sub(pattern,'_', removeAllEntities(self.getMetadata(k)))
|
||||
|
||||
|
||||
return string.Template(template).substitute(values).encode('utf8')
|
||||
|
||||
# pass fetch in from adapter in case we need the cookies collected
|
||||
@@ -537,7 +734,7 @@ class Story(Configurable):
|
||||
# isn't used anywhere.
|
||||
if cover and self.getConfig('never_make_cover'):
|
||||
return
|
||||
|
||||
|
||||
url = url.strip() # ran across an image with a space in the
|
||||
# src. Browser handled it, so we'd better, too.
|
||||
|
||||
@@ -545,12 +742,18 @@ class Story(Configurable):
|
||||
# gets too big too fast and breaks things.
|
||||
if is_appengine:
|
||||
return
|
||||
|
||||
|
||||
if url.startswith("http") or url.startswith("file") or parenturl == None:
|
||||
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,
|
||||
@@ -569,6 +772,10 @@ class Story(Configurable):
|
||||
'','',''))
|
||||
#print("\n===========\nparsedUrl.path:%s\ntoppath:%s\nimgurl:%s\n\n"%(parsedUrl.path,toppath,imgurl))
|
||||
|
||||
# apply coverexclusion to explicit covers, too. Primarily for ffnet imageu.
|
||||
if cover and coverexclusion and re.search(coverexclusion,imgurl):
|
||||
return
|
||||
|
||||
prefix='ffdl'
|
||||
if imgurl not in self.imgurls:
|
||||
parsedUrl = urlparse.urlparse(imgurl)
|
||||
@@ -598,9 +805,9 @@ class Story(Configurable):
|
||||
except Exception, e:
|
||||
logger.info("Failed to load or convert image, skipping:\n%s\nException: %s"%(imgurl,e))
|
||||
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]
|
||||
@@ -619,24 +826,24 @@ 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})
|
||||
self.imgurls.append(imgurl)
|
||||
|
||||
|
||||
newsrc = "images/%s-%s.%s"%(
|
||||
prefix,
|
||||
self.imgurls.index(imgurl),
|
||||
ext)
|
||||
self.imgtuples.append({'newsrc':newsrc,'mime':mime,'data':data})
|
||||
|
||||
|
||||
#logger.debug("\nimgurl:%s\nnewsrc:%s\nimage size:%d\n"%(imgurl,newsrc,len(data)))
|
||||
else:
|
||||
newsrc = self.imgtuples[self.imgurls.index(imgurl)]['newsrc']
|
||||
|
||||
|
||||
#print("===============\n%s\nimg url:%s\n============"%(newsrc,self.imgurls[-1]))
|
||||
|
||||
|
||||
return newsrc
|
||||
|
||||
def getImgUrls(self):
|
||||
@@ -645,9 +852,9 @@ class Story(Configurable):
|
||||
#parsedUrl = urlparse.urlparse(url)
|
||||
retlist.append(self.imgtuples[i])
|
||||
return retlist
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return "Metadata: " +str(self.metadata)
|
||||
return "Metadata: " +str(self.metadata)
|
||||
|
||||
def commaGroups(s):
|
||||
groups = []
|
||||
|
||||
@@ -361,7 +361,7 @@ div { margin: 0pt; padding: 0pt; }
|
||||
|
||||
metadata.appendChild(newTag(contentdom,"dc:contributor",text="fanficdownloader [http://fanficdownloader.googlecode.com]",attrs={"opf:role":"bkp"}))
|
||||
metadata.appendChild(newTag(contentdom,"dc:rights",text=""))
|
||||
if self.story.getMetadata('langcode') != None:
|
||||
if self.story.getMetadata('langcode'):
|
||||
metadata.appendChild(newTag(contentdom,"dc:language",text=self.story.getMetadata('langcode')))
|
||||
else:
|
||||
metadata.appendChild(newTag(contentdom,"dc:language",text='en'))
|
||||
|
||||
+7
-14
@@ -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,12 +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>Additional fixes/enhancements for replace_br_with_p heuristic processing feature. Turn on in personal User Config. Thanks, Asbjørn Grandt.</li>
|
||||
<li>Fix for AO3 authors all coming out as Anonymous.</li>
|
||||
<li>Known issue: Password protected FimFiction.net stories aren't working. FimF changed API access.</li>
|
||||
<li>Known issue: Specific metadata 'eroticatags' for literotica.com doesn't work on all stories.</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Questions? Check out our
|
||||
<a href="http://code.google.com/p/fanficdownloader/wiki/FanFictionDownloaderFAQs">FAQs</a>.
|
||||
@@ -80,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-81.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
|
||||
<a href="http://2-0-01.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
|
||||
</p>
|
||||
<div id='error'>
|
||||
{{ error_message }}
|
||||
|
||||
+1
-3
@@ -1,9 +1,7 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# epubmerge.py 1.0
|
||||
|
||||
# Copyright 2011, Jim Miller
|
||||
# Copyright 2014, Jim Miller
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
||||
+493
-206
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