mirror of
https://github.com/wassname/FanFicFare.git
synced 2026-09-13 12:11:20 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eebb1ee8d0 | ||
|
|
bda307ed3d | ||
|
|
6b0c519f99 | ||
|
|
6533ee0187 | ||
|
|
3cc3b32012 | ||
|
|
315536a75b | ||
|
|
1c4250c0c1 | ||
|
|
6f7e700cd2 | ||
|
|
a593bd201c | ||
|
|
37885a9039 | ||
|
|
97437aa7da | ||
|
|
ccfcd801a3 | ||
|
|
4df321c18e | ||
|
|
664fb639bd | ||
|
|
0cd71263b1 | ||
|
|
2fc1a14ac4 | ||
|
|
8772b50451 | ||
|
|
7390424f0f | ||
|
|
3a16c49c2e | ||
|
|
d8066d4bcf | ||
|
|
61fff980b5 | ||
|
|
bf704fcdc7 | ||
|
|
c9f86bd784 | ||
|
|
76faea3b4e | ||
|
|
6b3a19bb45 | ||
|
|
df7bf64845 | ||
|
|
6b8ccc0073 | ||
|
|
2083a79464 | ||
|
|
c0a962bd9d | ||
|
|
66b33369c1 | ||
|
|
82d28f26f4 | ||
|
|
ff6cd7ccf1 | ||
|
|
7d8691171e | ||
|
|
329c55d8ed | ||
|
|
e68d2484a6 | ||
|
|
eb5f10f5c1 | ||
|
|
0161991c2a | ||
|
|
be0d48ec7b | ||
|
|
cf54f274d4 | ||
|
|
bffc389bcf | ||
|
|
6de973fe2d | ||
|
|
67b44a991b | ||
|
|
13b0aa358a | ||
|
|
047a03c61c | ||
|
|
b5cc612f96 | ||
|
|
df836412cc | ||
|
|
a28b8cb139 | ||
|
|
03ceecd38f | ||
|
|
f5d511a996 | ||
|
|
1724c6f42f | ||
|
|
58ad4c0381 | ||
|
|
b1c9fd0e30 | ||
|
|
b8f168add6 | ||
|
|
20e90a5cd5 | ||
|
|
dd4a22e7d8 | ||
|
|
58033a1afa | ||
|
|
1fd6913dfb | ||
|
|
9ccdc4d884 | ||
|
|
12dd969560 | ||
|
|
0af5e1e9b1 | ||
|
|
4a57d95eb2 | ||
|
|
8e9870d4fd | ||
|
|
30bafd4e53 | ||
|
|
c14c52f670 | ||
|
|
664001c35c | ||
|
|
572f4e4c7f | ||
|
|
3ccfa1086a | ||
|
|
2f0e431e35 | ||
|
|
8730e88658 | ||
|
|
bf277ac005 | ||
|
|
1fa2bf356f | ||
|
|
90891a9a52 | ||
|
|
0a2d4c2aca | ||
|
|
cbd8d9c34b | ||
|
|
7e07be9ff7 | ||
|
|
35efd0fe98 | ||
|
|
ca5077b9ef | ||
|
|
a167ec4c59 | ||
|
|
44e12d1ef3 | ||
|
|
83c8987b8b | ||
|
|
2e01380b5d | ||
|
|
d221adabbb | ||
|
|
7d831b9cdc | ||
|
|
f090485369 | ||
|
|
2af3a44e11 | ||
|
|
70d7253dac | ||
|
|
be4e3610d5 | ||
|
|
69507816a4 | ||
|
|
20a789566f | ||
|
|
138de3ac3a | ||
|
|
f2960a8db4 | ||
|
|
3dd6550882 | ||
|
|
9f511dad8d | ||
|
|
7a08e3afdd | ||
|
|
a644beea94 | ||
|
|
b275007393 | ||
|
|
13602b023d | ||
|
|
1be99aa95c | ||
|
|
4bf3399e35 | ||
|
|
35c066ea65 | ||
|
|
b25a869185 | ||
|
|
22e916bda9 | ||
|
|
784375d15e | ||
|
|
18fd7d3653 | ||
|
|
34333a1c48 | ||
|
|
4069b1d15d | ||
|
|
b962059e4c | ||
|
|
679fcc9d47 |
@@ -7,15 +7,21 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2016, Jim Miller'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import sys
|
||||
import sys, os
|
||||
if sys.version_info >= (2, 7):
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
loghandler=logging.StreamHandler()
|
||||
loghandler.setFormatter(logging.Formatter("FFF:%(levelname)s:%(filename)s(%(lineno)d):%(message)s"))
|
||||
loghandler.setFormatter(logging.Formatter("FFF: %(levelname)s: %(asctime)s: %(filename)s(%(lineno)d): %(message)s"))
|
||||
logger.addHandler(loghandler)
|
||||
loghandler.setLevel(logging.DEBUG)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
from calibre.constants import DEBUG
|
||||
if os.environ.get('CALIBRE_WORKER', None) is not None or DEBUG:
|
||||
loghandler.setLevel(logging.DEBUG)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
loghandler.setLevel(logging.CRITICAL)
|
||||
logger.setLevel(logging.CRITICAL)
|
||||
|
||||
# pulls in translation files for _() strings
|
||||
try:
|
||||
@@ -42,7 +48,7 @@ class FanFicFareBase(InterfaceActionBase):
|
||||
description = _('UI plugin to download FanFiction stories from various sites.')
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
author = 'Jim Miller'
|
||||
version = (2, 2, 17)
|
||||
version = (2, 3, 3)
|
||||
minimum_calibre_version = (1, 48, 0)
|
||||
|
||||
#: This field defines the GUI plugin class that contains all the code
|
||||
|
||||
+148
-54
@@ -4,13 +4,13 @@ from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2015, Jim Miller'
|
||||
__copyright__ = '2016, Jim Miller'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
import traceback, copy, threading
|
||||
import traceback, copy, threading, re
|
||||
from collections import OrderedDict
|
||||
|
||||
try:
|
||||
@@ -81,8 +81,8 @@ no_trans = { 'pini':'personal.ini',
|
||||
STD_COLS_SKIP = ['size','cover','news','ondevice','path','series_sort','sort']
|
||||
|
||||
from calibre_plugins.fanficfare_plugin.prefs \
|
||||
import (prefs, PREFS_NAMESPACE, updatecalcover_order, calcover_save_options,
|
||||
gencalcover_order, SAVE_YES, SAVE_NO)
|
||||
import (prefs, PREFS_NAMESPACE, prefs_save_options, updatecalcover_order,
|
||||
gencalcover_order, do_wordcount_order, SAVE_YES, SAVE_NO)
|
||||
|
||||
from calibre_plugins.fanficfare_plugin.dialogs \
|
||||
import (UPDATE, UPDATEALWAYS, collision_order, save_collisions, RejectListDialog,
|
||||
@@ -261,6 +261,7 @@ class ConfigWidget(QWidget):
|
||||
prefs['checkforurlchange'] = self.basic_tab.checkforurlchange.isChecked()
|
||||
prefs['injectseries'] = self.basic_tab.injectseries.isChecked()
|
||||
prefs['matchtitleauth'] = self.basic_tab.matchtitleauth.isChecked()
|
||||
prefs['do_wordcount'] = prefs_save_options[unicode(self.basic_tab.do_wordcount.currentText())]
|
||||
prefs['smarten_punctuation'] = self.basic_tab.smarten_punctuation.isChecked()
|
||||
prefs['reject_always'] = self.basic_tab.reject_always.isChecked()
|
||||
|
||||
@@ -286,10 +287,10 @@ class ConfigWidget(QWidget):
|
||||
prefs['cal_cols_pass_in'] = self.personalini_tab.cal_cols_pass_in.isChecked()
|
||||
|
||||
# Covers tab
|
||||
prefs['updatecalcover'] = calcover_save_options[unicode(self.calibrecover_tab.updatecalcover.currentText())]
|
||||
prefs['updatecalcover'] = prefs_save_options[unicode(self.calibrecover_tab.updatecalcover.currentText())]
|
||||
# for backward compatibility:
|
||||
prefs['updatecover'] = prefs['updatecalcover'] == SAVE_YES
|
||||
prefs['gencalcover'] = calcover_save_options[unicode(self.calibrecover_tab.gencalcover.currentText())]
|
||||
prefs['gencalcover'] = prefs_save_options[unicode(self.calibrecover_tab.gencalcover.currentText())]
|
||||
prefs['calibre_gen_cover'] = self.calibrecover_tab.calibre_gen_cover.isChecked()
|
||||
prefs['plugin_gen_cover'] = self.calibrecover_tab.plugin_gen_cover.isChecked()
|
||||
prefs['gcnewonly'] = self.calibrecover_tab.gcnewonly.isChecked()
|
||||
@@ -331,6 +332,7 @@ class ConfigWidget(QWidget):
|
||||
# Custom Columns tab
|
||||
# error column
|
||||
prefs['errorcol'] = unicode(convert_qvariant(self.cust_columns_tab.errorcol.itemData(self.cust_columns_tab.errorcol.currentIndex())))
|
||||
prefs['save_all_errors'] = self.cust_columns_tab.save_all_errors.isChecked()
|
||||
|
||||
# metadata column
|
||||
prefs['savemetacol'] = unicode(convert_qvariant(self.cust_columns_tab.savemetacol.itemData(self.cust_columns_tab.savemetacol.currentIndex())))
|
||||
@@ -478,6 +480,10 @@ class BasicTab(QWidget):
|
||||
self.lookforurlinhtml.setChecked(prefs['lookforurlinhtml'])
|
||||
self.l.addWidget(self.lookforurlinhtml)
|
||||
|
||||
proc_gb = groupbox = QGroupBox(_("Post Processing Options"))
|
||||
self.l = QVBoxLayout()
|
||||
groupbox.setLayout(self.l)
|
||||
|
||||
self.mark = QCheckBox(_("Mark added/updated books when finished?"),self)
|
||||
self.mark.setToolTip(_("Mark added/updated books when finished. Use with option below.\nYou can also manually search for 'marked:fff_success'.\n'marked:fff_failed' is also available, or search 'marked:fff' for both."))
|
||||
self.mark.setChecked(prefs['mark'])
|
||||
@@ -493,6 +499,24 @@ class BasicTab(QWidget):
|
||||
self.smarten_punctuation.setChecked(prefs['smarten_punctuation'])
|
||||
self.l.addWidget(self.smarten_punctuation)
|
||||
|
||||
|
||||
tooltip = _("Calculate Word Counts using Calibre internal methods.\n"
|
||||
"Many sites include Word Count, but many do not.\n"
|
||||
"This will count the words in each book and include it as if it came from the site.")
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel(_('Calculate Word Count:'))
|
||||
label.setToolTip(tooltip)
|
||||
horz.addWidget(label)
|
||||
self.do_wordcount = QComboBox(self)
|
||||
for i in do_wordcount_order:
|
||||
self.do_wordcount.addItem(i)
|
||||
self.do_wordcount.setCurrentIndex(self.do_wordcount.findText(prefs_save_options[prefs['do_wordcount']]))
|
||||
self.do_wordcount.setToolTip(tooltip)
|
||||
label.setBuddy(self.do_wordcount)
|
||||
horz.addWidget(self.do_wordcount)
|
||||
self.l.addLayout(horz)
|
||||
|
||||
|
||||
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'])
|
||||
@@ -564,14 +588,17 @@ class BasicTab(QWidget):
|
||||
|
||||
horz = QHBoxLayout()
|
||||
|
||||
horz.addWidget(cali_gb)
|
||||
vertleft = QVBoxLayout()
|
||||
vertleft.addWidget(cali_gb)
|
||||
vertleft.addWidget(proc_gb)
|
||||
|
||||
vert = QVBoxLayout()
|
||||
vert.addWidget(gui_gb)
|
||||
vert.addWidget(misc_gb)
|
||||
vert.addWidget(rej_gb)
|
||||
vertright = QVBoxLayout()
|
||||
vertright.addWidget(gui_gb)
|
||||
vertright.addWidget(misc_gb)
|
||||
vertright.addWidget(rej_gb)
|
||||
|
||||
horz.addLayout(vert)
|
||||
horz.addLayout(vertleft)
|
||||
horz.addLayout(vertright)
|
||||
|
||||
topl.addLayout(horz)
|
||||
topl.insertStretch(-1)
|
||||
@@ -639,53 +666,94 @@ class PersonalIniTab(QWidget):
|
||||
label = QLabel(_('These settings provide more detailed control over what metadata will be displayed inside the ebook as well as let you set %(isa)s and %(u)s/%(p)s for different sites.')%no_trans)
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
# self.l.addSpacing(5)
|
||||
|
||||
label = QLabel(_("FanFicFare now includes find, color coding, and error checking for personal.ini editing. Red generally indicates errors."))
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
|
||||
# self.label = QLabel('personal.ini:')
|
||||
# self.l.addWidget(self.label)
|
||||
|
||||
# self.ini = QTextEdit(self)
|
||||
# try:
|
||||
# self.ini.setFont(QFont("Courier",
|
||||
# self.plugin_action.gui.font().pointSize()+1))
|
||||
# except Exception as e:
|
||||
# logger.error("Couldn't get font: %s"%e)
|
||||
# self.ini.setLineWrapMode(QTextEdit.NoWrap)
|
||||
# self.ini.setText(prefs['personal.ini'])
|
||||
# self.l.addWidget(self.ini)
|
||||
self.l.addSpacing(5)
|
||||
|
||||
self.personalini = prefs['personal.ini']
|
||||
|
||||
groupbox = QGroupBox(_("personal.ini"))
|
||||
vert = QVBoxLayout()
|
||||
groupbox.setLayout(vert)
|
||||
self.l.addWidget(groupbox)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
vert.addLayout(horz)
|
||||
self.ini_button = QPushButton(_('Edit personal.ini'), self)
|
||||
#self.ini_button.setToolTip(_("Edit personal.ini file."))
|
||||
self.ini_button.clicked.connect(self.add_ini_button)
|
||||
self.l.addWidget(self.ini_button)
|
||||
horz.addWidget(self.ini_button)
|
||||
|
||||
label = QLabel(_("FanFicFare now includes find, color coding, and error checking for personal.ini editing. Red generally indicates errors."))
|
||||
label.setWordWrap(True)
|
||||
horz.addWidget(label)
|
||||
|
||||
vert.addSpacing(5)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
vert.addLayout(horz)
|
||||
self.ini_button = QPushButton(_('View "Safe" personal.ini'), self)
|
||||
#self.ini_button.setToolTip(_("Edit personal.ini file."))
|
||||
self.ini_button.clicked.connect(self.safe_ini_button)
|
||||
horz.addWidget(self.ini_button)
|
||||
|
||||
label = QLabel(_("View your personal.ini with usernames and passwords removed. For safely sharing your personal.ini settings with others."))
|
||||
label.setWordWrap(True)
|
||||
horz.addWidget(label)
|
||||
|
||||
self.l.addSpacing(5)
|
||||
|
||||
groupbox = QGroupBox(_("defaults.ini"))
|
||||
horz = QHBoxLayout()
|
||||
groupbox.setLayout(horz)
|
||||
self.l.addWidget(groupbox)
|
||||
|
||||
view_label = _("View all of the plugin's configurable settings\nand their default settings.")
|
||||
self.defaults = QPushButton(_('View Defaults')+' (plugin-defaults.ini)', self)
|
||||
self.defaults.setToolTip(view_label)
|
||||
self.defaults.clicked.connect(self.show_defaults)
|
||||
horz.addWidget(self.defaults)
|
||||
|
||||
label = QLabel(view_label)
|
||||
label.setWordWrap(True)
|
||||
horz.addWidget(label)
|
||||
|
||||
self.l.addSpacing(5)
|
||||
|
||||
groupbox = QGroupBox(_("Calibre Columns"))
|
||||
vert = QVBoxLayout()
|
||||
groupbox.setLayout(vert)
|
||||
self.l.addWidget(groupbox)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
vert.addLayout(horz)
|
||||
pass_label = _("If checked, when updating/overwriting an existing book, FanFicFare will have the Calibre Columns available to use in replace_metadata, title_page, etc.<br>Click the button below to see the Calibre Column namess.")%no_trans
|
||||
self.cal_cols_pass_in = QCheckBox(_('Pass Calibre Columns into FanFicFare on Update/Overwrite')%no_trans,self)
|
||||
self.cal_cols_pass_in.setToolTip(pass_label)
|
||||
self.cal_cols_pass_in.setChecked(prefs['cal_cols_pass_in'])
|
||||
horz.addWidget(self.cal_cols_pass_in)
|
||||
|
||||
label = QLabel(pass_label)
|
||||
label.setWordWrap(True)
|
||||
horz.addWidget(label)
|
||||
|
||||
vert.addSpacing(5)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
vert.addLayout(horz)
|
||||
col_label = _("FanFicFare can pass the Calibre Columns into the download/update process.<br>This will show you the columns available by name.")
|
||||
self.showcalcols = QPushButton(_('Show Calibre Column Names'), self)
|
||||
self.showcalcols.setToolTip(col_label)
|
||||
self.showcalcols.clicked.connect(self.show_showcalcols)
|
||||
horz.addWidget(self.showcalcols)
|
||||
|
||||
label = QLabel(col_label)
|
||||
label.setWordWrap(True)
|
||||
horz.addWidget(label)
|
||||
|
||||
label = QLabel(_("Changes will only be saved if you click 'OK' to leave Customize FanFicFare."))
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
|
||||
self.defaults = QPushButton(_('View Defaults')+' (plugin-defaults.ini)', self)
|
||||
self.defaults.setToolTip(_("View all of the plugin's configurable settings\nand their default settings."))
|
||||
self.defaults.clicked.connect(self.show_defaults)
|
||||
self.l.addWidget(self.defaults)
|
||||
|
||||
self.cal_cols_pass_in = QCheckBox(_('Pass Calibre Columns into FanFicFare on Update/Overwrite')%no_trans,self)
|
||||
self.cal_cols_pass_in.setToolTip(_("If checked, when updating/overwriting an existing book, FanFicFare will have the Calibre Columns available to use in replace_metadata, title_page, etc.<br>Click the button below to see the Calibre Column namess.")%no_trans)
|
||||
self.cal_cols_pass_in.setChecked(prefs['cal_cols_pass_in'])
|
||||
self.l.addWidget(self.cal_cols_pass_in)
|
||||
|
||||
self.showcalcols = QPushButton(_('Show Calibre Column Names'), self)
|
||||
self.showcalcols.setToolTip(_("FanFicFare can pass the Calibre Columns into the download/update process.<br>This will show you the columns available by name."))
|
||||
self.showcalcols.clicked.connect(self.show_showcalcols)
|
||||
self.l.addWidget(self.showcalcols)
|
||||
|
||||
self.l.insertStretch(-1)
|
||||
# let edit box fill the space.
|
||||
|
||||
def show_defaults(self):
|
||||
IniTextDialog(self,
|
||||
@@ -697,6 +765,18 @@ class PersonalIniTab(QWidget):
|
||||
read_only=True,
|
||||
save_size_name='fff:defaults.ini').exec_()
|
||||
|
||||
def safe_ini_button(self):
|
||||
personalini = re.sub(r'((username|password) *[=:]).*$',r'\1XXXXXXXX',self.personalini,flags=re.MULTILINE)
|
||||
|
||||
d = EditTextDialog(self,
|
||||
personalini,
|
||||
icon=self.windowIcon(),
|
||||
title=_("View 'Safe' personal.ini"),
|
||||
label=_("View your personal.ini with usernames and passwords removed. For safely sharing your personal.ini settings with others."),
|
||||
save_size_name='fff:safe personal.ini',
|
||||
read_only=True)
|
||||
d.exec_()
|
||||
|
||||
def add_ini_button(self):
|
||||
d = IniTextDialog(self,
|
||||
self.personalini,
|
||||
@@ -840,11 +920,11 @@ class CalibreCoverTab(QWidget):
|
||||
self.updatecalcover.addItem(i)
|
||||
# back compat. If has own value, use.
|
||||
if prefs['updatecalcover']:
|
||||
self.updatecalcover.setCurrentIndex(self.updatecalcover.findText(calcover_save_options[prefs['updatecalcover']]))
|
||||
self.updatecalcover.setCurrentIndex(self.updatecalcover.findText(prefs_save_options[prefs['updatecalcover']]))
|
||||
elif prefs['updatecover']: # doesn't have own val, set YES if old value set.
|
||||
self.updatecalcover.setCurrentIndex(self.updatecalcover.findText(calcover_save_options[SAVE_YES]))
|
||||
self.updatecalcover.setCurrentIndex(self.updatecalcover.findText(prefs_save_options[SAVE_YES]))
|
||||
else: # doesn't have own value, old value not set, NO.
|
||||
self.updatecalcover.setCurrentIndex(self.updatecalcover.findText(calcover_save_options[SAVE_NO]))
|
||||
self.updatecalcover.setCurrentIndex(self.updatecalcover.findText(prefs_save_options[SAVE_NO]))
|
||||
self.updatecalcover.setToolTip(tooltip)
|
||||
label.setBuddy(self.updatecalcover)
|
||||
horz.addWidget(self.updatecalcover)
|
||||
@@ -862,11 +942,11 @@ class CalibreCoverTab(QWidget):
|
||||
self.gencalcover.addItem(i)
|
||||
# back compat. If has own value, use.
|
||||
# if prefs['gencalcover']:
|
||||
self.gencalcover.setCurrentIndex(self.gencalcover.findText(calcover_save_options[prefs['gencalcover']]))
|
||||
self.gencalcover.setCurrentIndex(self.gencalcover.findText(prefs_save_options[prefs['gencalcover']]))
|
||||
# elif prefs['gencover']: # doesn't have own val, set YES if old value set.
|
||||
# self.gencalcover.setCurrentIndex(self.gencalcover.findText(calcover_save_options[SAVE_YES]))
|
||||
# self.gencalcover.setCurrentIndex(self.gencalcover.findText(prefs_save_options[SAVE_YES]))
|
||||
# else: # doesn't have own value, old value not set, NO.
|
||||
# self.gencalcover.setCurrentIndex(self.gencalcover.findText(calcover_save_options[SAVE_NO]))
|
||||
# self.gencalcover.setCurrentIndex(self.gencalcover.findText(prefs_save_options[SAVE_NO]))
|
||||
|
||||
self.gencalcover.setToolTip(tooltip)
|
||||
label.setBuddy(self.gencalcover)
|
||||
@@ -990,7 +1070,7 @@ class CalibreCoverTab(QWidget):
|
||||
|
||||
## First, cover gen on/off
|
||||
for e in self.gencov_elements:
|
||||
e.setEnabled(calcover_save_options[unicode(self.gencalcover.currentText())] != SAVE_NO)
|
||||
e.setEnabled(prefs_save_options[unicode(self.gencalcover.currentText())] != SAVE_NO)
|
||||
|
||||
# next, disable plugin settings when using calibre gen cov.
|
||||
if not self.plugin_gen_cover.isChecked():
|
||||
@@ -1261,6 +1341,7 @@ class CustomColumnsTab(QWidget):
|
||||
tooltip=_("When an update or overwrite of an existing story fails, record the reason in this column.\n(Text and Long Text columns only.)")
|
||||
label.setToolTip(tooltip)
|
||||
horz.addWidget(label)
|
||||
|
||||
self.errorcol = QComboBox(self)
|
||||
self.errorcol.setToolTip(tooltip)
|
||||
self.errorcol.addItem('','none')
|
||||
@@ -1269,6 +1350,15 @@ class CustomColumnsTab(QWidget):
|
||||
self.errorcol.addItem(column['name'],key)
|
||||
self.errorcol.setCurrentIndex(self.errorcol.findData(prefs['errorcol']))
|
||||
horz.addWidget(self.errorcol)
|
||||
|
||||
self.save_all_errors = QCheckBox(_('Save All Errors'),self)
|
||||
self.save_all_errors.setToolTip(_('If unchecked, these errors will not be saved:%s')%(
|
||||
'\n'+
|
||||
'\n'.join((_("Not Overwriting, web site is not newer."),
|
||||
_("Already contains %d chapters.").replace('%d','X')))))
|
||||
self.save_all_errors.setChecked(prefs['save_all_errors'])
|
||||
horz.addWidget(self.save_all_errors)
|
||||
|
||||
self.l.addLayout(horz)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
@@ -1284,6 +1374,10 @@ class CustomColumnsTab(QWidget):
|
||||
self.savemetacol.addItem(column['name'],key)
|
||||
self.savemetacol.setCurrentIndex(self.savemetacol.findData(prefs['savemetacol']))
|
||||
horz.addWidget(self.savemetacol)
|
||||
|
||||
label = QLabel('')
|
||||
horz.addWidget(label) # empty spacer for alignment with error column line.
|
||||
|
||||
self.l.addLayout(horz)
|
||||
|
||||
#print("prefs['custom_cols'] %s"%prefs['custom_cols'])
|
||||
|
||||
+78
-73
@@ -4,7 +4,7 @@ from __future__ import (unicode_literals, division,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2015, Jim Miller'
|
||||
__copyright__ = '2016, Jim Miller'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import traceback, re
|
||||
@@ -21,19 +21,19 @@ from datetime import datetime
|
||||
try:
|
||||
from PyQt5 import QtWidgets as QtGui
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5.Qt import (QDialog, QTableWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
|
||||
QPushButton, QFont, QLabel, QCheckBox, QIcon, QLineEdit,
|
||||
QComboBox, QProgressDialog, QTimer, QDialogButtonBox,
|
||||
QPixmap, Qt, QAbstractItemView, QTextEdit, pyqtSignal,
|
||||
QGroupBox, QFrame, QTextBrowser, QSize, QAction)
|
||||
from PyQt5.Qt import (QDialog, QWidget, QTableWidget, QVBoxLayout, QHBoxLayout,
|
||||
QGridLayout, QPushButton, QFont, QLabel, QCheckBox, QIcon,
|
||||
QLineEdit, QComboBox, QProgressDialog, QTimer, QDialogButtonBox,
|
||||
QScrollArea, QPixmap, Qt, QAbstractItemView, QTextEdit,
|
||||
pyqtSignal, QGroupBox, QFrame, QTextBrowser, QSize, QAction)
|
||||
except ImportError as e:
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4.Qt import (QDialog, QTableWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
|
||||
QPushButton, QFont, QLabel, QCheckBox, QIcon, QLineEdit,
|
||||
QComboBox, QProgressDialog, QTimer, QDialogButtonBox,
|
||||
QPixmap, Qt, QAbstractItemView, QTextEdit, pyqtSignal,
|
||||
QGroupBox, QFrame, QTextBrowser, QSize, QAction)
|
||||
from PyQt4.Qt import (QDialog, QWidget, QTableWidget, QVBoxLayout, QHBoxLayout,
|
||||
QGridLayout, QPushButton, QFont, QLabel, QCheckBox, QIcon,
|
||||
QLineEdit, QComboBox, QProgressDialog, QTimer, QDialogButtonBox,
|
||||
QScrollArea, QPixmap, Qt, QAbstractItemView, QTextEdit,
|
||||
pyqtSignal, QGroupBox, QFrame, QTextBrowser, QSize, QAction)
|
||||
|
||||
try:
|
||||
from calibre.gui2 import QVariant
|
||||
@@ -73,55 +73,30 @@ from calibre_plugins.fanficfare_plugin.fanficfare.configurable \
|
||||
|
||||
from inihighlighter import IniHighlighter
|
||||
|
||||
SKIP=_('Skip')
|
||||
ADDNEW=_('Add New Book')
|
||||
UPDATE=_('Update EPUB if New Chapters')
|
||||
UPDATEALWAYS=_('Update EPUB Always')
|
||||
OVERWRITE=_('Overwrite if Newer')
|
||||
OVERWRITEALWAYS=_('Overwrite Always')
|
||||
CALIBREONLY=_('Update Calibre Metadata from Web Site')
|
||||
CALIBREONLYSAVECOL=_('Update Calibre Metadata from Saved Metadata Column')
|
||||
collision_order=[SKIP,
|
||||
ADDNEW,
|
||||
UPDATE,
|
||||
UPDATEALWAYS,
|
||||
OVERWRITE,
|
||||
OVERWRITEALWAYS,
|
||||
CALIBREONLY,
|
||||
CALIBREONLYSAVECOL,]
|
||||
|
||||
# best idea I've had for how to deal with config/pref saving the
|
||||
# collision name in english.
|
||||
SAVE_SKIP='Skip'
|
||||
SAVE_ADDNEW='Add New Book'
|
||||
SAVE_UPDATE='Update EPUB if New Chapters'
|
||||
SAVE_UPDATEALWAYS='Update EPUB Always'
|
||||
SAVE_OVERWRITE='Overwrite if Newer'
|
||||
SAVE_OVERWRITEALWAYS='Overwrite Always'
|
||||
SAVE_CALIBREONLY='Update Calibre Metadata Only'
|
||||
SAVE_CALIBREONLYSAVECOL='Update Calibre Metadata Only(Saved Column)'
|
||||
save_collisions={
|
||||
SKIP:SAVE_SKIP,
|
||||
ADDNEW:SAVE_ADDNEW,
|
||||
UPDATE:SAVE_UPDATE,
|
||||
UPDATEALWAYS:SAVE_UPDATEALWAYS,
|
||||
OVERWRITE:SAVE_OVERWRITE,
|
||||
OVERWRITEALWAYS:SAVE_OVERWRITEALWAYS,
|
||||
CALIBREONLY:SAVE_CALIBREONLY,
|
||||
CALIBREONLYSAVECOL:SAVE_CALIBREONLYSAVECOL,
|
||||
SAVE_SKIP:SKIP,
|
||||
SAVE_ADDNEW:ADDNEW,
|
||||
SAVE_UPDATE:UPDATE,
|
||||
SAVE_UPDATEALWAYS:UPDATEALWAYS,
|
||||
SAVE_OVERWRITE:OVERWRITE,
|
||||
SAVE_OVERWRITEALWAYS:OVERWRITEALWAYS,
|
||||
SAVE_CALIBREONLY:CALIBREONLY,
|
||||
SAVE_CALIBREONLYSAVECOL:CALIBREONLYSAVECOL,
|
||||
}
|
||||
|
||||
anthology_collision_order=[UPDATE,
|
||||
UPDATEALWAYS,
|
||||
OVERWRITEALWAYS]
|
||||
## moved to prefs.py so they can be included in jobs.py.
|
||||
from calibre_plugins.fanficfare_plugin.prefs import \
|
||||
( SAVE_YES,
|
||||
SAVE_YES_UNLESS_SITE,
|
||||
SKIP,
|
||||
ADDNEW,
|
||||
UPDATE,
|
||||
UPDATEALWAYS,
|
||||
OVERWRITE,
|
||||
OVERWRITEALWAYS,
|
||||
CALIBREONLY,
|
||||
CALIBREONLYSAVECOL,
|
||||
collision_order,
|
||||
SAVE_SKIP,
|
||||
SAVE_ADDNEW,
|
||||
SAVE_UPDATE,
|
||||
SAVE_UPDATEALWAYS,
|
||||
SAVE_OVERWRITE,
|
||||
SAVE_OVERWRITEALWAYS,
|
||||
SAVE_CALIBREONLY,
|
||||
SAVE_CALIBREONLYSAVECOL,
|
||||
save_collisions,
|
||||
anthology_collision_order,
|
||||
)
|
||||
|
||||
gpstyle='QGroupBox {border:0; padding-top:10px; padding-bottom:0px; margin-bottom:0px;}' # background-color:red;
|
||||
|
||||
@@ -183,9 +158,10 @@ class RejectUrlEntry:
|
||||
return retval
|
||||
|
||||
class NotGoingToDownload(Exception):
|
||||
def __init__(self,error,icon='dialog_error.png'):
|
||||
def __init__(self,error,icon='dialog_error.png',showerror=True):
|
||||
self.error=error
|
||||
self.icon=icon
|
||||
self.showerror=showerror
|
||||
|
||||
def __str__(self):
|
||||
return self.error
|
||||
@@ -473,8 +449,9 @@ class AddNewDialog(SizePersistedDialog):
|
||||
'updatemeta': self.updatemeta.isChecked(),
|
||||
'bgmeta': False, # self.bgmeta.isChecked(),
|
||||
'updateepubcover': self.updateepubcover.isChecked(),
|
||||
'smarten_punctuation':self.prefs['smarten_punctuation']
|
||||
}
|
||||
'smarten_punctuation':self.prefs['smarten_punctuation'],
|
||||
'do_wordcount':self.prefs['do_wordcount'],
|
||||
}
|
||||
|
||||
if self.merge:
|
||||
retval['fileform']=='epub'
|
||||
@@ -604,14 +581,34 @@ class UserPassDialog(QDialog):
|
||||
self.status=False
|
||||
self.hide()
|
||||
|
||||
class LoopProgressDialog(QProgressDialog):
|
||||
def LoopProgressDialog(gui,
|
||||
book_list,
|
||||
foreach_function,
|
||||
finish_function,
|
||||
init_label=_("Fetching metadata for stories..."),
|
||||
win_title=_("Downloading metadata for stories"),
|
||||
status_prefix=_("Fetched metadata for")):
|
||||
ld = _LoopProgressDialog(gui,
|
||||
book_list,
|
||||
foreach_function,
|
||||
init_label,
|
||||
win_title,
|
||||
status_prefix)
|
||||
|
||||
# Mac OS X gets upset if the finish_function is called from inside
|
||||
# the real _LoopProgressDialog class.
|
||||
|
||||
# reflect old behavior.
|
||||
if not ld.wasCanceled():
|
||||
finish_function(book_list)
|
||||
|
||||
class _LoopProgressDialog(QProgressDialog):
|
||||
'''
|
||||
ProgressDialog displayed while fetching metadata for each story.
|
||||
'''
|
||||
def __init__(self, gui,
|
||||
book_list,
|
||||
foreach_function,
|
||||
finish_function,
|
||||
init_label=_("Fetching metadata for stories..."),
|
||||
win_title=_("Downloading metadata for stories"),
|
||||
status_prefix=_("Fetched metadata for")):
|
||||
@@ -622,7 +619,6 @@ class LoopProgressDialog(QProgressDialog):
|
||||
self.setMinimumWidth(500)
|
||||
self.book_list = book_list
|
||||
self.foreach_function = foreach_function
|
||||
self.finish_function = finish_function
|
||||
self.status_prefix = status_prefix
|
||||
self.i = 0
|
||||
self.start_time = datetime.now()
|
||||
@@ -662,15 +658,16 @@ class LoopProgressDialog(QProgressDialog):
|
||||
self.foreach_function(book)
|
||||
|
||||
except NotGoingToDownload as d:
|
||||
book['status']=_('Skipped')
|
||||
book['good']=False
|
||||
book['showerror']=d.showerror
|
||||
book['comment']=unicode(d)
|
||||
book['icon'] = d.icon
|
||||
|
||||
except Exception as e:
|
||||
book['good']=False
|
||||
book['comment']=unicode(e)
|
||||
logger.error("Exception: %s:%s"%(book,unicode(e)))
|
||||
traceback.print_exc()
|
||||
logger.error("Exception: %s:%s"%(book,unicode(e)),exc_info=True)
|
||||
|
||||
self.updateStatus()
|
||||
self.i += 1
|
||||
@@ -682,8 +679,6 @@ class LoopProgressDialog(QProgressDialog):
|
||||
|
||||
def do_when_finished(self):
|
||||
self.hide()
|
||||
# Queues a job to process these books in the background.
|
||||
self.finish_function(self.book_list)
|
||||
|
||||
def time_duration_format(seconds):
|
||||
"""
|
||||
@@ -898,7 +893,8 @@ class UpdateExistingDialog(SizePersistedDialog):
|
||||
'updatemeta': self.updatemeta.isChecked(),
|
||||
'bgmeta': self.bgmeta.isChecked(),
|
||||
'updateepubcover': self.updateepubcover.isChecked(),
|
||||
'smarten_punctuation':self.prefs['smarten_punctuation']
|
||||
'smarten_punctuation':self.prefs['smarten_punctuation'],
|
||||
'do_wordcount':self.prefs['do_wordcount'],
|
||||
}
|
||||
|
||||
class StoryListTableWidget(QTableWidget):
|
||||
@@ -1454,6 +1450,15 @@ class ViewLog(SizePersistedDialog):
|
||||
|
||||
self.lineno = None
|
||||
|
||||
scrollable = QScrollArea()
|
||||
scrollcontent = QWidget()
|
||||
scrollable.setWidget(scrollcontent)
|
||||
scrollable.setWidgetResizable(True)
|
||||
self.l.addWidget(scrollable)
|
||||
|
||||
self.sl = QVBoxLayout()
|
||||
scrollcontent.setLayout(self.sl)
|
||||
|
||||
## error = (lineno, msg)
|
||||
for (lineno, error_msg) in errors:
|
||||
# print('adding label for error:%s: %s'%(lineno, error_msg))
|
||||
@@ -1464,7 +1469,7 @@ class ViewLog(SizePersistedDialog):
|
||||
label.setStyleSheet("QLabel { margin-left: 2em; color : blue; } QLabel:hover { color: red; }");
|
||||
label.setToolTip(_('Click to go to line %s')%lineno)
|
||||
label.mouseReleaseEvent = partial(self.label_clicked, lineno=lineno)
|
||||
self.l.addWidget(label)
|
||||
self.sl.addWidget(label)
|
||||
|
||||
# html='<p>'+'</p><p>'.join([ '(lineno: %s) %s'%e for e in errors ])+'</p>'
|
||||
|
||||
@@ -1474,7 +1479,7 @@ class ViewLog(SizePersistedDialog):
|
||||
# self.tb.setHtml(html)
|
||||
# l.addWidget(self.tb)
|
||||
|
||||
self.l.insertStretch(-1)
|
||||
self.sl.insertStretch(-1)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
|
||||
|
||||
+114
-63
@@ -4,9 +4,25 @@ from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2015, Jim Miller'
|
||||
__copyright__ = '2016, Jim Miller'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
|
||||
# import cProfile
|
||||
|
||||
# def do_cprofile(func):
|
||||
# def profiled_func(*args, **kwargs):
|
||||
# profile = cProfile.Profile()
|
||||
# try:
|
||||
# profile.enable()
|
||||
# result = func(*args, **kwargs)
|
||||
# profile.disable()
|
||||
# return result
|
||||
# finally:
|
||||
# profile.print_stats()
|
||||
# return profiled_func
|
||||
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -57,7 +73,7 @@ except:
|
||||
|
||||
from calibre.library.field_metadata import FieldMetadata
|
||||
field_metadata = FieldMetadata()
|
||||
|
||||
|
||||
from calibre_plugins.fanficfare_plugin.common_utils import (
|
||||
set_plugin_icon_resources, get_icon, create_menu_action_unique,
|
||||
get_library_uuid)
|
||||
@@ -476,15 +492,16 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
'updatemeta': prefs['updatemeta'],
|
||||
'bgmeta': False,
|
||||
'updateepubcover': prefs['updateepubcover'],
|
||||
'smarten_punctuation':prefs['smarten_punctuation']
|
||||
'smarten_punctuation':prefs['smarten_punctuation'],
|
||||
'do_wordcount':prefs['do_wordcount'],
|
||||
},"\n".join(url_list))
|
||||
else:
|
||||
self.gui.status_bar.show_message(_('Finished Fetching Story URLs from Email.'),3000)
|
||||
|
||||
|
||||
else:
|
||||
if url_list:
|
||||
self.add_dialog("\n".join(url_list),merge=False)
|
||||
else:
|
||||
else:
|
||||
msg = _('No Valid Story URLs Found in Unread Emails.')
|
||||
if reject_list:
|
||||
msg = msg + '<p>'+(_('(%d Story URLs Skipped, on Rejected URL List)')%len(reject_list))+'</p>'
|
||||
@@ -492,7 +509,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
msg,
|
||||
show=True,
|
||||
show_copy_button=False)
|
||||
|
||||
|
||||
def get_urls_from_page_menu(self,anthology=False):
|
||||
|
||||
urltxt = ""
|
||||
@@ -517,7 +534,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
|
||||
self.gui.status_bar.show_message(_('Finished Fetching Story URLs from Page.'),3000)
|
||||
self.restore_cursor()
|
||||
|
||||
|
||||
if url_list:
|
||||
self.add_dialog("\n".join(url_list),merge=d.anthology,anthology_url=url)
|
||||
else:
|
||||
@@ -589,7 +606,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
self.gui.status_bar.show_message(_('Can only UnNew books in library'),
|
||||
3000)
|
||||
return
|
||||
|
||||
|
||||
if not self.gui.current_view().selectionModel().selectedRows() :
|
||||
self.gui.status_bar.show_message(_('No Selected Books to Get URLs From'),
|
||||
3000)
|
||||
@@ -615,7 +632,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
suffix='.epub',
|
||||
dir=tdir)
|
||||
db.copy_format_to(book['calibre_id'],'EPUB',tmp,index_is_id=True)
|
||||
|
||||
|
||||
unnewtmp = PersistentTemporaryFile(prefix='unnew-%s-'%book['calibre_id'],
|
||||
suffix='.epub',
|
||||
dir=tdir)
|
||||
@@ -639,7 +656,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
if fmt.lower() != 'epub' and db.has_format(book['calibre_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
|
||||
|
||||
|
||||
def get_unnew_books_finish(self, book_list, tdir=None):
|
||||
remove_dir(tdir)
|
||||
if prefs['autoconvert']:
|
||||
@@ -770,7 +787,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
|
||||
self.busy_cursor()
|
||||
self.gui.status_bar.show_message(_('Fetching Story URLs for Series...'))
|
||||
|
||||
|
||||
# get list from identifiers:url/uri if present, but only if
|
||||
# it's *not* a valid story URL.
|
||||
mergeurl = self.get_story_url(db,book_id)
|
||||
@@ -781,7 +798,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
|
||||
self.gui.status_bar.show_message(_('Finished Fetching Story URLs for Series.'),3000)
|
||||
self.restore_cursor()
|
||||
|
||||
|
||||
#print("urlmapfile:%s"%urlmapfile)
|
||||
|
||||
# AddNewDialog collects URLs, format and presents buttons.
|
||||
@@ -941,7 +958,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
init_label=_("Fetching metadata for stories...")
|
||||
win_title=_("Downloading metadata for stories")
|
||||
status_prefix=_("Fetched metadata for")
|
||||
|
||||
|
||||
self.gui.status_bar.show_message(status_bar, 3000)
|
||||
LoopProgressDialog(self.gui,
|
||||
books,
|
||||
@@ -993,7 +1010,8 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
show_copy_button=False):
|
||||
rejecturllist.remove(url)
|
||||
return False
|
||||
|
||||
|
||||
# @do_cprofile
|
||||
def prep_download_loop(self,book,
|
||||
options={'fileform':'epub',
|
||||
'collision':ADDNEW,
|
||||
@@ -1014,6 +1032,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
## Check reject list. Redundant with below for when story URL
|
||||
## changes, but also kept here to avoid network hit in most
|
||||
## common case where given url is story url.
|
||||
|
||||
if self.reject_url(merge,book):
|
||||
return
|
||||
|
||||
@@ -1053,7 +1072,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
## Getting metadata from configured column.
|
||||
custom_columns = self.gui.library_view.model().custom_columns
|
||||
if ( collision in (CALIBREONLYSAVECOL) and
|
||||
prefs['savemetacol'] != '' and
|
||||
prefs['savemetacol'] != '' and
|
||||
prefs['savemetacol'] in custom_columns ):
|
||||
|
||||
savedmeta_book_id = book['calibre_id']
|
||||
@@ -1062,13 +1081,13 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
identicalbooks = self.do_id_search(url)
|
||||
if len(identicalbooks) == 1:
|
||||
savedmeta_book_id = identicalbooks.pop()
|
||||
|
||||
|
||||
if savedmeta_book_id:
|
||||
label = custom_columns[prefs['savemetacol']]['label']
|
||||
savedmetadata = db.get_custom(savedmeta_book_id, label=label, index_is_id=True)
|
||||
else:
|
||||
savedmetadata = None
|
||||
|
||||
|
||||
if savedmetadata:
|
||||
# sets flag inside story so getStoryMetadataOnly won't hit server.
|
||||
adapter.setStoryMetadata(savedmetadata)
|
||||
@@ -1104,19 +1123,19 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
if userpass.status:
|
||||
adapter.username = userpass.user.text()
|
||||
adapter.password = userpass.passwd.text()
|
||||
|
||||
|
||||
except exceptions.AdultCheckRequired:
|
||||
if question_dialog(self.gui, _('Are You an Adult?'), '<p>'+
|
||||
_("%s requires that you be an adult. Please confirm you are an adult in your locale:")%url,
|
||||
show_copy_button=False):
|
||||
adapter.is_adult=True
|
||||
|
||||
|
||||
# let other exceptions percolate up.
|
||||
story = adapter.getStoryMetadataOnly(get_cover=False)
|
||||
book['title'] = story.getMetadata('title')
|
||||
book['author'] = [story.getMetadata('author')]
|
||||
book['url'] = story.getMetadata('storyUrl')
|
||||
|
||||
|
||||
## Check reject list. Redundant with below for when story
|
||||
## URL changes, but also kept here to avoid network hit in
|
||||
## most common case where given url is story url.
|
||||
@@ -1148,7 +1167,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
book['icon']='rotate-right.png'
|
||||
book['status'] = _('Skipped')
|
||||
return
|
||||
|
||||
|
||||
################################################################################################################################################33
|
||||
|
||||
book['is_adult'] = adapter.is_adult
|
||||
@@ -1157,7 +1176,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
|
||||
book['icon'] = 'plus.png'
|
||||
book['status'] = _('Add')
|
||||
|
||||
|
||||
if not bgmeta:
|
||||
# set PI version instead of default.
|
||||
if 'version' in options:
|
||||
@@ -1168,7 +1187,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
if prefs['savemetacol'] != '':
|
||||
# get metadata to save in configured column.
|
||||
book['savemetacol'] = story.dump_html_metadata()
|
||||
|
||||
|
||||
book['title'] = story.getMetadata("title", removeallentities=True)
|
||||
book['author_sort'] = book['author'] = story.getList("author", removeallentities=True)
|
||||
book['publisher'] = story.getMetadata("site")
|
||||
@@ -1179,7 +1198,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
else:
|
||||
book['comments']=''
|
||||
book['series'] = story.getMetadata("series", removeallentities=True)
|
||||
|
||||
|
||||
if story.getMetadataRaw('datePublished'):
|
||||
book['pubdate'] = story.getMetadataRaw('datePublished').replace(tzinfo=local_tz)
|
||||
if story.getMetadataRaw('dateUpdated'):
|
||||
@@ -1188,7 +1207,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
book['timestamp'] = story.getMetadataRaw('dateCreated').replace(tzinfo=local_tz)
|
||||
else:
|
||||
book['timestamp'] = None # need *something* there for calibre.
|
||||
|
||||
|
||||
if not merge:# skip all the collision code when d/ling for merging.
|
||||
if collision in (CALIBREONLY, CALIBREONLYSAVECOL):
|
||||
book['icon'] = 'metadata.png'
|
||||
@@ -1299,7 +1318,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
urlchaptercount = int(story.getMetadata('numChapters').replace(',',''))
|
||||
if chaptercount == urlchaptercount:
|
||||
if collision == UPDATE:
|
||||
raise NotGoingToDownload(_("Already contains %d chapters.")%chaptercount,'edit-undo.png')
|
||||
raise NotGoingToDownload(_("Already contains %d chapters.")%chaptercount,'edit-undo.png',showerror=False)
|
||||
elif chaptercount > urlchaptercount:
|
||||
raise NotGoingToDownload(_("Existing epub contains %d chapters, web site only has %d. Use Overwrite to force update.") % (chaptercount,urlchaptercount),'dialog_error.png')
|
||||
elif chaptercount == 0:
|
||||
@@ -1307,17 +1326,20 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
|
||||
if collision == OVERWRITE and \
|
||||
db.has_format(book_id,formmapping[fileform],index_is_id=True):
|
||||
logger.debug("OVERWRITE file: "+db.format_abspath(book_id, formmapping[fileform], index_is_id=True))
|
||||
fileupdated=datetime.fromtimestamp(os.stat(db.format_abspath(book_id, formmapping[fileform], index_is_id=True))[8])
|
||||
logger.debug("OVERWRITE file updated: %s"%fileupdated)
|
||||
book['fileupdated']=fileupdated
|
||||
if not bgmeta:
|
||||
# check make sure incoming is newer.
|
||||
lastupdated=story.getMetadataRaw('dateUpdated')
|
||||
|
||||
logger.debug("OVERWRITE site updated: %s"%lastupdated)
|
||||
|
||||
# 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')
|
||||
raise NotGoingToDownload(_("Not Overwriting, web site is not newer."),'edit-undo.png',showerror=False)
|
||||
|
||||
# For update, provide a tmp file copy of the existing epub so
|
||||
# it can't change underneath us. Now also overwrite for logpage preserve.
|
||||
@@ -1355,19 +1377,19 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
if not label and k in field_metadata:
|
||||
label=field_metadata[k]['name']
|
||||
key='calibre_std_'+k
|
||||
|
||||
|
||||
# if k == 'user_categories':
|
||||
# value=u', '.join(mi.get(k))
|
||||
# label=_('User Categories')
|
||||
|
||||
|
||||
if label: # only if it has a human readable name.
|
||||
if value is None or not book['calibre_id']:
|
||||
## if existing book, populate existing calibre column
|
||||
## values in metadata, else '' to hide.
|
||||
value=''
|
||||
value=''
|
||||
book['calibre_columns'][key]={'val':value,'label':label}
|
||||
#logger.debug("%s(%s): %s"%(label,key,value))
|
||||
|
||||
|
||||
# custom columns
|
||||
for k, column in self.gui.library_view.model().custom_columns.iteritems():
|
||||
if k != prefs['savemetacol']:
|
||||
@@ -1383,7 +1405,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
value=''
|
||||
book['calibre_columns'][key]={'val':value,'label':label}
|
||||
# logger.debug("%s(%s): %s"%(label,key,value))
|
||||
|
||||
|
||||
# if still 'good', make a temp file to write the output to.
|
||||
# For HTML format users, make the filename inside the zip something reasonable.
|
||||
# For crazy long titles/authors, limit it to 200chars.
|
||||
@@ -1450,10 +1472,20 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
htmllog = htmllog + '</table></body></html>'
|
||||
|
||||
payload = ([], book_list, options)
|
||||
self.gui.proceed_question(self.update_error_column,
|
||||
payload, htmllog,
|
||||
_('FanFicFare log'), _('FanFicFare download ended'), msg,
|
||||
show_copy_button=False)
|
||||
|
||||
# log_viewer_unique_name implemented here: https://github.com/kovidgoyal/calibre/compare/v2.56.0...v2.57.0
|
||||
if calibre_version >= (2, 57, 0):
|
||||
self.gui.proceed_question(self.update_error_column,
|
||||
payload, htmllog,
|
||||
_('FanFicFare log'), _('FanFicFare download ended'), msg,
|
||||
show_copy_button=False,
|
||||
log_viewer_unique_name="FanFicFare log viewer")
|
||||
else:
|
||||
self.gui.proceed_question(self.update_error_column,
|
||||
payload, htmllog,
|
||||
_('FanFicFare log'), _('FanFicFare download ended'), msg,
|
||||
show_copy_button=False)
|
||||
|
||||
return
|
||||
|
||||
cookiejarfile = PersistentTemporaryFile(suffix='.cookiejar',
|
||||
@@ -1472,7 +1504,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
cpus = self.gui.job_manager.server.pool_size
|
||||
args = ['calibre_plugins.fanficfare_plugin.jobs', 'do_download_worker',
|
||||
(book_list, options, cpus, merge)]
|
||||
desc = _('Download FanFiction Book')
|
||||
desc = _('Download %s FanFiction Book(s)') % len(filter(lambda x : x['good'], book_list))
|
||||
job = self.gui.job_manager.run_job(
|
||||
self.Dispatcher(partial(self.download_list_completed,options=options,merge=merge)),
|
||||
func, args=args,
|
||||
@@ -1490,7 +1522,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
custom_columns = self.gui.library_view.model().custom_columns
|
||||
if book['calibre_id'] and prefs['errorcol'] != '' and prefs['errorcol'] in custom_columns:
|
||||
label = custom_columns[prefs['errorcol']]['label']
|
||||
if not book['good']:
|
||||
if not book['good'] and (book['showerror'] or prefs['save_all_errors']):
|
||||
logger.debug("record/update error message column %s %s"%(book['title'],book['url']))
|
||||
self.set_custom(db, book['calibre_id'], 'comment', book['comment'], label=label, commit=True) # book['comment']
|
||||
else:
|
||||
@@ -1673,24 +1705,32 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
if 'status' in book:
|
||||
status = book['status']
|
||||
else:
|
||||
status = 'Good'
|
||||
status = _('Good')
|
||||
htmllog = htmllog + '<tr><td>' + '</td><td>'.join([escapehtml(status),escapehtml(book['title']),escapehtml(", ".join(book['author'])),escapehtml(book['comment']),book['url']]) + '</td></tr>'
|
||||
|
||||
for book in bad_list:
|
||||
if 'status' in book:
|
||||
status = book['status']
|
||||
else:
|
||||
status = 'Bad'
|
||||
status = _('Bad')
|
||||
htmllog = htmllog + '<tr><td>' + '</td><td>'.join([escapehtml(status),escapehtml(book['title']),escapehtml(", ".join(book['author'])),escapehtml(book['comment']),book['url']]) + '</td></tr>'
|
||||
|
||||
htmllog = htmllog + '</table></body></html>'
|
||||
|
||||
do_update_func = self.do_download_list_update
|
||||
|
||||
self.gui.proceed_question(do_update_func,
|
||||
payload, htmllog,
|
||||
_('FanFicFare log'), _('FanFicFare download complete'), msg,
|
||||
show_copy_button=False)
|
||||
# log_viewer_unique_name implemented here: https://github.com/kovidgoyal/calibre/compare/v2.56.0...v2.57.0
|
||||
if calibre_version >= (2, 57, 0):
|
||||
self.gui.proceed_question(do_update_func,
|
||||
payload, htmllog,
|
||||
_('FanFicFare log'), _('FanFicFare download complete'), msg,
|
||||
show_copy_button=False,
|
||||
log_viewer_unique_name="FanFicFare log viewer")
|
||||
else:
|
||||
self.gui.proceed_question(do_update_func,
|
||||
payload, htmllog,
|
||||
_('FanFicFare log'), _('FanFicFare download complete'), msg,
|
||||
show_copy_button=False)
|
||||
|
||||
def do_download_merge_update(self, payload):
|
||||
|
||||
@@ -1728,6 +1768,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
|
||||
self.get_epubmerge_plugin().do_merge(tmp.name,
|
||||
[ x['outfile'] for x in good_list ],
|
||||
tags=mergebook['tags'],
|
||||
titleopt=mergebook['title'],
|
||||
keepmetadatafiles=True,
|
||||
source=mergebook['url'])
|
||||
@@ -1773,7 +1814,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
status_prefix=_("Updated"))
|
||||
|
||||
def update_error_column_loop(self,book,db=None,label=None):
|
||||
if book['calibre_id'] and label:
|
||||
if book['calibre_id'] and label and (book['showerror'] or prefs['save_all_errors']):
|
||||
logger.debug("add/update bad %s %s %s"%(book['title'],book['url'],book['comment']))
|
||||
self.set_custom(db, book['calibre_id'], 'comment', book['comment'], label=label, commit=True)
|
||||
|
||||
@@ -2020,29 +2061,32 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
prefs['gencalcover'] == SAVE_YES ## yes, always
|
||||
or (prefs['gencalcover'] == SAVE_YES_UNLESS_IMG ## yes, unless image.
|
||||
and book['all_metadata']['cover_image'] not in ('specific','first','default')) ):
|
||||
|
||||
|
||||
cover_generated = False # flag for polish below.
|
||||
# Yes, should do gencov. Which?
|
||||
if prefs['calibre_gen_cover'] and HAS_CALGC:
|
||||
# calibre's builtin, if available.
|
||||
cdata = cal_generate_cover(mi)
|
||||
## calibre's builtin, if available. fetch updated mi
|
||||
## object from database. Additional normalization of
|
||||
## series (at least) happens
|
||||
realmi = db.get_metadata(book_id, index_is_id=True)
|
||||
cdata = cal_generate_cover(realmi)
|
||||
db.set_cover(book_id, cdata)
|
||||
cover_generated = True
|
||||
elif prefs['plugin_gen_cover'] and 'Generate Cover' in self.gui.iactions:
|
||||
# plugin, if available.
|
||||
|
||||
|
||||
#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
|
||||
db.refresh_ids([book_id])
|
||||
|
||||
|
||||
gc_plugin = self.gui.iactions['Generate Cover']
|
||||
setting_name = None
|
||||
if prefs['allow_gc_from_ini']:
|
||||
if not configuration: # might already have it from allow_custcol_from_ini
|
||||
configuration = get_fff_config(book['url'],options['fileform'])
|
||||
|
||||
|
||||
# template => regexp to match => GC Setting to use.
|
||||
# generate_cover_settings:
|
||||
# ${category} => Buffy:? the Vampire Slayer => Buffy
|
||||
@@ -2051,38 +2095,41 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
# (template,regexp,setting) = map( lambda x: x.strip(), line.split("=>") )
|
||||
for (template,regexp,setting) in configuration.get_generate_cover_settings():
|
||||
value = Template(template).safe_substitute(book['all_metadata']).encode('utf8')
|
||||
print("%s(%s) => %s => %s"%(template,value,regexp,setting))
|
||||
# print("%s(%s) => %s => %s"%(template,value,regexp,setting))
|
||||
if re.search(regexp,value):
|
||||
setting_name = setting
|
||||
break
|
||||
|
||||
|
||||
if setting_name:
|
||||
logger.debug("Generate Cover Setting from generate_cover_settings(%s)"%setting_name)
|
||||
if setting_name not in gc_plugin.get_saved_setting_names():
|
||||
logger.info("GC Name %s not found, discarding! (check personal.ini for typos)"%setting_name)
|
||||
setting_name = None
|
||||
|
||||
|
||||
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)
|
||||
## fetch updated mi object from
|
||||
## database. Additional normalization of series
|
||||
## (at least) happens
|
||||
realmi = db.get_metadata(book_id, index_is_id=True)
|
||||
gc_plugin.generate_cover_for_book(realmi,saved_setting_name=setting_name)
|
||||
cover_generated = True
|
||||
|
||||
|
||||
if cover_generated and 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),
|
||||
@@ -2093,7 +2140,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
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']],
|
||||
@@ -2184,6 +2231,10 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
book['comments'] = '' # note this is the book comments.
|
||||
|
||||
book['good'] = True
|
||||
book['showerror'] = True # False when NotGoingToDownload is
|
||||
# not-overwrite / not-update / skip
|
||||
# -- what some would consider 'not an
|
||||
# error'
|
||||
book['calibre_id'] = None
|
||||
book['begin'] = None
|
||||
book['end'] = None
|
||||
@@ -2203,7 +2254,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
book = self.convert_url_to_book(url)
|
||||
if book['url'] in uniqueurls:
|
||||
book['good'] = False
|
||||
book['comment'] = "Same story already included."
|
||||
book['comment'] = _("Same story already included.")
|
||||
uniqueurls.add(book['url'])
|
||||
book['listorder']=i # BG d/l jobs don't come back in order.
|
||||
# Didn't matter until anthologies & 'marked' successes
|
||||
@@ -2455,7 +2506,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
|
||||
def restore_cursor(self):
|
||||
QApplication.restoreOverrideCursor()
|
||||
|
||||
|
||||
def split_text_to_urls(urls):
|
||||
# remove dups while preserving order.
|
||||
dups=set()
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import (unicode_literals, division,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2015, Jim Miller'
|
||||
__copyright__ = '2016, Jim Miller'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import re
|
||||
@@ -68,6 +68,10 @@ class IniHighlighter(QSyntaxHighlighter):
|
||||
self.teststoryRule = HighlightingRule( r"^\[teststory:([0-9]+|defaults)\]", Qt.darkCyan, blocknum=3 )
|
||||
self.highlightingRules.append( self.teststoryRule )
|
||||
|
||||
# storyUrl sections
|
||||
self.storyUrlRule = HighlightingRule( r"^\[https?://.*\]", Qt.darkMagenta, blocknum=4 )
|
||||
self.highlightingRules.append( self.storyUrlRule )
|
||||
|
||||
# NOT comments -- but can be custom columns, so don't flag.
|
||||
#self.highlightingRules.append( HighlightingRule( r"(?<!^)#[^\n]*" , Qt.red ) )
|
||||
|
||||
@@ -96,6 +100,10 @@ class IniHighlighter(QSyntaxHighlighter):
|
||||
if blocknum == 3:
|
||||
self.setFormat( 0, len(text), self.teststoryRule.highlight )
|
||||
|
||||
# storyUrl section rules:
|
||||
if blocknum == 4:
|
||||
self.setFormat( 0, len(text), self.storyUrlRule.highlight )
|
||||
|
||||
self.setCurrentBlockState( blocknum )
|
||||
|
||||
class HighlightingRule():
|
||||
|
||||
+31
-12
@@ -4,7 +4,7 @@ from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2015, Jim Miller, 2011, Grant Drake <grant.drake@gmail.com>'
|
||||
__copyright__ = '2016, Jim Miller, 2011, Grant Drake <grant.drake@gmail.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import logging
|
||||
@@ -20,6 +20,15 @@ from calibre.constants import numeric_version as calibre_version
|
||||
from calibre.utils.date import local_tz
|
||||
from calibre.library.comments import sanitize_comments_html
|
||||
|
||||
from calibre_plugins.fanficfare_plugin.wordcount import get_word_count
|
||||
from calibre_plugins.fanficfare_plugin.prefs import (SAVE_YES, SAVE_YES_UNLESS_SITE)
|
||||
|
||||
# pulls in translation files for _() strings
|
||||
try:
|
||||
load_translations()
|
||||
except NameError:
|
||||
pass # load_translations() added in calibre 1.9
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
#
|
||||
# Functions to perform downloads using worker jobs
|
||||
@@ -79,7 +88,7 @@ def do_download_worker(book_list,
|
||||
book_list.append(job.result)
|
||||
book_id = job._book['calibre_id']
|
||||
count = count + 1
|
||||
notification(float(count)/total, '%d of %d stories finished downloading'%(count,total))
|
||||
notification(float(count)/total, _('%d of %d stories finished downloading')%(count,total))
|
||||
# Add this job's output to the current log
|
||||
logger.info('Logfile for book ID %s (%s)'%(book_id, job._book['title']))
|
||||
logger.info(job.details)
|
||||
@@ -148,7 +157,7 @@ def do_download_for_worker(book,options,merge,notification=lambda x,y:x):
|
||||
adapter.setChaptersRange(book['begin'],book['end'])
|
||||
|
||||
adapter.load_cookiejar(options['cookiejarfile'])
|
||||
logger.debug("cookiejar:%s"%adapter.cookiejar)
|
||||
#logger.debug("cookiejar:%s"%adapter.cookiejar)
|
||||
adapter.set_pagecache(options['pagecache'])
|
||||
|
||||
story = adapter.getStoryMetadataOnly()
|
||||
@@ -186,7 +195,7 @@ def do_download_for_worker(book,options,merge,notification=lambda x,y:x):
|
||||
## No need to download at all. Shouldn't ever get down here.
|
||||
if options['collision'] in (CALIBREONLY, CALIBREONLYSAVECOL):
|
||||
logger.info("Skipping CALIBREONLY 'update' down inside worker--this shouldn't be happening...")
|
||||
book['comment'] = 'Metadata collected.'
|
||||
book['comment'] = _('Metadata collected.')
|
||||
book['all_metadata'] = story.getAllMetadata(removeallentities=True)
|
||||
if options['savemetacol'] != '':
|
||||
book['savemetacol'] = story.dump_html_metadata()
|
||||
@@ -211,13 +220,14 @@ def do_download_for_worker(book,options,merge,notification=lambda x,y:x):
|
||||
# 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')
|
||||
raise NotGoingToDownload(_("Not Overwriting, web site is not newer."),'edit-undo.png',showerror=False)
|
||||
|
||||
|
||||
logger.info("write to %s"%outfile)
|
||||
inject_cal_cols(book,story,configuration)
|
||||
writer.writeStory(outfilename=outfile, forceOverwrite=True)
|
||||
book['comment'] = 'Download %s completed, %s chapters.'%(options['fileform'],story.getMetadata("numChapters"))
|
||||
|
||||
book['comment'] = _('Download %s completed, %s chapters.')%(options['fileform'],story.getMetadata("numChapters"))
|
||||
book['all_metadata'] = story.getAllMetadata(removeallentities=True)
|
||||
if options['savemetacol'] != '':
|
||||
book['savemetacol'] = story.dump_html_metadata()
|
||||
@@ -249,7 +259,7 @@ def do_download_for_worker(book,options,merge,notification=lambda x,y:x):
|
||||
book['outfile'] = book['epub_for_update'] # for anthology merge ops.
|
||||
return book
|
||||
else: # not merge,
|
||||
raise NotGoingToDownload(_("Already contains %d chapters.")%chaptercount,'edit-undo.png')
|
||||
raise NotGoingToDownload(_("Already contains %d chapters.")%chaptercount,'edit-undo.png',showerror=False)
|
||||
elif chaptercount > urlchaptercount:
|
||||
raise NotGoingToDownload(_("Existing epub contains %d chapters, web site only has %d. Use Overwrite to force update.") % (chaptercount,urlchaptercount),'dialog_error.png')
|
||||
elif chaptercount == 0:
|
||||
@@ -271,6 +281,16 @@ def do_download_for_worker(book,options,merge,notification=lambda x,y:x):
|
||||
if options['savemetacol'] != '':
|
||||
book['savemetacol'] = story.dump_html_metadata()
|
||||
|
||||
if options['do_wordcount'] == SAVE_YES or (
|
||||
options['do_wordcount'] == SAVE_YES_UNLESS_SITE and not story.getMetadataRaw('numWords') ):
|
||||
wordcount = get_word_count(outfile)
|
||||
logger.info("get_word_count:%s"%wordcount)
|
||||
story.setMetadata('numWords',wordcount)
|
||||
writer.writeStory(outfilename=outfile, forceOverwrite=True)
|
||||
book['all_metadata'] = story.getAllMetadata(removeallentities=True)
|
||||
if options['savemetacol'] != '':
|
||||
book['savemetacol'] = story.dump_html_metadata()
|
||||
|
||||
if options['smarten_punctuation'] and options['fileform'] == "epub" \
|
||||
and calibre_version >= (0, 9, 39):
|
||||
# for smarten punc
|
||||
@@ -286,11 +306,11 @@ def do_download_for_worker(book,options,merge,notification=lambda x,y:x):
|
||||
opts = O(**opts)
|
||||
|
||||
log = Log(level=Log.DEBUG)
|
||||
# report = []
|
||||
polish({outfile:outfile}, opts, log, logger.info) # report.append
|
||||
polish({outfile:outfile}, opts, log, logger.info)
|
||||
|
||||
except NotGoingToDownload as d:
|
||||
book['good']=False
|
||||
book['showerror']=d.showerror
|
||||
book['comment']=unicode(d)
|
||||
book['icon'] = d.icon
|
||||
|
||||
@@ -298,9 +318,8 @@ def do_download_for_worker(book,options,merge,notification=lambda x,y:x):
|
||||
book['good']=False
|
||||
book['comment']=unicode(e)
|
||||
book['icon']='dialog_error.png'
|
||||
book['status'] = 'Error'
|
||||
logger.info("Exception: %s:%s"%(book,unicode(e)))
|
||||
traceback.print_exc()
|
||||
book['status'] = _('Error')
|
||||
logger.info("Exception: %s:%s"%(book,unicode(e)),exc_info=True)
|
||||
|
||||
#time.sleep(10)
|
||||
return book
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2015 Fanficdownloader team, 2015 FanFicFare team
|
||||
# Copyright 2015 Fanficdownloader team, 2016 FanFicFare team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -474,8 +474,8 @@ add_to_include_subject_tags:,tagsfromtitle.SPLIT,forumtags
|
||||
## base_xenforoforum reads Published and Updated datetimes from
|
||||
## Threadmarks if used, or from the posted & updated times of the
|
||||
## 'first' post if no threadmarks.
|
||||
datePublished_format:%%Y-%%m-%%d %%H:%%M:%%S
|
||||
dateUpdated_format:%%Y-%%m-%%d %%H:%%M:%%S
|
||||
datePublished_format:%%Y-%%m-%%d %%H:%%M
|
||||
dateUpdated_format:%%Y-%%m-%%d %%H:%%M
|
||||
|
||||
## Only take the first X characters of the 'first' post to use as
|
||||
## the description.
|
||||
@@ -488,6 +488,33 @@ description_limit:500
|
||||
## in the ebook for that chapter.
|
||||
continue_on_chapter_error:false
|
||||
|
||||
## When given a thread URL, use threadmarks as chapter links when
|
||||
## there are at least this many threadmarks. A number of older
|
||||
## threads have a single threadmark to an 'index' post. Set to 1 to
|
||||
## use threadmarks whenever they exist.
|
||||
minimum_threadmarks:2
|
||||
|
||||
## When 'first post' (or post URL) is being added as a chapter, give
|
||||
## the chapter this title.
|
||||
first_post_title:First Post
|
||||
|
||||
## In normal operation, if given a post URL or a thread URL with less
|
||||
## than minimum_threadmarks, the given post or the first post of the
|
||||
## thread will be included as the first chapter (with chapter title
|
||||
## from first_post_title) unless that post is explicitly linked to in
|
||||
## the collected chapter list. First post is not included when using
|
||||
## thread marks.
|
||||
##
|
||||
## If always_include_first_post:true, then the given or first post
|
||||
## will be included as above even if it is a link in the post or even
|
||||
## if threadmarks are used. Can result in a duplicated chapter.
|
||||
always_include_first_post:false
|
||||
|
||||
## In normal operation, forumtags will only be populated when
|
||||
## threadmarks are used for chapters (see minimum_threadmarks above).
|
||||
## When always_use_forumtags:true, always populate forumtags.
|
||||
always_use_forumtags:false
|
||||
|
||||
## Each output format has a section that overrides [defaults]
|
||||
[html]
|
||||
|
||||
@@ -747,7 +774,7 @@ extratags: FanFiction,Testing,HTML
|
||||
|
||||
## AO3 adapter defines a few extra metadata entries.
|
||||
## If there's ever more than 4 series, add series04,series04Url etc.
|
||||
extra_valid_entries:fandoms,freeformtags,freefromtags,ao3categories,comments,kudos,hits,bookmarks,collections,series00,series01,series02,series03,series00Url,series01Url,series02Url,series03Url,series00HTML,series01HTML,series02HTML,series03HTML
|
||||
extra_valid_entries:fandoms,freeformtags,freefromtags,ao3categories,comments,kudos,hits,bookmarks,collections,byline,series00,series01,series02,series03,series00Url,series01Url,series02Url,series03Url,series00HTML,series01HTML,series02HTML,series03HTML
|
||||
fandoms_label:Fandoms
|
||||
freeformtags_label:Freeform Tags
|
||||
freefromtags_label:Freeform Tags
|
||||
@@ -783,7 +810,7 @@ include_in_category:fandoms
|
||||
include_in_freefromtags:freeformtags
|
||||
|
||||
## adds to titlepage_entries instead of replacing it.
|
||||
#extra_titlepage_entries: fandoms,freeformtags,ao3categories,comments,kudos,hits,bookmarks,series01HTML,series02HTML,series03HTML
|
||||
#extra_titlepage_entries: fandoms,freeformtags,ao3categories,comments,kudos,hits,bookmarks,series01HTML,series02HTML,series03HTML,byline
|
||||
|
||||
## adds to include_subject_tags instead of replacing it.
|
||||
#extra_subject_tags:fandoms,freeformtags,ao3categories
|
||||
@@ -820,18 +847,6 @@ extracategories:The Sentinel
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[bdsm-geschichten.net]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## This site offers no index page so we can either guess the chapter URLs
|
||||
## by dec/incrementing numbers ('guess') or walk all the chapters in the metadata
|
||||
## parsing state ('parse'). Since guessing can lead to errors for non-standard
|
||||
## story URLs, the default is to parse
|
||||
#find_chapters:guess
|
||||
|
||||
[bloodshedverse.com]
|
||||
## website encoding(s) In theory, each website reports the character
|
||||
## encoding they use for each page. In practice, some sites report it
|
||||
@@ -842,6 +857,12 @@ extracategories:The Sentinel
|
||||
## it has +90% confidence. 'auto' is not reliable.
|
||||
website_encodings:Windows-1252,ISO-8859-1,auto
|
||||
|
||||
## dateUpdate doesn't usually have time, but it does on
|
||||
## bloodshedverse.com. See
|
||||
## http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
## Note that ini format requires % to be escaped as %%.
|
||||
dateUpdated_format:%%Y-%%m-%%d %%H:%%M
|
||||
|
||||
## Extra metadata that this adapter knows about. See [dramione.org]
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:warnings,reviews
|
||||
@@ -871,15 +892,6 @@ strip_text_links:true
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Blood Ties
|
||||
|
||||
[buffynfaith.net]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Buffy: The Vampire Slayer
|
||||
|
||||
## Some sites also require the user to confirm they are adult for
|
||||
## adult content. In commandline version, this should go in your
|
||||
## personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[fanfic.castletv.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -1193,14 +1205,41 @@ dislikes_label:Dislikes
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
## finestories.com has started requiring login by email rather than
|
||||
## pen name.
|
||||
#username:youremail@yourdomain.dom
|
||||
#password:yourpassword
|
||||
|
||||
# shows size as "10 KB", not word count
|
||||
extra_valid_entries:size
|
||||
## Clear FanFiction from defaults, site is original fiction.
|
||||
extratags:
|
||||
|
||||
# don't show twitter icon.
|
||||
cover_exclusion_regexp:/res/css/bir.png
|
||||
extra_valid_entries:size,universe,universeUrl,universeHTML,sitetags,notice,codes,score
|
||||
#extra_titlepage_entries:size,universeHTML,sitetags,notice,score
|
||||
include_in_codes:sitetags
|
||||
|
||||
## adds to include_subject_tags instead of replacing it.
|
||||
#extra_subject_tags:sitetags
|
||||
|
||||
size_label:Size
|
||||
universe_label:Universe
|
||||
universeUrl_label:Universe URL
|
||||
universeHTML_label:Universe
|
||||
sitetags_label:Site Tags
|
||||
notice_label:Notice
|
||||
score_label:Score
|
||||
|
||||
## Assume entryUrl, apply to "<a class='%slink' href='%s'>%s</a>" to
|
||||
## make entryHTML.
|
||||
make_linkhtml_entries:universe
|
||||
|
||||
## storiesonline.net stories can be in a series or a universe, but not
|
||||
## both. By default, universe will be populated in 'series' with
|
||||
## index=0
|
||||
universe_as_series: true
|
||||
|
||||
## some sites include images that we don't ever want becoming the
|
||||
## cover image. This lets you exclude them.
|
||||
cover_exclusion_regexp:/css/bir.png
|
||||
|
||||
[forums.spacebattles.com]
|
||||
## see [base_xenforoforum]
|
||||
@@ -1208,27 +1247,6 @@ cover_exclusion_regexp:/res/css/bir.png
|
||||
[forums.sufficientvelocity.com]
|
||||
## see [base_xenforoforum]
|
||||
|
||||
[grangerenchanted.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
## Some sites also require the user to confirm they are adult for
|
||||
## adult content. In commandline version, this should go in your
|
||||
## personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
extracharacters:Hermione Granger
|
||||
|
||||
## Extra metadata that this adapter knows about. See [dramione.org]
|
||||
## for examples of how to use them.
|
||||
extra_valid_entries:read,reviews
|
||||
|
||||
[hlfiction.net]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Highlander
|
||||
@@ -1280,9 +1298,21 @@ crossoverfandom_label:Crossover Fandom
|
||||
extra_titlepage_entries:universe,crossoverfandom
|
||||
|
||||
[literotica.com]
|
||||
extra_valid_entries:eroticatags
|
||||
extra_valid_entries:eroticatags,averrating
|
||||
eroticatags_label:Erotica Tags
|
||||
extra_titlepage_entries: eroticatags
|
||||
averrating_label:Average Rating
|
||||
extra_titlepage_entries:eroticatags,averrating
|
||||
|
||||
## Extract more erotica_tags from the meta tag of each chapter
|
||||
use_meta_keywords: true
|
||||
|
||||
## For multiple chapter stories, attempt to clean up the chapter title. This will
|
||||
## remove the story title and change "Ch. 01" to "Chapter 1", "Pt. 01" to "Part 1"
|
||||
## or just use the text. If this can't be done, the full title is used.
|
||||
clean_chapter_titles: false
|
||||
|
||||
## Add the chapter description at the start of each chapter.
|
||||
description_in_chapter: false
|
||||
|
||||
[lotrfanfiction.com]
|
||||
extra_valid_entries: readings
|
||||
@@ -1464,22 +1494,6 @@ extracategories:Supernatural
|
||||
extracharacters:Sam,Dean
|
||||
extraships:Sam/Dean
|
||||
|
||||
[scarhead.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
## Some sites also require the user to confirm they are adult for
|
||||
## adult content. In commandline version, this should go in your
|
||||
## personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
|
||||
[sheppardweir.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -1557,6 +1571,24 @@ extracategories:Transgender
|
||||
## confirm they are adult for adult content.
|
||||
#is_adult:true
|
||||
|
||||
## This site has a number of additional site specific metadata
|
||||
## entries. This is the first test case of base_efiction 'Auto
|
||||
## metadata' automatically including unrecognized metadata. Still
|
||||
## requires entries in extra_valid_entries to be used.
|
||||
extra_valid_entries:turnedinto,featureditems,locale,motivationforchange,sexualorientation,storytheme,bodymodification,personality,storytype,typeofchange
|
||||
#add_to_titlepage_entries:,turnedinto,featureditems,locale,motivationforchange,sexualorientation,storytheme,bodymodification,personality,storytype,typeofchange
|
||||
|
||||
turnedinto_label:Turned Into
|
||||
featureditems_label:Featured Items
|
||||
locale_label:Locale
|
||||
motivationforchange_label:Motivation for Change
|
||||
sexualorientation_label:Sexual Orientation
|
||||
storytheme_label:Story Theme
|
||||
bodymodification_label:Body Modification
|
||||
personality_label:Personality
|
||||
storytype_label:Story Type
|
||||
typeofchange_label:Type of Change
|
||||
|
||||
[thehexfiles.net]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
@@ -1603,15 +1635,6 @@ readings_label: Readings
|
||||
## personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[tokra.fandomnet.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Stargate: SG-1
|
||||
|
||||
[tolkienfanfiction.com]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Lord of the Rings
|
||||
@@ -1773,6 +1796,10 @@ check_next_chapter:false
|
||||
#password:yourpassword
|
||||
|
||||
[www.ficbook.net]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[www.fictionalley.org]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
@@ -1891,6 +1918,9 @@ extracategories:Harry Potter
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
|
||||
extra_valid_entries:reads,reviews
|
||||
reads_label:Total Read Count
|
||||
|
||||
[www.hpfanficarchive.com]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
@@ -1972,11 +2002,6 @@ cover_exclusion_regexp:/stories/999/images/.*?_trophy.png
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:NCIS
|
||||
|
||||
[www.nickngreg.nl]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:CSI
|
||||
extraships:Nick Stokes/Greg Sanders
|
||||
|
||||
[www.phoenixsong.net]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -2000,7 +2025,7 @@ extra_valid_entries:stars,reviews,reads,takesplaces,snapeflavours,sitetags
|
||||
stars_label:Frogs
|
||||
takesplaces_label:Takes Place
|
||||
snapeflavours_label:Snape Flavour
|
||||
sitetags_labels:Site Tags
|
||||
sitetags_label:Site Tags
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
@@ -2036,8 +2061,8 @@ pages_label:Pages
|
||||
readers_label:Readers
|
||||
reads_label:Reads
|
||||
favorites_label:Favorites
|
||||
searchtags:Search Tags
|
||||
comments:Comments
|
||||
searchtags_label:Search Tags
|
||||
comments_label:Comments
|
||||
|
||||
include_in_category:category,searchtags
|
||||
|
||||
@@ -2209,6 +2234,80 @@ extracategories:Stargate: Atlantis
|
||||
extra_valid_entries:reviews
|
||||
reviews_label:Reviews
|
||||
|
||||
[buffygiles.velocitygrass.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracharacters:Buffy,Giles
|
||||
|
||||
[fanfiction.lucifael.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
[www.andromeda-web.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Andromeda
|
||||
|
||||
[www.artemis-fowl.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Artemis Fowl
|
||||
|
||||
[www.naiceanilme.net]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
## commandline version, this should go in your personal.ini, not
|
||||
## defaults.ini.
|
||||
#username:YourName
|
||||
#password:yourpassword
|
||||
|
||||
[overrides]
|
||||
## It may sometimes be useful to override all of the specific format,
|
||||
## site and site:format sections in your private configuration. For
|
||||
|
||||
+60
-3
@@ -4,7 +4,7 @@ from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2015, Jim Miller'
|
||||
__copyright__ = '2016, Jim Miller'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import logging
|
||||
@@ -15,9 +15,59 @@ import copy
|
||||
from calibre.utils.config import JSONConfig
|
||||
from calibre.gui2.ui import get_gui
|
||||
|
||||
from calibre_plugins.fanficfare_plugin.dialogs import SAVE_UPDATE
|
||||
from calibre_plugins.fanficfare_plugin.common_utils import get_library_uuid
|
||||
|
||||
SKIP=_('Skip')
|
||||
ADDNEW=_('Add New Book')
|
||||
UPDATE=_('Update EPUB if New Chapters')
|
||||
UPDATEALWAYS=_('Update EPUB Always')
|
||||
OVERWRITE=_('Overwrite if Newer')
|
||||
OVERWRITEALWAYS=_('Overwrite Always')
|
||||
CALIBREONLY=_('Update Calibre Metadata from Web Site')
|
||||
CALIBREONLYSAVECOL=_('Update Calibre Metadata from Saved Metadata Column')
|
||||
collision_order=[SKIP,
|
||||
ADDNEW,
|
||||
UPDATE,
|
||||
UPDATEALWAYS,
|
||||
OVERWRITE,
|
||||
OVERWRITEALWAYS,
|
||||
CALIBREONLY,
|
||||
CALIBREONLYSAVECOL,]
|
||||
|
||||
# best idea I've had for how to deal with config/pref saving the
|
||||
# collision name in english.
|
||||
SAVE_SKIP='Skip'
|
||||
SAVE_ADDNEW='Add New Book'
|
||||
SAVE_UPDATE='Update EPUB if New Chapters'
|
||||
SAVE_UPDATEALWAYS='Update EPUB Always'
|
||||
SAVE_OVERWRITE='Overwrite if Newer'
|
||||
SAVE_OVERWRITEALWAYS='Overwrite Always'
|
||||
SAVE_CALIBREONLY='Update Calibre Metadata Only'
|
||||
SAVE_CALIBREONLYSAVECOL='Update Calibre Metadata Only(Saved Column)'
|
||||
save_collisions={
|
||||
SKIP:SAVE_SKIP,
|
||||
ADDNEW:SAVE_ADDNEW,
|
||||
UPDATE:SAVE_UPDATE,
|
||||
UPDATEALWAYS:SAVE_UPDATEALWAYS,
|
||||
OVERWRITE:SAVE_OVERWRITE,
|
||||
OVERWRITEALWAYS:SAVE_OVERWRITEALWAYS,
|
||||
CALIBREONLY:SAVE_CALIBREONLY,
|
||||
CALIBREONLYSAVECOL:SAVE_CALIBREONLYSAVECOL,
|
||||
SAVE_SKIP:SKIP,
|
||||
SAVE_ADDNEW:ADDNEW,
|
||||
SAVE_UPDATE:UPDATE,
|
||||
SAVE_UPDATEALWAYS:UPDATEALWAYS,
|
||||
SAVE_OVERWRITE:OVERWRITE,
|
||||
SAVE_OVERWRITEALWAYS:OVERWRITEALWAYS,
|
||||
SAVE_CALIBREONLY:CALIBREONLY,
|
||||
SAVE_CALIBREONLYSAVECOL:CALIBREONLYSAVECOL,
|
||||
}
|
||||
|
||||
anthology_collision_order=[UPDATE,
|
||||
UPDATEALWAYS,
|
||||
OVERWRITEALWAYS]
|
||||
|
||||
|
||||
# Show translated strings, but save the same string in prefs so your
|
||||
# prefs are the same in different languages.
|
||||
YES=_('Yes, Always')
|
||||
@@ -26,9 +76,11 @@ YES_IF_IMG=_('Yes, if EPUB has a cover image')
|
||||
SAVE_YES_IF_IMG='Yes, if img'
|
||||
YES_UNLESS_IMG=_('Yes, unless FanFicFare found a cover image')
|
||||
SAVE_YES_UNLESS_IMG='Yes, unless img'
|
||||
YES_UNLESS_SITE=_('Yes, unless found on site')
|
||||
SAVE_YES_UNLESS_SITE='Yes, unless site'
|
||||
NO=_('No')
|
||||
SAVE_NO='No'
|
||||
calcover_save_options = {
|
||||
prefs_save_options = {
|
||||
YES:SAVE_YES,
|
||||
SAVE_YES:YES,
|
||||
YES_IF_IMG:SAVE_YES_IF_IMG,
|
||||
@@ -37,9 +89,12 @@ calcover_save_options = {
|
||||
SAVE_YES_UNLESS_IMG:YES_UNLESS_IMG,
|
||||
NO:SAVE_NO,
|
||||
SAVE_NO:NO,
|
||||
YES_UNLESS_SITE:SAVE_YES_UNLESS_SITE,
|
||||
SAVE_YES_UNLESS_SITE:YES_UNLESS_SITE,
|
||||
}
|
||||
updatecalcover_order=[YES,YES_IF_IMG,NO]
|
||||
gencalcover_order=[YES,YES_UNLESS_IMG,NO]
|
||||
do_wordcount_order=[YES,YES_UNLESS_SITE,NO]
|
||||
|
||||
# if don't have any settings for FanFicFarePlugin, copy from
|
||||
# predecessor FanFictionDownLoaderPlugin.
|
||||
@@ -78,6 +133,7 @@ default_prefs['checkforseriesurlid'] = True
|
||||
default_prefs['checkforurlchange'] = True
|
||||
default_prefs['injectseries'] = False
|
||||
default_prefs['matchtitleauth'] = True
|
||||
default_prefs['do_wordcount'] = SAVE_YES_UNLESS_SITE
|
||||
default_prefs['smarten_punctuation'] = False
|
||||
default_prefs['show_est_time'] = False
|
||||
|
||||
@@ -102,6 +158,7 @@ default_prefs['countpagesstats'] = []
|
||||
default_prefs['wordcountmissing'] = False
|
||||
|
||||
default_prefs['errorcol'] = ''
|
||||
default_prefs['save_all_errors'] = True
|
||||
default_prefs['savemetacol'] = ''
|
||||
default_prefs['custom_cols'] = {}
|
||||
default_prefs['custom_cols_newonly'] = {}
|
||||
|
||||
+532
-502
File diff suppressed because it is too large
Load Diff
+527
-497
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+526
-496
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
+523
-494
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+551
-521
File diff suppressed because it is too large
Load Diff
+528
-499
File diff suppressed because it is too large
Load Diff
+720
-446
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2016, Jim Miller, 2011, Grant Drake <grant.drake@gmail.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
'''
|
||||
A lot of this is lifted from Count Pages plugin by Grant Drake (with
|
||||
some changes from davidfor.)
|
||||
'''
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
import re
|
||||
|
||||
from calibre.ebooks.oeb.iterator import EbookIterator
|
||||
|
||||
RE_HTML_BODY = re.compile(u'<body[^>]*>(.*)</body>', re.UNICODE | re.DOTALL | re.IGNORECASE)
|
||||
RE_STRIP_MARKUP = re.compile(u'<[^>]+>', re.UNICODE)
|
||||
|
||||
|
||||
def get_word_count(book_path):
|
||||
'''
|
||||
Estimate a word count
|
||||
'''
|
||||
from calibre.utils.localization import get_lang
|
||||
|
||||
iterator = _open_epub_file(book_path)
|
||||
|
||||
lang = iterator.opf.language
|
||||
lang = get_lang() if not lang else lang
|
||||
count = _get_epub_standard_word_count(iterator, lang)
|
||||
|
||||
return count
|
||||
|
||||
def _open_epub_file(book_path, strip_html=False):
|
||||
'''
|
||||
Given a path to an EPUB file, read the contents into a giant block of text
|
||||
'''
|
||||
iterator = EbookIterator(book_path)
|
||||
iterator.__enter__(only_input_plugin=True, run_char_count=True,
|
||||
read_anchor_map=False)
|
||||
return iterator
|
||||
|
||||
def _get_epub_standard_word_count(iterator, lang='en'):
|
||||
'''
|
||||
This algorithm counts individual words instead of pages
|
||||
'''
|
||||
|
||||
book_text = _read_epub_contents(iterator, strip_html=True)
|
||||
|
||||
try:
|
||||
from calibre.spell.break_iterator import count_words
|
||||
wordcount = count_words(book_text, lang)
|
||||
logger.debug('\tWord count - count_words method:%s'%wordcount)
|
||||
except:
|
||||
try: # The above method is new and no-one will have it as of 08/01/2016. Use an older method for a beta.
|
||||
from calibre.spell.break_iterator import split_into_words_and_positions
|
||||
wordcount = len(split_into_words_and_positions(book_text, lang))
|
||||
logger.debug('\tWord count - split_into_words_and_positions method:%s'%wordcount)
|
||||
except:
|
||||
from calibre.utils.wordcount import get_wordcount_obj
|
||||
wordcount = get_wordcount_obj(book_text)
|
||||
wordcount = wordcount.words
|
||||
logger.debug('\tWord count - old method:%s'%wordcount)
|
||||
|
||||
return wordcount
|
||||
|
||||
def _read_epub_contents(iterator, strip_html=False):
|
||||
'''
|
||||
Given an iterator for an ePub file, read the contents into a giant block of text
|
||||
'''
|
||||
book_files = []
|
||||
for path in iterator.spine:
|
||||
with open(path, 'rb') as f:
|
||||
html = f.read().decode('utf-8', 'replace')
|
||||
if strip_html:
|
||||
html = unicode(_extract_body_text(html)).strip()
|
||||
#print('FOUND HTML:', html)
|
||||
book_files.append(html)
|
||||
return ''.join(book_files)
|
||||
|
||||
def _extract_body_text(data):
|
||||
'''
|
||||
Get the body text of this html content wit any html tags stripped
|
||||
'''
|
||||
body = RE_HTML_BODY.findall(data)
|
||||
if body:
|
||||
return RE_STRIP_MARKUP.sub('', body[0]).replace('.','. ')
|
||||
return ''
|
||||
|
||||
@@ -27,7 +27,7 @@ except:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
loghandler=logging.StreamHandler()
|
||||
loghandler.setFormatter(logging.Formatter("FFF:%(levelname)s:%(filename)s(%(lineno)d):%(message)s"))
|
||||
loghandler.setFormatter(logging.Formatter("FFF: %(levelname)s: %(asctime)s: %(filename)s(%(lineno)d): %(message)s"))
|
||||
logger.addHandler(loghandler)
|
||||
loghandler.setLevel(logging.DEBUG)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2011 Fanficdownloader team, 2015 FanFicFare team
|
||||
# Copyright 2011 Fanficdownloader team, 2016 FanFicFare team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -85,7 +85,6 @@ import adapter_hpfanficarchivecom
|
||||
import adapter_twilightarchivescom
|
||||
import adapter_nhamagicalworldsus
|
||||
import adapter_hlfictionnet
|
||||
import adapter_grangerenchantedcom
|
||||
import adapter_dracoandginnycom
|
||||
import adapter_scarvesandcoffeenet
|
||||
import adapter_thepetulantpoetesscom
|
||||
@@ -102,13 +101,9 @@ import adapter_efictionestelielde
|
||||
import adapter_pommedesangcom
|
||||
import adapter_restrictedsectionorg
|
||||
import adapter_imagineeficcom
|
||||
import adapter_buffynfaithnet
|
||||
import adapter_psychficcom
|
||||
import adapter_tokrafandomnetcom
|
||||
import adapter_asr3slashzoneorg
|
||||
import adapter_nickandgregnet
|
||||
import adapter_potterheadsanonymouscom
|
||||
import adapter_scarheadnet
|
||||
import adapter_fictionpadcom
|
||||
import adapter_storiesonlinenet
|
||||
import adapter_trekiverseorg
|
||||
@@ -120,7 +115,6 @@ import adapter_nocturnallightnet
|
||||
import adapter_fanfichu
|
||||
import adapter_fanfictioncsodaidokhu
|
||||
import adapter_fictionmaniatv
|
||||
import adapter_bdsmgeschichten
|
||||
import adapter_tolkienfanfiction
|
||||
import adapter_themaplebookshelf
|
||||
import adapter_fannation
|
||||
@@ -139,6 +133,11 @@ import adapter_ninelivesarchivecom
|
||||
import adapter_masseffect2in
|
||||
import adapter_quotevcom
|
||||
import adapter_mcstoriescom
|
||||
import adapter_lucifaelff
|
||||
import adapter_buffygilescom
|
||||
import adapter_andromedawebcom
|
||||
import adapter_artemisfowlcom
|
||||
import adapter_naiceanilmenet
|
||||
|
||||
## This bit of complexity allows adapters to be added by just adding
|
||||
## importing. It eliminates the long if/else clauses we used to need
|
||||
@@ -169,8 +168,9 @@ def getNormalStoryURL(url):
|
||||
return None
|
||||
|
||||
def getNormalStoryURLSite(url):
|
||||
# print("getNormalStoryURLSite:%s"%url)
|
||||
if not getNormalStoryURL.__dummyconfig:
|
||||
getNormalStoryURL.__dummyconfig = Configuration("test1.com","EPUB")
|
||||
getNormalStoryURL.__dummyconfig = Configuration(["test1.com"],"EPUB",lightweight=True)
|
||||
# pulling up an adapter is pretty low over-head. If
|
||||
# it fails, it's a bad url.
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2016 FanFicFare 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.
|
||||
#
|
||||
# ####### Not all lables are captured. they are not formtted correctly on the
|
||||
# ####### webpage.
|
||||
|
||||
# Software: eFiction
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
def getClass():
|
||||
return AndromedaWebComAdapter # XXX
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class AndromedaWebComAdapter(BaseSiteAdapter): # XXX
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.decode = ["Windows-1252",
|
||||
"utf8"] # 1252 is a superset of iso-8859-1.
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
self.is_adult=False
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
# XXX Most sites don't have the /fiction part. Replace all to remove it usually.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/fiction/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','awc') # XXX
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%d %b %Y" # XXX
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'www.andromeda-web.com' # XXX
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/fiction/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain()+"/fiction/viewstory.php?sid=")+r"\d+$"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
if 'Registered Users Only' in data \
|
||||
or 'There is no such account on our website' in data \
|
||||
or "That password doesn't match the one in our database" in data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def performLogin(self, url):
|
||||
params = {}
|
||||
|
||||
if self.password:
|
||||
params['penname'] = self.username
|
||||
params['password'] = self.password
|
||||
else:
|
||||
params['penname'] = self.getConfig("username")
|
||||
params['password'] = self.getConfig("password")
|
||||
params['cookiecheck'] = '1'
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/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 = "&warning=2"
|
||||
else:
|
||||
addurl=""
|
||||
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
if self.needToLoginCheck(data):
|
||||
# need to log in for this one.
|
||||
self.performLogin(url)
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
# Since the warning text can change by warning level, let's
|
||||
# look for the warning pass url. ksarchive uses
|
||||
# &warning= -- actually, so do other sites. Must be an
|
||||
# eFiction book.
|
||||
|
||||
# fiction/viewstory.php?sid=1882&warning=4
|
||||
# fiction/viewstory.php?sid=1654&ageconsent=ok&warning=2
|
||||
#print data
|
||||
m = re.search(r"'fiction/viewstory.php\?sid=10(&warning=2)'",data)
|
||||
m = re.search(r"'fiction/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 = self.make_soup(data)
|
||||
# print data
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
pagetitle = soup.find('div',{'id':'content'})
|
||||
|
||||
## Title
|
||||
a = pagetitle.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
|
||||
|
||||
self.story.setMetadata('title',stripHTML(a))
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
a = pagetitle.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
|
||||
self.story.setMetadata('authorId',a['href'].split('=')[1])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href'])
|
||||
self.story.setMetadata('author',a.string)
|
||||
|
||||
# Find the chapters:
|
||||
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")):
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/fiction/'+chapter['href']+addurl))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
# eFiction sites don't help us out a lot with their meta data
|
||||
# formating, so it's a little ugly.
|
||||
|
||||
# utility method
|
||||
def defaultGetattr(d,k):
|
||||
try:
|
||||
return d[k]
|
||||
except:
|
||||
return ""
|
||||
|
||||
# <span class="label">Rated:</span> NC-17<br /> etc
|
||||
labels = soup.findAll('span',{'class':'label'})
|
||||
for labelspan in labels:
|
||||
value = labelspan.nextSibling
|
||||
label = labelspan.string
|
||||
|
||||
if 'Summary' in label:
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while 'label' not in defaultGetattr(value,'class'):
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
if 'Rated' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
if 'Categories' in label:
|
||||
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
for cat in cats:
|
||||
self.story.addToList('category',cat.string)
|
||||
|
||||
if 'Characters' in label:
|
||||
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
|
||||
for char in chars:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
if 'Genre' in label:
|
||||
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1'))
|
||||
for genre in genres:
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
if 'Warnings' in label:
|
||||
warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=3'))
|
||||
for warning in warnings:
|
||||
self.story.addToList('warnings',warning.string)
|
||||
|
||||
if 'Completed' in label:
|
||||
if 'Yes' in value:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
if 'Published' in label:
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
if 'Updated' in label:
|
||||
# there's a stray [ at the end.
|
||||
#value = value[0:-1]
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
try:
|
||||
# Find Series name from series URL.
|
||||
a = soup.find('a', href=re.compile(r"fiction/viewseries.php\?seriesid=\d+"))
|
||||
series_name = a.string
|
||||
series_url = 'http://'+self.host+'/'+a['href']
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
seriessoup = self.make_soup(self._fetchUrl(series_url))
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'^fiction/viewstory.php\?sid=\d+$'))
|
||||
i=1
|
||||
for a in storyas:
|
||||
if a['href'] == ('fiction/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 = self.make_soup(self._fetchUrl(url))
|
||||
|
||||
div = soup.find('div', {'class' : 'story'})
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
@@ -190,6 +190,10 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
self.story.addToList('authorUrl',a['href'])
|
||||
self.story.addToList('author',a.text)
|
||||
|
||||
byline = metasoup.find('h3',{'class':'byline'})
|
||||
if byline:
|
||||
self.story.setMetadata('byline',stripHTML(byline))
|
||||
|
||||
newestChapter = None
|
||||
self.newestChapterNum = None # save for comparing during update.
|
||||
# Scan all chapters to find the oldest and newest, on AO3 it's
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2016 FanFicFare 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.
|
||||
#
|
||||
# ####### Not all lables are captured. they are not formtted correctly on the
|
||||
# ####### webpage.
|
||||
|
||||
# Software: eFiction
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
def getClass():
|
||||
return ArtemisFowlComAdapter # XXX
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class ArtemisFowlComAdapter(BaseSiteAdapter): # XXX
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.decode = ["Windows-1252",
|
||||
"utf8"] # 1252 is a superset of iso-8859-1.
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
self.is_adult=False
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
# XXX Most sites don't have the /fiction part. Replace all to remove it usually.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/fanfiction/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','afcff') # XXX
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%d/%m/%y" # XXX
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'www.artemis-fowl.com' # XXX
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/fanfiction/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain()+"/fanfiction/viewstory.php?sid=")+r"\d+$"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
if 'Registered Users Only' in data \
|
||||
or 'There is no such account on our website' in data \
|
||||
or "That password doesn't match the one in our database" in data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def performLogin(self, url):
|
||||
params = {}
|
||||
|
||||
if self.password:
|
||||
params['penname'] = self.username
|
||||
params['password'] = self.password
|
||||
else:
|
||||
params['penname'] = self.getConfig("username")
|
||||
params['password'] = self.getConfig("password")
|
||||
params['cookiecheck'] = '1'
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/user.php?action=login'
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Member Account" not in d : #Member Account
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
# Weirdly, different sites use different warning numbers.
|
||||
# If the title search below fails, there's a good chance
|
||||
# you need a different number. print data at that point
|
||||
# and see what the 'click here to continue' url says.
|
||||
addurl = "&warning=5"
|
||||
else:
|
||||
addurl=""
|
||||
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
if self.needToLoginCheck(data):
|
||||
# need to log in for this one.
|
||||
self.performLogin(url)
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
# Since the warning text can change by warning level, let's
|
||||
# look for the warning pass url. ksarchive uses
|
||||
# &warning= -- actually, so do other sites. Must be an
|
||||
# eFiction book.
|
||||
|
||||
# fanfiction/viewstory.php?sid=1882&warning=4
|
||||
# fanfiction/viewstory.php?sid=1654&ageconsent=ok&warning=2
|
||||
#print data
|
||||
m = re.search(r"'fanfiction/viewstory.php\?sid=10(&warning=5)'",data)
|
||||
m = re.search(r"'fanfiction/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 = self.make_soup(data)
|
||||
# print data
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
pagetitle = soup.find('div',{'id':'pagetitle'})
|
||||
|
||||
## Title
|
||||
a = pagetitle.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
|
||||
|
||||
self.story.setMetadata('title',stripHTML(a))
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
a = pagetitle.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
|
||||
self.story.setMetadata('authorId',a['href'].split('=')[1])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href'])
|
||||
self.story.setMetadata('author',a.string)
|
||||
|
||||
# Find the chapters:
|
||||
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")):
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/fanfiction/'+chapter['href']+addurl))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
# eFiction sites don't help us out a lot with their meta data
|
||||
# formating, so it's a little ugly.
|
||||
|
||||
# utility method
|
||||
def defaultGetattr(d,k):
|
||||
try:
|
||||
return d[k]
|
||||
except:
|
||||
return ""
|
||||
|
||||
# <span class="label">Rated:</span> NC-17<br /> etc
|
||||
labels = soup.findAll('span',{'class':'label'})
|
||||
for labelspan in labels:
|
||||
value = labelspan.nextSibling
|
||||
label = labelspan.string
|
||||
|
||||
if 'Summary' in label:
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while 'label' not in defaultGetattr(value,'class'):
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
if 'Rated' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
if 'Categories' in label:
|
||||
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
for cat in cats:
|
||||
self.story.addToList('category',cat.string)
|
||||
|
||||
if 'Characters' in label:
|
||||
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
|
||||
for char in chars:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
if 'Genre' in label:
|
||||
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1'))
|
||||
for genre in genres:
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
if 'Warnings' in label:
|
||||
warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=3'))
|
||||
for warning in warnings:
|
||||
self.story.addToList('warnings',warning.string)
|
||||
|
||||
if 'Completed' in label:
|
||||
if 'Yes' in value:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
if 'Published' in label:
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
if 'Updated' in label:
|
||||
# there's a stray [ at the end.
|
||||
#value = value[0:-1]
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
try:
|
||||
# Find Series name from series URL.
|
||||
a = soup.find('a', href=re.compile(r"fanfiction/viewseries.php\?seriesid=\d+"))
|
||||
series_name = a.string
|
||||
series_url = 'http://'+self.host+'/'+a['href']
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
seriessoup = self.make_soup(self._fetchUrl(series_url))
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'^fanfiction/viewstory.php\?sid=\d+$'))
|
||||
i=1
|
||||
for a in storyas:
|
||||
if a['href'] == ('fanfiction/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 = self.make_soup(self._fetchUrl(url))
|
||||
|
||||
div = soup.find('div', {'id' : 'story'})
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
@@ -77,7 +77,8 @@ class AshwinderSycophantHexComAdapter(BaseSiteAdapter):
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
if 'This story contains adult content and/or themes.' in data \
|
||||
or "That password doesn't match the one in our database" in data:
|
||||
or "That password doesn't match the one in our database" in data \
|
||||
or "Member Login" in data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -131,7 +132,7 @@ class AshwinderSycophantHexComAdapter(BaseSiteAdapter):
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -119,7 +119,7 @@ class Asr3SlashzoneOrgAdapter(BaseSiteAdapter):
|
||||
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.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -1,347 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2014 Fanficdownloader team, 2015 FanFicFare 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 bs4.element import Tag, Comment
|
||||
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(cls):
|
||||
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 = self.make_soup(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, 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()
|
||||
# 11. April 2015 - 17:08
|
||||
date = re.sub(r"(\d+\. \D+ \d+ - \d+:\d+).*", r"\1", 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 = self.make_soup(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) != 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 = self.make_soup(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) != 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, unicode(int(curChap) + 1), nextLink)
|
||||
else:
|
||||
break
|
||||
try:
|
||||
data = self._fetchUrl(nextLink)
|
||||
soup = self.make_soup(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, 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, unicode(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] = self.make_soup(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, unicode(i), self.url)
|
||||
nextChapterTitle = re.sub("1", unicode(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 = self.make_soup(data1)
|
||||
#strip comments from soup
|
||||
[comment.extract() for comment in soup.findAll(text=lambda text:isinstance(text, Comment))]
|
||||
|
||||
# get story text
|
||||
storyDiv1 = soup.new_tag("div")
|
||||
for para in soup.find("div", "full-node").find('div', 'content').findAll("p"):
|
||||
storyDiv1.append(para)
|
||||
storyDiv1.append(soup.new_tag("br"))
|
||||
storytext = self.utf8FromSoup(url,storyDiv1)
|
||||
|
||||
return storytext
|
||||
|
||||
|
||||
def getClass():
|
||||
return BdsmGeschichtenAdapter
|
||||
@@ -28,7 +28,7 @@ class BloodshedverseComAdapter(BaseSiteAdapter):
|
||||
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'
|
||||
UPDATED_DATETIME_FORMAT = '%m/%d/%Y %I:%M %p'
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
@@ -168,14 +168,8 @@ class BloodshedverseComAdapter(BaseSiteAdapter):
|
||||
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)
|
||||
date = makeDate(value, self.UPDATED_DATETIME_FORMAT)
|
||||
# ugly %p(am/pm) hack moved into makeDate so other sites can use it.
|
||||
self.story.setMetadata('dateUpdated', date)
|
||||
|
||||
if self.story.getMetadata('rating') == 'NC-17' and not (self.is_adult or self.getConfig('is_adult')):
|
||||
|
||||
@@ -198,7 +198,7 @@ class BloodTiesFansComAdapter(BaseSiteAdapter): # XXX
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
+81
-80
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2013 Fanficdownloader team, 2015 FanFicFare team
|
||||
# Copyright 2016 FanFicFare team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -29,11 +29,11 @@ from .. import exceptions as exceptions
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
def getClass():
|
||||
return ScarHeadNetAdapter
|
||||
return BuffyGilesComAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class ScarHeadNetAdapter(BaseSiteAdapter):
|
||||
class BuffyGilesComAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
@@ -52,10 +52,11 @@ class ScarHeadNetAdapter(BaseSiteAdapter):
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
# XXX Most sites don't have the /efiction part. Replace all to remove it usually.
|
||||
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','shn')
|
||||
self.story.setMetadata('siteabbrev','bufg')
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
@@ -64,14 +65,14 @@ class ScarHeadNetAdapter(BaseSiteAdapter):
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'scarhead.net'
|
||||
return 'buffygiles.velocitygrass.com'
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234"
|
||||
return "http://"+cls.getSiteDomain()+"/efiction/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$"
|
||||
return re.escape("http://"+self.getSiteDomain()+"/efiction/viewstory.php?sid=")+r"\d+$"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
@@ -116,7 +117,7 @@ class ScarHeadNetAdapter(BaseSiteAdapter):
|
||||
# If the title search below fails, there's a good chance
|
||||
# you need a different number. print data at that point
|
||||
# and see what the 'click here to continue' url says.
|
||||
addurl = "&ageconsent=ok&warning=5"
|
||||
addurl = "&warning=5"
|
||||
else:
|
||||
addurl=""
|
||||
|
||||
@@ -143,11 +144,11 @@ class ScarHeadNetAdapter(BaseSiteAdapter):
|
||||
# &warning= -- actually, so do other sites. Must be an
|
||||
# eFiction book.
|
||||
|
||||
# viewstory.php?sid=1882&warning=4
|
||||
# viewstory.php?sid=1654&ageconsent=ok&warning=5
|
||||
# efiction/viewstory.php?sid=1882&warning=4
|
||||
# efiction/viewstory.php?sid=1654&ageconsent=ok&warning=5
|
||||
#print data
|
||||
#m = re.search(r"'viewstory.php\?sid=1882(&warning=4)'",data)
|
||||
m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data)
|
||||
m = re.search(r"'efiction/viewstory.php\?sid=542(&warning=5)'",data)
|
||||
m = re.search(r"'efiction/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
|
||||
@@ -178,10 +179,11 @@ class ScarHeadNetAdapter(BaseSiteAdapter):
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
pagetitle = soup.find('tr',{'valign':'top'})
|
||||
pagetitle = soup.find('div',{'id':'pagetitle'})
|
||||
|
||||
## Title
|
||||
a = pagetitle.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
|
||||
|
||||
self.story.setMetadata('title',stripHTML(a))
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
@@ -193,88 +195,87 @@ class ScarHeadNetAdapter(BaseSiteAdapter):
|
||||
# Find the chapters:
|
||||
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")):
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href']+addurl))
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/efiction/'+chapter['href']+addurl))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
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.
|
||||
|
||||
cats = soup.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
for cat in cats:
|
||||
if '/' == cat.string[0]:
|
||||
self.story.addToList('ships','Harry Potter'+cat.string.split('(')[0])
|
||||
elif 'Harry' in cat.string:
|
||||
self.story.addToList('ships',cat.string.split('(')[0])
|
||||
else:
|
||||
self.story.addToList('category',cat.string)
|
||||
if '(' in cat.string:
|
||||
self.story.addToList('category',cat.string.split('(')[1].split(')')[0])
|
||||
|
||||
|
||||
|
||||
|
||||
chars = soup.findAll('a',href=re.compile(r'browse.php\?type=characters'))
|
||||
for char in chars:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
genres = soup.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2'))
|
||||
for genre in genres:
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
warnings = soup.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1'))
|
||||
for warning in warnings:
|
||||
self.story.addToList('warnings',warning.string)
|
||||
|
||||
textsoup = stripHTML(soup)
|
||||
|
||||
a = textsoup.split('Published: ')[1].split(' ')[0]
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(a), self.dateformat))
|
||||
a = textsoup.split('Updated: ')[1].split(' ')[0]
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(a), self.dateformat))
|
||||
a = textsoup.split('Rating: ')[1].split(' ')[0]
|
||||
self.story.setMetadata('rating', a)
|
||||
a = textsoup.split('Length: ')[1].split('(')[1].split(' ')[0]
|
||||
self.story.setMetadata('numWords', a)
|
||||
a = textsoup.split('Completed: ')[1].split(' ')[0]
|
||||
if 'Yes' in a:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
#a = textsoup.split('Summary: ')[1].split('Add Story to Favorites')[0]
|
||||
#self.setDescription(url,a)
|
||||
|
||||
|
||||
|
||||
a=soup.find(text=re.compile("Summary: "))
|
||||
i=0
|
||||
svalue = ""
|
||||
while i == 0:
|
||||
# utility method
|
||||
def defaultGetattr(d,k):
|
||||
try:
|
||||
b = unicode(a)
|
||||
svalue += b.split('Summary: ')[1]
|
||||
return d[k]
|
||||
except:
|
||||
svalue += unicode(a)
|
||||
if a.nextSibling != None:
|
||||
a = a.nextSibling
|
||||
else:
|
||||
a = a.parent.nextSibling
|
||||
if 'Disclaimer: ' in stripHTML(a):
|
||||
i=1
|
||||
self.setDescription(url,svalue)
|
||||
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 'label' not in defaultGetattr(value,'class'):
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
if 'Rated' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
if 'Categories' in label:
|
||||
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
for cat in cats:
|
||||
self.story.addToList('category',cat.string)
|
||||
|
||||
if 'Characters' in label:
|
||||
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
|
||||
for char in chars:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
if 'Genre' in label:
|
||||
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1'))
|
||||
for genre in genres:
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
if 'Warnings' in label:
|
||||
warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=3'))
|
||||
for warning in warnings:
|
||||
self.story.addToList('warnings',warning.string)
|
||||
|
||||
if 'Completed' in label:
|
||||
if 'Yes' in value:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
if 'Published' in label:
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
if 'Updated' in label:
|
||||
# there's a stray [ at the end.
|
||||
#value = value[0:-1]
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
try:
|
||||
# Find Series name from series URL.
|
||||
a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+"))
|
||||
a = soup.find('a', href=re.compile(r"efiction/viewseries.php\?seriesid=\d+"))
|
||||
series_name = a.string
|
||||
series_url = 'http://'+self.host+'/'+a['href']
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
seriessoup = self.make_soup(self._fetchUrl(series_url))
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$'))
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'^efiction/viewstory.php\?sid=\d+$'))
|
||||
i=1
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
if a['href'] == ('efiction/viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
@@ -283,7 +284,7 @@ class ScarHeadNetAdapter(BaseSiteAdapter):
|
||||
except:
|
||||
# I find it hard to care if the series parsing fails
|
||||
pass
|
||||
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
@@ -1,295 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2013 Fanficdownloader team, 2015 FanFicFare 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 cookielib as cl
|
||||
|
||||
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
|
||||
# This function is called by the downloader in all adapter_*.py files
|
||||
# in this dir to register the adapter class. So it needs to be
|
||||
# updated to reflect the class below it. That, plus getSiteDomain()
|
||||
# take care of 'Registering'.
|
||||
def getClass():
|
||||
return BuffyNFaithNetAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class BuffyNFaithNetAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.setHeader()
|
||||
|
||||
self.decode = ["Windows-1252",
|
||||
"utf8"] # 1252 is a superset of iso-8859-1.
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
self.is_adult=False
|
||||
|
||||
# get storyId from url--url validation guarantees query correct
|
||||
m = re.match(self.getSiteURLPattern(),url)
|
||||
if m:
|
||||
self.story.setMetadata('storyId',m.group('id'))
|
||||
|
||||
# normalized story URL. gets rid of chapter if there, left with ch 1 URL on this site
|
||||
nurl = "http://"+self.getSiteDomain()+"/fanfictions/index.php?act=vie&id="+self.story.getMetadata('storyId')
|
||||
self._setURL(nurl)
|
||||
#argh, this mangles the ampersands I need on metadata['storyUrl']
|
||||
#will set it this way
|
||||
self.story.setMetadata('storyUrl',nurl,condremoveentities=False)
|
||||
else:
|
||||
raise exceptions.InvalidStoryURL(url,
|
||||
self.getSiteDomain(),
|
||||
self.getSiteExampleURLs())
|
||||
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','bnfnet')
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'buffynfaith.net'
|
||||
|
||||
@classmethod
|
||||
def stripURLParameters(cls,url):
|
||||
"Only needs to be overriden if URL contains more than one parameter"
|
||||
## This adapter needs at least two parameters left on the URL, act and id
|
||||
return re.sub(r"(\?act=(vie|ovr)&id=\d+)&.*$",r"\1",url)
|
||||
|
||||
def setHeader(self):
|
||||
"buffynfaith.net wants a Referer for images. Used both above and below(after cookieproc added)"
|
||||
self.opener.addheaders.append(('Referer', 'http://'+self.getSiteDomain()+'/'))
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/fanfictions/index.php?act=vie&id=1234 http://"+cls.getSiteDomain()+"/fanfictions/index.php?act=ovr&id=1234 http://"+cls.getSiteDomain()+"/fanfictions/index.php?act=vie&id=1234&ch=2"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
#http://buffynfaith.net/fanfictions/index.php?act=vie&id=963
|
||||
#http://buffynfaith.net/fanfictions/index.php?act=vie&id=949
|
||||
#http://buffynfaith.net/fanfictions/index.php?act=vie&id=949&ch=2
|
||||
p = re.escape("http://"+self.getSiteDomain()+"/fanfictions/index.php?act=")+\
|
||||
r"(vie|ovr)&id=(?P<id>\d+)(&ch=(?P<ch>\d+))?$"
|
||||
return p
|
||||
|
||||
def use_pagecache(self):
|
||||
'''
|
||||
adapters that will work with the page cache need to implement
|
||||
this and change it to True.
|
||||
'''
|
||||
return True
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
dateformat = "%d %B %Y"
|
||||
url = self.url
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
#set a cookie to get past adult check
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
cookie = cl.Cookie(version=0, name='my_age', value='yes',
|
||||
port=None, port_specified=False,
|
||||
domain=self.getSiteDomain(), domain_specified=False, domain_initial_dot=False,
|
||||
path='/', path_specified=True,
|
||||
secure=False,
|
||||
expires=time.time()+10000,
|
||||
discard=False,
|
||||
comment=None,
|
||||
comment_url=None,
|
||||
rest={'HttpOnly': None},
|
||||
rfc2109=False)
|
||||
self.get_cookiejar().set_cookie(cookie)
|
||||
self.setHeader()
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
#print data
|
||||
|
||||
if "ADULT CONTENT WARNING" in data:
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = self.make_soup(data)
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
#stuff in <head>: description
|
||||
svalue = soup.head.find('meta',attrs={'name':'description'})['content']
|
||||
#self.story.setMetadata('description',svalue)
|
||||
self.setDescription(url,svalue)
|
||||
|
||||
#useful stuff in rest of doc, all contained in this:
|
||||
doc = soup.body.find('div', id='my_wrapper')
|
||||
|
||||
#first the site category (more of a genre to me, meh) and title, in this element:
|
||||
mt = doc.find('div',attrs={'class':'maintitle'})
|
||||
self.story.addToList('genre',mt.findAll('a')[1].string)
|
||||
self.story.setMetadata('title',mt.findAll('a')[1].nextSibling[len(' » '):])
|
||||
del mt
|
||||
|
||||
#the actual category, for me, is 'Buffy: The Vampire Slayer'
|
||||
#self.story.addToList('category','Buffy: The Vampire Slayer')
|
||||
#No need to do it here, it is better to set it in in plugin-defaults.ini and defaults.ini
|
||||
|
||||
#then a block that sits in a table cell like so:
|
||||
#(contains a lot of metadata)
|
||||
mblock = doc.find('td', align='left', width = '70%').contents
|
||||
while len(mblock) > 0:
|
||||
i = mblock.pop(0)
|
||||
if 'Author:' in i.string:
|
||||
#drop empty space
|
||||
mblock.pop(0)
|
||||
#get author link
|
||||
a = mblock.pop(0)
|
||||
authre = re.escape('./index.php?act=bio&id=')+'(?P<authid>\d+)'
|
||||
m = re.match(authre,a['href'])
|
||||
self.story.setMetadata('author',a.string)
|
||||
self.story.setMetadata('authorId',m.group('authid'))
|
||||
authurl = u'http://%s/fanfictions/index.php?act=bio&id=%s' % ( self.getSiteDomain(),
|
||||
self.story.getMetadata('authorId'))
|
||||
self.story.setMetadata('authorUrl',authurl,condremoveentities=False)
|
||||
#drop empty space
|
||||
mblock.pop(0)
|
||||
if 'Rating:' in i.string:
|
||||
self.story.setMetadata('rating',mblock.pop(0).strip())
|
||||
if 'Published:' in i.string:
|
||||
date = mblock.pop(0).strip()
|
||||
#get rid of 'st', 'nd', 'rd', 'th' after day number
|
||||
date = date[0:2]+date[4:]
|
||||
self.story.setMetadata('datePublished',makeDate(date, dateformat))
|
||||
if 'Last Updated:' in i.string:
|
||||
date = mblock.pop(0).strip()
|
||||
#get rid of 'st', 'nd', 'rd', 'th' after day number
|
||||
date = date[0:2]+date[4:]
|
||||
self.story.setMetadata('dateUpdated',makeDate(date, dateformat))
|
||||
if 'Genre:' in i.string:
|
||||
genres = mblock.pop(0).strip()
|
||||
genres = genres.split('/')
|
||||
for genre in genres: self.story.addToList('genre',genre)
|
||||
#end ifs
|
||||
#end while
|
||||
|
||||
# Find the chapter selector
|
||||
select = soup.find('select', { 'name' : 'ch' } )
|
||||
|
||||
if select is None:
|
||||
# no selector found, so it's a one-chapter story.
|
||||
#self.chapterUrls.append((self.story.getMetadata('title'),url))
|
||||
self.chapterUrls.append((self.story.getMetadata('title'),url))
|
||||
else:
|
||||
allOptions = select.findAll('option')
|
||||
for o in allOptions:
|
||||
url = u'http://%s/fanfictions/index.php?act=vie&id=%s&ch=%s' % ( self.getSiteDomain(),
|
||||
self.story.getMetadata('storyId'),
|
||||
o['value'])
|
||||
title = u"%s" % o
|
||||
title = stripHTML(title)
|
||||
ts = title.split(' ',1)
|
||||
title = ts[0]+'. '+ts[1]
|
||||
self.chapterUrls.append((title,url))
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
## Go scrape the rest of the metadata from the author's page.
|
||||
data = self._fetchUrl(self.story.getMetadata('authorUrl'))
|
||||
soup = self.make_soup(data)
|
||||
#find the story link and its parent div
|
||||
storya = soup.find('a',{'href':self.story.getMetadata('storyUrl')})
|
||||
storydiv = storya.parent
|
||||
#warnings come under a <spawn> tag. Never seen that before...
|
||||
#appears to just be a line of freeform text, not necessarily a list
|
||||
#optional
|
||||
spawn = storydiv.find('spawn',{'id':'warnings'})
|
||||
if spawn is not None:
|
||||
warns = spawn.nextSibling.strip()
|
||||
self.story.addToList('warnings',warns)
|
||||
#some meta in spans - this should get all, even the ones jammed in a table
|
||||
spans = storydiv.findAll('span')
|
||||
for s in spans:
|
||||
if s.string == 'Ship:':
|
||||
list = s.nextSibling.strip().split()
|
||||
self.story.extendList('ships',list)
|
||||
if s.string == 'Characters:':
|
||||
list = s.nextSibling.strip().split(',')
|
||||
self.story.extendList('characters',list)
|
||||
if s.string == 'Status:':
|
||||
st = s.nextSibling.strip()
|
||||
self.story.setMetadata('status',st)
|
||||
if s.string == 'Words:':
|
||||
st = s.nextSibling.strip()
|
||||
self.story.setMetadata('numWords',st)
|
||||
|
||||
#reviews - is this worth having?
|
||||
#ffnet adapter gathers it, don't know if anything else does
|
||||
#or if it's ever going to be used!
|
||||
a = storydiv.find('a',{'id':'bold-blue'})
|
||||
if a:
|
||||
revs = a.nextSibling.strip()[1:-1]
|
||||
self.story.setMetadata('reviews',st)
|
||||
else:
|
||||
revs = '0'
|
||||
self.story.setMetadata('reviews',st)
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
soup = self.make_soup(self._fetchUrl(url))
|
||||
|
||||
div = soup.find('div', {'id' : 'fanfiction'})
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
#remove all the unnecessary bookmark tags
|
||||
[s.extract() for s in div('div',{'class':"tiny_box2"})]
|
||||
|
||||
#is there a review link?
|
||||
r = div.find('a',href=re.compile(re.escape("./index.php?act=irv")+".*$"))
|
||||
if r is not None:
|
||||
#remove the review link and its parent div
|
||||
r.parent.extract()
|
||||
|
||||
#There might also be a link to the sequel on the last chapter
|
||||
#I'm inclined to keep it in, but the URL needs to be changed from relative to absolute
|
||||
#Shame there isn't proper series metadata available
|
||||
#(I couldn't find it anyway)
|
||||
s = div.find('a',href=re.compile(re.escape("./index.php?act=ovr")+".*$"))
|
||||
if s is not None:
|
||||
s['href'] = 'http://'+self.getSiteDomain()+'/fanfictions'+s['href'][1:]
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
@@ -106,7 +106,7 @@ class ChaosSycophantHexComAdapter(BaseSiteAdapter):
|
||||
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.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -105,7 +105,7 @@ class CSIForensicsComAdapter(BaseSiteAdapter):
|
||||
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.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -122,7 +122,7 @@ class DestinysGatewayComAdapter(BaseSiteAdapter):
|
||||
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.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -147,7 +147,7 @@ class DokugaComAdapter(BaseSiteAdapter):
|
||||
soup = self.make_soup(data)
|
||||
|
||||
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.")
|
||||
raise exceptions.AccessDenied(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
# print data
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
@@ -161,7 +161,7 @@ class DracoAndGinnyComAdapter(BaseSiteAdapter):
|
||||
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.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -146,7 +146,7 @@ class DramioneOrgAdapter(BaseSiteAdapter):
|
||||
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.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -91,7 +91,7 @@ class EfictionEstelielDeAdapter(BaseSiteAdapter):
|
||||
raise e
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
raise exceptions.AccessDenied(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ class EFPFanFicNet(BaseSiteAdapter):
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
# if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
# raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
# raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -122,7 +122,7 @@ class ErosnSapphoSycophantHexComAdapter(BaseSiteAdapter):
|
||||
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.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -177,7 +177,7 @@ class FanficCastleTVNetAdapter(BaseSiteAdapter): # XXX
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -170,7 +170,7 @@ class FanfictionJunkiesDeAdapter(BaseSiteAdapter): # XXX
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -171,7 +171,7 @@ class FanFiktionDeAdapter(BaseSiteAdapter):
|
||||
if head.find('span',title='Fertiggestellt'):
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In Progress')
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
#find metadata on the author's page
|
||||
asoup = self.make_soup(self._fetchUrl("http://"+self.getSiteDomain()+"?a=q&a1=v&t=nickdetailsstories&lbi=stories&ar=0&nick="+self.story.getMetadata('authorId')))
|
||||
|
||||
@@ -55,7 +55,7 @@ class FicBookNetAdapter(BaseSiteAdapter):
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/readfic/'+self.story.getMetadata('storyId'))
|
||||
self._setURL('https://' + self.getSiteDomain() + '/readfic/'+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','fbn')
|
||||
@@ -71,10 +71,10 @@ class FicBookNetAdapter(BaseSiteAdapter):
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/readfic/12345 http://"+cls.getSiteDomain()+"/readfic/93626/246417#part_content"
|
||||
return "https://"+cls.getSiteDomain()+"/readfic/12345 https://"+cls.getSiteDomain()+"/readfic/93626/246417#part_content"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain()+"/readfic/")+r"\d+"
|
||||
return r"https?://"+re.escape(self.getSiteDomain()+"/readfic/")+r"\d+"
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
@@ -92,39 +92,51 @@ class FicBookNetAdapter(BaseSiteAdapter):
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = self.make_soup(data)
|
||||
|
||||
adult_div = soup.find('div',id='adultCoverWarning')
|
||||
if adult_div:
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
adult_div.extract()
|
||||
else:
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
table = soup.find('td',{'width':'50%'})
|
||||
|
||||
## Title
|
||||
a = soup.find('h1')
|
||||
a = soup.find('section',{'class':'chapter-info'}).find('h1')
|
||||
# kill '+' marks if present.
|
||||
sup = a.find('sup')
|
||||
if sup:
|
||||
sup.extract()
|
||||
self.story.setMetadata('title',stripHTML(a))
|
||||
logger.debug("Title: (%s)"%self.story.getMetadata('title'))
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
a = table.find('a')
|
||||
# assume first avatar-nickname -- there can be a second marked 'beta'.
|
||||
a = soup.find('a',{'class':'avatar-nickname'})
|
||||
self.story.setMetadata('authorId',a.text) # Author's name is unique
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href'])
|
||||
self.story.setMetadata('authorUrl','https://'+self.host+'/'+a['href'])
|
||||
self.story.setMetadata('author',a.text)
|
||||
logger.debug("Author: (%s)"%self.story.getMetadata('author'))
|
||||
|
||||
# Find the chapters:
|
||||
chapters = soup.find('div', {'class' : 'part_list'})
|
||||
chapters = soup.find('ul', {'class' : 'table-of-contents'})
|
||||
if chapters != None:
|
||||
chapters=chapters.findAll('a', href=re.compile(r'/readfic/'+self.story.getMetadata('storyId')+"/\d+#part_content$"))
|
||||
self.story.setMetadata('numChapters',len(chapters))
|
||||
for x in range(0,len(chapters)):
|
||||
chapter=chapters[x]
|
||||
churl='http://'+self.host+chapter['href']
|
||||
churl='https://'+self.host+chapter['href']
|
||||
self.chapterUrls.append((stripHTML(chapter),churl))
|
||||
if x == 0:
|
||||
pubdate = translit.translit(stripHTML(self.make_soup(self._fetchUrl(churl)).find('div', {'class' : 'part_added'}).find('span')))
|
||||
pubdate = translit.translit(stripHTML(chapter.parent.find('span')))
|
||||
# pubdate = translit.translit(stripHTML(self.make_soup(self._fetchUrl(churl)).find('div', {'class' : 'part_added'}).find('span')))
|
||||
if x == len(chapters)-1:
|
||||
update = translit.translit(stripHTML(self.make_soup(self._fetchUrl(churl)).find('div', {'class' : 'part_added'}).find('span')))
|
||||
update = translit.translit(stripHTML(chapter.parent.find('span')))
|
||||
# update = translit.translit(stripHTML(self.make_soup(self._fetchUrl(churl)).find('div', {'class' : 'part_added'}).find('span')))
|
||||
else:
|
||||
self.chapterUrls.append((self.story.getMetadata('title'),url))
|
||||
self.story.setMetadata('numChapters',1)
|
||||
pubdate=translit.translit(stripHTML(soup.find('div', {'class' : 'part_added'}).find('span')))
|
||||
pubdate=translit.translit(stripHTML(soup.find('div',{'class':'title-area'}).find('span')))
|
||||
update=pubdate
|
||||
|
||||
logger.debug("numChapters: (%s)"%self.story.getMetadata('numChapters'))
|
||||
@@ -158,54 +170,63 @@ class FicBookNetAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('dateUpdated', makeDate(update, self.dateformat))
|
||||
self.story.setMetadata('datePublished', makeDate(pubdate, self.dateformat))
|
||||
self.story.setMetadata('language','Russian')
|
||||
|
||||
pr=soup.find('a', href=re.compile(r'/printfic/\w+'))
|
||||
pr='http://'+self.host+pr['href']
|
||||
pr = self.make_soup(self._fetchUrl(pr))
|
||||
pr=pr.findAll('div', {'class' : 'part_text'})
|
||||
|
||||
## after site change, I don't see word count anywhere.
|
||||
# pr=soup.find('a', href=re.compile(r'/printfic/\w+'))
|
||||
# pr='https://'+self.host+pr['href']
|
||||
# pr = self.make_soup(self._fetchUrl(pr))
|
||||
# pr=pr.findAll('div', {'class' : 'part_text'})
|
||||
# i=0
|
||||
# for part in pr:
|
||||
# i=i+len(stripHTML(part).split(' '))
|
||||
# self.story.setMetadata('numWords', unicode(i))
|
||||
|
||||
|
||||
dlinfo = soup.find('dl',{'class':'info'})
|
||||
|
||||
i=0
|
||||
for part in pr:
|
||||
i=i+len(stripHTML(part).split(' '))
|
||||
self.story.setMetadata('numWords', unicode(i))
|
||||
|
||||
i=0
|
||||
fandoms = table.findAll('a', href=re.compile(r'/fanfiction/\w+'))
|
||||
fandoms = dlinfo.find('dd').findAll('a', href=re.compile(r'/fanfiction/\w+'))
|
||||
for fandom in fandoms:
|
||||
self.story.addToList('category',fandom.string)
|
||||
i=i+1
|
||||
if i > 1:
|
||||
self.story.addToList('genre', u'Кроссовер')
|
||||
|
||||
meta=table.findAll('a', href=re.compile(r'/ratings/'))
|
||||
i=0
|
||||
for m in meta:
|
||||
if i == 0:
|
||||
self.story.setMetadata('rating', stripHTML(m))
|
||||
i=1
|
||||
elif i == 1:
|
||||
if not "," in m.nextSibling:
|
||||
i=2
|
||||
self.story.addToList('genre', m.find('b').text)
|
||||
elif i == 2:
|
||||
self.story.addToList('warnings', m.find('b').text)
|
||||
|
||||
|
||||
if table.find('span', {'style' : 'color: green'}):
|
||||
for genre in dlinfo.findAll('a',href=re.compile(r'/genres/')):
|
||||
self.story.addToList('genre',stripHTML(genre))
|
||||
|
||||
ratingdt = dlinfo.find('dt',text='Рейтинг:')
|
||||
self.story.setMetadata('rating', stripHTML(ratingdt.next_sibling))
|
||||
|
||||
# meta=table.findAll('a', href=re.compile(r'/ratings/'))
|
||||
# i=0
|
||||
# for m in meta:
|
||||
# if i == 0:
|
||||
# self.story.setMetadata('rating', stripHTML(m))
|
||||
# i=1
|
||||
# elif i == 1:
|
||||
# if not "," in m.nextSibling:
|
||||
# i=2
|
||||
# self.story.addToList('genre', m.find('b').text)
|
||||
# elif i == 2:
|
||||
# self.story.addToList('warnings', m.find('b').text)
|
||||
|
||||
if dlinfo.find('span', {'style' : 'color: green'}):
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In Progress')
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
|
||||
tags = table.findAll('b')
|
||||
tags = dlinfo.findAll('dt')
|
||||
for tag in tags:
|
||||
label = translit.translit(tag.text)
|
||||
if 'Piersonazhi:' in label or u'Персонажи:' in label:
|
||||
chars=tag.nextSibling.string.split(', ')
|
||||
chars=stripHTML(tag.next_sibling).split(', ')
|
||||
for char in chars:
|
||||
self.story.addToList('characters',char)
|
||||
break
|
||||
|
||||
summary=soup.find('span', {'class' : 'urlize'})
|
||||
summary=soup.find('div', {'class' : 'urlize'})
|
||||
self.setDescription(url,summary)
|
||||
#self.story.setMetadata('description', summary.text)
|
||||
|
||||
|
||||
@@ -188,6 +188,7 @@ class FictionAlleyOrgSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
for small in storydd.findAll('small'):
|
||||
small.extract() ## removes the <small> tags, leaving only the summary.
|
||||
storydd.name = 'div' ## change tag name else Calibre treats it oddly.
|
||||
self.setDescription(url,storydd)
|
||||
#self.story.setMetadata('description',stripHTML(storydd))
|
||||
|
||||
@@ -203,11 +204,14 @@ class FictionAlleyOrgSiteAdapter(BaseSiteAdapter):
|
||||
# Yes, it's an evil kludge, but what can ya do? Using
|
||||
# something other than div prevents soup from pairing
|
||||
# our div with poor html inside the story text.
|
||||
data = data.replace('<!-- headerend -->','<crazytagstringnobodywouldstumbleonaccidently id="storytext">').replace('<!-- footerstart -->','</crazytagstringnobodywouldstumbleonaccidently>')
|
||||
crazy = "crazytagstringnobodywouldstumbleonaccidently"
|
||||
data = data.replace('<!-- headerend -->','<'+crazy+' id="storytext">').replace('<!-- footerstart -->','</'+crazy+'>')
|
||||
|
||||
# problems with some stories confusing Soup. This is a nasty
|
||||
# hack, but it works.
|
||||
data = data[data.index("<crazytagstringnobodywouldstumbleonaccidently"):]
|
||||
data = data[data.index('<'+crazy+''):]
|
||||
# ditto with extra crap at the end.
|
||||
data = data[:data.index('</'+crazy+'>')+len('</'+crazy+'>')]
|
||||
|
||||
soup = self.make_soup(data)
|
||||
body = soup.findAll('body') ## some stories use a nested body and body
|
||||
@@ -218,7 +222,7 @@ class FictionAlleyOrgSiteAdapter(BaseSiteAdapter):
|
||||
text = body[1]
|
||||
text.name='div' # force to be a div to avoid multiple body tags.
|
||||
else:
|
||||
text = soup.find('crazytagstringnobodywouldstumbleonaccidently', {'id' : 'storytext'})
|
||||
text = soup.find(crazy, {'id' : 'storytext'})
|
||||
text.name='div' # change to div tag.
|
||||
|
||||
if not data or not text:
|
||||
|
||||
@@ -272,7 +272,7 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
#groups
|
||||
if soup.find('button', {'id':'button-view-all-groups'}):
|
||||
groupResponse = self._fetchUrl("http://www.fimfiction.net/ajax/groups/story_groups_list.php?story=%s" % (self.story.getMetadata("storyId")))
|
||||
groupResponse = self._fetchUrl("https://www.fimfiction.net/ajax/stories/%s/groups" % (self.story.getMetadata("storyId")))
|
||||
groupData = json.loads(groupResponse)
|
||||
groupList = self.make_soup(groupData["content"])
|
||||
else:
|
||||
|
||||
@@ -15,279 +15,32 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
from adapter_storiesonlinenet import StoriesOnlineNetAdapter
|
||||
|
||||
def getClass():
|
||||
return FineStoriesComAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class FineStoriesComAdapter(BaseSiteAdapter):
|
||||
class FineStoriesComAdapter(StoriesOnlineNetAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
self.is_adult=False
|
||||
|
||||
# get storyId from url
|
||||
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2].split(':')[0])
|
||||
if 'storyInfo' in self.story.getMetadata('storyId'):
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/s/storyInfo.php?id='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','fnst')
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%Y-%m-%d"
|
||||
@classmethod
|
||||
def getSiteAbbrev(self):
|
||||
return 'fnst'
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'finestories.com'
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/s/1234 http://"+cls.getSiteDomain()+"/s/1234:4010 http://"+cls.getSiteDomain()+"/library/storyInfo.php?id=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain())+r"/(s|library)?/(storyInfo.php\?id=)?\d+(:\d+)?(;\d+)?$"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
if 'Free Registration' in data \
|
||||
or "Log In" in data \
|
||||
or "Invalid Password!" in data \
|
||||
or "Invalid User Name!" in data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def performLogin(self, url):
|
||||
params = {}
|
||||
|
||||
if self.password:
|
||||
params['theusername'] = self.username
|
||||
params['thepassword'] = self.password
|
||||
else:
|
||||
params['theusername'] = self.getConfig("username")
|
||||
params['thepassword'] = self.getConfig("password")
|
||||
params['rememberMe'] = '1'
|
||||
params['page'] = 'http://'+self.getSiteDomain()+'/'
|
||||
params['submit'] = 'Login'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/login.php'
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['theusername']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "My Account" not in d : #Member Account
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['theusername']))
|
||||
raise exceptions.FailedToLogin(url,params['theusername'])
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
if self.needToLoginCheck(data):
|
||||
# need to log in for this one.
|
||||
self.performLogin(url)
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = self.make_soup(data)
|
||||
# print data
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
## Title
|
||||
a = soup.find('a', href=re.compile(r'/s/'+self.story.getMetadata('storyId')))
|
||||
self.story.setMetadata('title',stripHTML(a))
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
a = soup.find('a', href=re.compile(r"/a/\w+"))
|
||||
self.story.setMetadata('authorId',a['href'].split('/')[2])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+a['href'])
|
||||
self.story.setMetadata('author',a.text)
|
||||
|
||||
# Find the chapters:
|
||||
chapters = soup.findAll('a', href=re.compile(r'/s/'+self.story.getMetadata('storyId')+":\d+$"))
|
||||
if len(chapters) != 0:
|
||||
for chapter in chapters:
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+chapter['href']))
|
||||
else:
|
||||
self.chapterUrls.append((self.story.getMetadata('title'),'http://'+self.host+'/s/'+self.story.getMetadata('storyId')))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
# surprisingly, the detailed page does not give enough details, so go to author's page
|
||||
|
||||
skip=0
|
||||
i=0
|
||||
while i == 0:
|
||||
asoup = self.make_soup(self._fetchUrl(self.story.getMetadata('authorUrl')+"&skip="+unicode(skip)))
|
||||
|
||||
tds = asoup.findAll('td', {'class' : 'lc2'})
|
||||
for lc2 in tds:
|
||||
if lc2.find('a', href=re.compile(r'/s/'+self.story.getMetadata('storyId'))):
|
||||
i=1
|
||||
break
|
||||
if tds[len(tds)-1] == lc2:
|
||||
skip=skip+10
|
||||
|
||||
for cat in lc2.findAll('div', {'class' : 'typediv'}):
|
||||
self.story.addToList('category',cat.text)
|
||||
|
||||
self.story.setMetadata('size', lc2.findNext('td', {'class' : 'num'}).text)
|
||||
|
||||
lc4 = lc2.findNext('td', {'class' : 'lc4'})
|
||||
|
||||
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(a.text, i)
|
||||
self.story.setMetadata('seriesUrl','http://'+self.host+a['href'])
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
a = lc4.find('a', href=re.compile(r"/library/universe.php\?id=\d+"))
|
||||
self.story.addToList("category",a.text)
|
||||
except:
|
||||
pass
|
||||
|
||||
for a in lc4.findAll('span', {'class' : 'help'}) + lc4.findAll('script'):
|
||||
a.extract()
|
||||
|
||||
self.setDescription('http://'+self.host+'/s/'+self.story.getMetadata('storyId'),lc4.text.split('[More Info')[0])
|
||||
|
||||
for b in lc4.findAll('b'):
|
||||
label = b.text
|
||||
value = b.nextSibling
|
||||
|
||||
if 'For Age' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
if 'Tags' in label:
|
||||
for genre in value.split(', '):
|
||||
self.story.addToList('genre',genre)
|
||||
|
||||
## Site uses a <script> to inject timestamp in locale plus <noscript> general version.
|
||||
if 'Posted' in label:
|
||||
value = b.find_next_sibling('noscript')
|
||||
if '(' in value:
|
||||
date = makeDate(stripHTML(value.split(' (')[0]), self.dateformat)
|
||||
else:
|
||||
date = makeDate(stripHTML(value), self.dateformat)
|
||||
self.story.setMetadata('datePublished', date)
|
||||
self.story.setMetadata('dateUpdated', date)
|
||||
|
||||
if 'Concluded' in label or 'Updated' in label:
|
||||
value = b.find_next_sibling('noscript')
|
||||
if '(' in value:
|
||||
date = makeDate(stripHTML(value.split(' (')[0]), self.dateformat)
|
||||
else:
|
||||
date = makeDate(stripHTML(value), self.dateformat)
|
||||
self.story.setMetadata('dateUpdated', date)
|
||||
|
||||
status = lc4.find('span', {'class' : 'ab'})
|
||||
if status != None:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
if "Last Activity" in status.text:
|
||||
self.story.setMetadata('dateUpdated', makeDate(status.text.split('Activity: ')[1].split(')')[0], self.dateformat))
|
||||
else:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
soup = self.make_soup(self._fetchUrl(url))
|
||||
|
||||
div = soup.find('article')
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
# some big chapters are split over several pages
|
||||
pager = div.find('span', {'class' : 'pager'})
|
||||
if pager != None:
|
||||
urls=pager.findAll('a')
|
||||
urls=urls[:len(urls)-1]
|
||||
|
||||
for ur in urls:
|
||||
soup = self.make_soup(self._fetchUrl("http://"+self.getSiteDomain()+ur['href']))
|
||||
|
||||
div1 = soup.find('article')
|
||||
|
||||
#print("div.contents:%s"%(div.contents,))
|
||||
# appending next section
|
||||
last=div.findAll('p')
|
||||
next=div1.find('span', {'class' : 'conTag'}).nextSibling
|
||||
last[len(last)-1]=last[len(last)-1].append(next)
|
||||
|
||||
self.clean_chapter(div1)
|
||||
#print("div.contents:%s"%(div.contents,))
|
||||
for t in div1.contents:
|
||||
div.append(t)
|
||||
|
||||
self.clean_chapter(div)
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
def clean_chapter(self,art):
|
||||
# discard included chapter heading.
|
||||
# discard included date
|
||||
# discard share block
|
||||
# continued/continues
|
||||
# discard next link
|
||||
for tag in art.find_all('h2') + \
|
||||
art.find_all('div', class_="date") + \
|
||||
art.find_all('div', class_="vform") + \
|
||||
art.find_all('span', class_="conTag") + \
|
||||
art.find_all('h3', class_="end"):
|
||||
tag.extract()
|
||||
|
||||
# remove pager blocks.
|
||||
for pager in art.find_all('span', class_="pager"):
|
||||
# remove br tags before and after pager.
|
||||
#print("br list prev: %s"%len(pager.find_previous_siblings('br')))
|
||||
#print("br list next: %s"%len(pager.find_next_siblings('br')))
|
||||
for tag in pager.find_next_siblings('br')[:2] + pager.find_previous_siblings('br')[:2]:
|
||||
tag.extract()
|
||||
pager.extract()
|
||||
|
||||
@@ -86,7 +86,7 @@ class HarryPotterFanFictionComSiteAdapter(BaseSiteAdapter):
|
||||
raise e
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -90,7 +90,7 @@ class HLFictionNetAdapter(BaseSiteAdapter):
|
||||
raise e
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -136,10 +136,11 @@ class HPFandomNetAdapterAdapter(BaseSiteAdapter): # XXX
|
||||
|
||||
authsoup = self.make_soup(authdata)
|
||||
|
||||
reviewsa = authsoup.find('a', href=re.compile(r"reviews\.php\?sid="+self.story.getMetadata('storyId')+r".*"))
|
||||
reviewsa = authsoup.find_all('a', href=re.compile(r"reviews\.php\?sid="+self.story.getMetadata('storyId')+r".*"))
|
||||
# <table><tr><td><p><b><a ...>
|
||||
metablock = reviewsa.findParent("table")
|
||||
metablock = reviewsa[0].findParent("table")
|
||||
#print("metablock:%s"%metablock)
|
||||
self.story.setMetadata('reviews',stripHTML(reviewsa[-1]))
|
||||
|
||||
## Title
|
||||
titlea = metablock.find('a', href=re.compile("viewstory.php"))
|
||||
@@ -148,6 +149,7 @@ class HPFandomNetAdapterAdapter(BaseSiteAdapter): # XXX
|
||||
raise exceptions.FailedToDownload("Story URL (%s) not found on author's page, can't use chapter URLs"%url)
|
||||
self.story.setMetadata('title',stripHTML(titlea))
|
||||
|
||||
total_reads = 0
|
||||
# Find the chapters: !!! hpfandom.net differs from every other
|
||||
# eFiction site--the sid on viewstory for chapters is
|
||||
# *different* for each chapter
|
||||
@@ -156,10 +158,16 @@ class HPFandomNetAdapterAdapter(BaseSiteAdapter): # XXX
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
#print("====chapter===%s"%m.group(1))
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/eff/'+m.group(1)))
|
||||
|
||||
try:
|
||||
total_reads += int(stripHTML((chapter.find_parent('tr').find_all('td')[-1])))
|
||||
except:
|
||||
pass # don't care
|
||||
if len(self.chapterUrls) == 0:
|
||||
self.chapterUrls.append((stripHTML(self.story.getMetadata('title')),url))
|
||||
|
||||
if total_reads > 0:
|
||||
self.story.setMetadata('reads',total_reads)
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
# eFiction sites don't help us out a lot with their meta data
|
||||
|
||||
@@ -91,7 +91,7 @@ class HPFanficArchiveComAdapter(BaseSiteAdapter):
|
||||
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -173,7 +173,7 @@ class IkEternalNetAdapter(BaseSiteAdapter):
|
||||
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.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -161,7 +161,7 @@ class ImagineEFicComAdapter(BaseSiteAdapter):
|
||||
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.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -148,7 +148,7 @@ class KSArchiveComAdapter(BaseSiteAdapter): # XXX
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -21,7 +21,6 @@ logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
import urlparse
|
||||
import time
|
||||
|
||||
from bs4.element import Comment
|
||||
from ..htmlcleanup import stripHTML
|
||||
@@ -33,13 +32,15 @@ class LiteroticaSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
logger.debug("LiteroticaComAdapter:__init__ - url='%s'" % 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.
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','litero')
|
||||
|
||||
# normalize to first chapter. Not sure if they ever have more than 2 digits.
|
||||
@@ -62,7 +63,7 @@ class LiteroticaSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = '%m/%d/%y'
|
||||
self.dateformat = "%m/%d/%y"
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
@@ -96,6 +97,18 @@ class LiteroticaSiteAdapter(BaseSiteAdapter):
|
||||
def getSiteURLPattern(self):
|
||||
return r"https?://(www|german|spanish|french|dutch|italian|romanian|portuguese|other)(\.i)?\.literotica\.com/s/([a-zA-Z0-9_-]+)"
|
||||
|
||||
def getCategories(self, soup):
|
||||
if self.getConfig("use_meta_keywords"):
|
||||
categories = soup.find("meta", {"name":"keywords"})['content'].split(', ')
|
||||
categories = [c for c in categories if not self.story.getMetadata('title') in c]
|
||||
if self.story.getMetadata('author') in categories:
|
||||
categories.remove(self.story.getMetadata('author'))
|
||||
logger.debug("Meta = %s" % categories)
|
||||
for category in categories:
|
||||
# logger.debug("\tCategory=%s" % category)
|
||||
# self.story.addToList('category', category.title())
|
||||
self.story.addToList('eroticatags', category.title())
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
"""
|
||||
NOTE: Some stories can have versions,
|
||||
@@ -119,6 +132,7 @@ class LiteroticaSiteAdapter(BaseSiteAdapter):
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
logger.debug("Chapter/Story URL: <%s> " % self.url)
|
||||
|
||||
try:
|
||||
data1 = self._fetchUrl(self.url)
|
||||
soup1 = self.make_soup(data1)
|
||||
@@ -145,6 +159,7 @@ class LiteroticaSiteAdapter(BaseSiteAdapter):
|
||||
soupAuth = self.make_soup(dataAuth)
|
||||
#strip comments from soup
|
||||
[comment.extract() for comment in soupAuth.findAll(text=lambda text:isinstance(text, Comment))]
|
||||
# logger.debug(soupAuth)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(authorurl)
|
||||
@@ -155,6 +170,15 @@ class LiteroticaSiteAdapter(BaseSiteAdapter):
|
||||
## site has started using //domain.name/asdf urls remove https?: from front
|
||||
## site has started putting https back on again.
|
||||
storyLink = soupAuth.find('a', href=re.compile(r'(https?:)?'+re.escape(self.url[self.url.index(':')+1:])))
|
||||
# storyLink = soupAuth.find('a', href=self.url)#[self.url.index(':')+1:])
|
||||
|
||||
if storyLink is not None:
|
||||
# pull the published date from the author page
|
||||
# default values from single link. Updated below if multiple chapter.
|
||||
logger.debug("Found story on the author page.")
|
||||
date = storyLink.parent.parent.findAll('td')[-1].text
|
||||
self.story.setMetadata('datePublished', makeDate(date, self.dateformat))
|
||||
self.story.setMetadata('dateUpdated',makeDate(date, self.dateformat))
|
||||
|
||||
if storyLink is not None:
|
||||
urlTr = storyLink.parent.parent
|
||||
@@ -166,101 +190,184 @@ class LiteroticaSiteAdapter(BaseSiteAdapter):
|
||||
raise exceptions.FailedToDownload("Couldn't find story <%s> on author's page <%s>" % (self.url, authorurl))
|
||||
|
||||
if isSingleStory:
|
||||
self.story.setMetadata('title', storyLink.text)
|
||||
# self.chapterUrls = [(soup1.h1.string, self.url)]
|
||||
# self.story.setMetadata('title', soup1.h1.string)
|
||||
|
||||
self.story.setMetadata('title', storyLink.text.strip('/'))
|
||||
logger.debug('Title: "%s"' % storyLink.text.strip('/'))
|
||||
self.story.setMetadata('description', urlTr.findAll("td")[1].text)
|
||||
self.story.addToList('eroticatags', urlTr.findAll("td")[2].text)
|
||||
self.story.addToList('category', urlTr.findAll("td")[2].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)]
|
||||
averrating = stripHTML(storyLink.parent)
|
||||
## title (0.00)
|
||||
averrating = averrating[averrating.rfind('(')+1:averrating.rfind(')')]
|
||||
try:
|
||||
self.story.setMetadata('averrating', float(averrating))
|
||||
except:
|
||||
pass
|
||||
# self.story.setMetadata('averrating',averrating)
|
||||
# parse out the list of chapters
|
||||
else:
|
||||
seriesTr = urlTr.previousSibling
|
||||
while 'ser-ttl' not in seriesTr['class']:
|
||||
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'))
|
||||
seriesTitle = m.group('title')
|
||||
|
||||
## Walk the chapters
|
||||
chapterTr = seriesTr.nextSibling
|
||||
self.chapterUrls = []
|
||||
dates = []
|
||||
descriptions = []
|
||||
ratings = []
|
||||
chapters = []
|
||||
while chapterTr is not None and 'sl' in chapterTr['class']:
|
||||
descriptions.append(chapterTr.findAll("td")[1].text)
|
||||
description = "%d. %s" % (len(descriptions)+1,stripHTML(chapterTr.findAll("td")[1]))
|
||||
description = stripHTML(chapterTr.findAll("td")[1])
|
||||
chapterLink = chapterTr.find("td", "fc").find("a")
|
||||
if not chapterLink["href"].startswith('http'):
|
||||
chapterLink["href"] = "http:" + chapterLink["href"]
|
||||
self.chapterUrls.append((chapterLink.text, chapterLink["href"]))
|
||||
self.story.addToList('eroticatags', chapterTr.findAll("td")[2].text)
|
||||
dates.append(makeDate(chapterTr.findAll('td')[-1].text, self.dateformat))
|
||||
pub_date = makeDate(chapterTr.findAll('td')[-1].text, self.dateformat)
|
||||
dates.append(pub_date)
|
||||
chapterTr = chapterTr.nextSibling
|
||||
|
||||
chapter_title = chapterLink.text
|
||||
if self.getConfig("clean_chapter_titles"):
|
||||
logger.debug('\tChapter Name: "%s"' % chapterLink.string)
|
||||
logger.debug('\tChapter Name: "%s"' % chapterLink.text)
|
||||
if chapterLink.text.lower().startswith(seriesTitle.lower()):
|
||||
chapter = chapterLink.text[len(seriesTitle):].strip()
|
||||
logger.debug('\tChapter: "%s"' % chapter)
|
||||
if chapter == '':
|
||||
chapter_title = 'Chapter %d' % (len(self.chapterUrls) + 1)
|
||||
else:
|
||||
separater_char = chapter[0]
|
||||
logger.debug('\tseparater_char: "%s"' % separater_char)
|
||||
chapter = chapter[1:].strip() if separater_char in [":", "-"] else chapter
|
||||
logger.debug('\tChapter: "%s"' % chapter)
|
||||
if chapter.lower().startswith('ch.'):
|
||||
chapter = chapter[len('ch.'):]
|
||||
try:
|
||||
chapter_title = 'Chapter %d' % int(chapter)
|
||||
except:
|
||||
chapter_title = 'Chapter %s' % chapter
|
||||
elif chapter.lower().startswith('pt.'):
|
||||
chapter = chapter[len('pt.'):]
|
||||
try:
|
||||
chapter_title = 'Part %d' % int(chapter)
|
||||
except:
|
||||
chapter_title = 'Part %s' % chapter
|
||||
elif separater_char in [":", "-"]:
|
||||
chapter_title = chapter
|
||||
|
||||
# if chapter_title == '':
|
||||
# chapter_title = chapterLink.string
|
||||
|
||||
## Set description to joint chapter descriptions
|
||||
self.story.setMetadata('description', " / ".join(descriptions))
|
||||
# pages include full URLs.
|
||||
chapurl = chapterLink['href']
|
||||
if chapurl.startswith('//'):
|
||||
chapurl = self.parsedUrl.scheme + ':' + chapurl
|
||||
logger.debug("Chapter URL: " + chapurl)
|
||||
logger.debug("Chapter Title: " + chapter_title)
|
||||
logger.debug("Chapter description: " + description)
|
||||
chapters.append((chapter_title, chapurl, description, pub_date))
|
||||
# self.chapterUrls.append((chapter_title, chapurl))
|
||||
numrating = stripHTML(chapterLink.parent)
|
||||
## title (0.00)
|
||||
numrating = numrating[numrating.rfind('(')+1:numrating.rfind(')')]
|
||||
try:
|
||||
ratings.append(float(numrating))
|
||||
except:
|
||||
pass
|
||||
|
||||
chapters = sorted(chapters, key=lambda chapter: chapter[3])
|
||||
for i, chapter in enumerate(chapters):
|
||||
self.chapterUrls.append((chapter[0], chapter[1]))
|
||||
descriptions.append("%d. %s" % (i + 1, chapter[2]))
|
||||
## 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])
|
||||
self.story.setMetadata('datePublished', chapters[0][3])
|
||||
self.story.setMetadata('dateUpdated', chapters[-1][3])
|
||||
## Set description to joint chapter descriptions
|
||||
self.setDescription(authorurl,"<p>"+"</p>\n<p>".join(descriptions)+"</p>")
|
||||
|
||||
# normalize on first chapter URL.
|
||||
self._setURL(self.chapterUrls[0][1])
|
||||
if len(ratings) > 0:
|
||||
self.story.setMetadata('averrating','%4.2f' % (sum(ratings) / float(len(ratings))))
|
||||
|
||||
# normalize on first chapter URL.
|
||||
self._setURL(self.chapterUrls[0][1])
|
||||
|
||||
# reset storyId to first chapter.
|
||||
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
|
||||
|
||||
self.story.setMetadata('numChapters', len(self.chapterUrls))
|
||||
|
||||
# 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()))
|
||||
self.story.setMetadata('category', soup1.find('div', 'b-breadcrumbs').findAll('a')[1].string)
|
||||
self.getCategories(soup1)
|
||||
# self.story.setMetadata('description', soup1.find('meta', {'name': 'description'})['content'])
|
||||
|
||||
return
|
||||
|
||||
|
||||
def getPageText(self, raw_page, url):
|
||||
logger.debug('Getting page text')
|
||||
# logger.debug(soup)
|
||||
raw_page = raw_page.replace('<div class="b-story-body-x x-r15"><div><p>','<div class="b-story-body-x x-r15"><div>')
|
||||
# logger.debug("\tChapter text: %s" % raw_page)
|
||||
page_soup = self.make_soup(raw_page)
|
||||
[comment.extract() for comment in page_soup.findAll(text=lambda text:isinstance(text, Comment))]
|
||||
story2 = page_soup.find('div', 'b-story-body-x').div
|
||||
# logger.debug("getPageText- name div div...")
|
||||
# logger.debug(soup)
|
||||
# story2.append(page_soup.new_tag('br'))
|
||||
div = self.utf8FromSoup(url, story2)
|
||||
# logger.debug(div)
|
||||
|
||||
fullhtml = unicode(div)
|
||||
# logger.debug(fullhtml)
|
||||
fullhtml = re.sub(r'<br />\s*<br />', r'</p><p>', fullhtml)
|
||||
fullhtml = re.sub(r'^<div>', r'', fullhtml)
|
||||
fullhtml = re.sub(r'</div>$', r'', fullhtml)
|
||||
fullhtml = re.sub(r'(<p><br/></p>\s+)+$', r'', fullhtml)
|
||||
# logger.debug(fullhtml)
|
||||
return fullhtml
|
||||
|
||||
def getChapterText(self, url):
|
||||
logger.debug('Getting chapter text from <%s>' % url)
|
||||
data1 = self._fetchUrl(url)
|
||||
# brute force approach to replace the wrapping <p> tag. If
|
||||
# done by changing tag name, it causes problems with nested
|
||||
# <p> tags.
|
||||
data1 = data1.replace('<div class="b-story-body-x x-r15"><div><p>','<div class="b-story-body-x x-r15"><div>')
|
||||
soup1 = self.make_soup(data1)
|
||||
|
||||
#strip comments from soup
|
||||
[comment.extract() for comment in soup1.findAll(text=lambda text:isinstance(text, Comment))]
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
# get story text
|
||||
story1 = soup1.find('div', 'b-story-body-x').div
|
||||
#print("story1:%s"%story1)
|
||||
# story1.name='div'
|
||||
story1.append(soup1.new_tag('br'))
|
||||
storytext = self.utf8FromSoup(url,story1)
|
||||
raw_page = self._fetchUrl(url)
|
||||
page_soup = self.make_soup(raw_page)
|
||||
pages = page_soup.find('select', {'name' : 'page'})
|
||||
page_nums = [page.text for page in pages.findAll('option')] if pages else 0
|
||||
|
||||
# find num pages
|
||||
pgs = int(soup1.find("span", "b-pager-caption-t r-d45").string.split(' ')[0])
|
||||
logger.debug("pages: "+unicode(pgs))
|
||||
fullhtml = ""
|
||||
self.getCategories(page_soup)
|
||||
if self.getConfig("description_in_chapter"):
|
||||
chapter_description = page_soup.find("meta", {"name" : "description"})['content']
|
||||
logger.debug("\tChapter description: %s" % chapter_description)
|
||||
fullhtml += '<p><b>Description:</b> %s</p><hr />' % chapter_description
|
||||
fullhtml += self.getPageText(raw_page, url)
|
||||
if pages:
|
||||
for page_no in xrange(2, len(page_nums) + 1):
|
||||
page_url = url + "?page=%s" % page_no
|
||||
logger.debug("page_url= %s" % page_url)
|
||||
raw_page = self._fetchUrl(page_url)
|
||||
fullhtml += self.getPageText(raw_page, url)
|
||||
|
||||
# fullhtml = self.utf8FromSoup(url, bs.BeautifulSoup(fullhtml))
|
||||
# fullhtml = re.sub(r'^<div>', r'', fullhtml)
|
||||
# fullhtml = re.sub(r'</div>$', r'', fullhtml)
|
||||
# if None == div:
|
||||
# raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
# get all the pages
|
||||
for i in xrange(2, pgs+1):
|
||||
try:
|
||||
logger.debug("fetching page "+unicode(i))
|
||||
time.sleep(0.5)
|
||||
data2 = self._fetchUrl(url, {'page': i})
|
||||
# brute force approach to replace the wrapping <p> tag. If
|
||||
# done by changing tag name, it causes problems with nested
|
||||
# <p> tags.
|
||||
data2 = data2.replace('<div class="b-story-body-x x-r15"><div><p>','<div class="b-story-body-x x-r15"><div>')
|
||||
soup2 = self.make_soup(data2)
|
||||
[comment.extract() for comment in soup2.findAll(text=lambda text:isinstance(text, Comment))]
|
||||
story2 = soup2.find('div', 'b-story-body-x').div
|
||||
# story2.name='div'
|
||||
story2.append(soup2.new_tag('br'))
|
||||
storytext += self.utf8FromSoup(url,story2)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(url)
|
||||
else:
|
||||
raise e
|
||||
return storytext
|
||||
return fullhtml
|
||||
|
||||
|
||||
def getClass():
|
||||
|
||||
+43
-53
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2012 Fanficdownloader team, 2015 FanFicFare team
|
||||
# Copyright 2016 FanFicFare team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -22,18 +22,18 @@ logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
|
||||
from bs4.element import Comment
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
def getClass():
|
||||
return GrangerEnchantedCom
|
||||
return Lucifaelff
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
class Lucifaelff(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
@@ -50,37 +50,28 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
|
||||
self.section=self.parsedUrl.path.split('/',)[1]
|
||||
|
||||
# normalized story URL.
|
||||
if "malfoymanor" in self.parsedUrl.netloc:
|
||||
self._setURL('http://malfoymanor.' + self.getSiteDomain() + '/themanor/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
self.story.addToList("category","The Manor")
|
||||
else:
|
||||
self._setURL('http://' + self.getSiteDomain() + '/enchant/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','gech')
|
||||
self.story.setMetadata('siteabbrev','luci')
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%d/%b/%Y"
|
||||
self.dateformat = "%d/%m/%Y"
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'grangerenchanted.com'
|
||||
|
||||
@classmethod
|
||||
def getAcceptDomains(cls):
|
||||
return ['grangerenchanted.com','malfoymanor.grangerenchanted.com']
|
||||
return 'fanfiction.lucifael.com'
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://grangerenchanted.com/enchant/viewstory.php?sid=1234 http://malfoymanor.grangerenchanted.com/themanor/viewstory.php?sid=1234"
|
||||
return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return r"http://(malfoymanor.)?grangerenchanted.com/(enchant|themanor)?/viewstory.php\?sid=\d+$"
|
||||
return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
@@ -103,10 +94,7 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
params['cookiecheck'] = '1'
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
if "enchant" in self.section:
|
||||
loginUrl = 'http://grangerenchanted.com/enchant/user.php?action=login'
|
||||
else:
|
||||
loginUrl = 'http://malfoymanor.grangerenchanted.com/themanor/user.php?action=login'
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/user.php?action=login'
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
@@ -128,13 +116,13 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
# If the title search below fails, there's a good chance
|
||||
# you need a different number. print data at that point
|
||||
# and see what the 'click here to continue' url says.
|
||||
addurl = "&ageconsent=ok&warning=1"
|
||||
addurl = "&ageconsent=ok&warning=4"
|
||||
else:
|
||||
addurl=""
|
||||
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+addurl
|
||||
url = self.url
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
@@ -145,11 +133,21 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
else:
|
||||
raise e
|
||||
|
||||
if self.needToLoginCheck(data):
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
# need to log in for this one.
|
||||
self.performLogin(url)
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
# Since the warning text can change by warning level, let's
|
||||
# look for the warning pass url. ksarchive uses
|
||||
# &warning= -- actually, so do other sites. Must be an
|
||||
# eFiction book.
|
||||
|
||||
# viewstory.php?sid=1882&warning=4
|
||||
# viewstory.php?sid=1654&ageconsent=ok&warning=5
|
||||
#print data
|
||||
#m = re.search(r"'viewstory.php\?sid=1882(&warning=4)'",data)
|
||||
m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data)
|
||||
if m != None:
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
@@ -181,12 +179,14 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
pagetitle = soup.find('div',{'id':'pagetitle'})
|
||||
|
||||
## Title
|
||||
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
|
||||
a = pagetitle.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
|
||||
self.story.setMetadata('title',stripHTML(a))
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
|
||||
a = pagetitle.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
|
||||
self.story.setMetadata('authorId',a['href'].split('=')[1])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href'])
|
||||
self.story.setMetadata('author',a.string)
|
||||
@@ -194,7 +194,7 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
# Find the chapters:
|
||||
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")):
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+self.section+'/'+chapter['href']+addurl))
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href']+addurl))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
@@ -217,10 +217,11 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
if 'Summary' in label:
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while value and 'label' not in defaultGetattr(value,'class') and '<span class="label">' not in unicode(value):
|
||||
while 'label' not in defaultGetattr(value,'class'):
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
if 'Rated' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
@@ -228,9 +229,6 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
if 'Read' in label:
|
||||
self.story.setMetadata('read', value)
|
||||
|
||||
if 'Categories' in label:
|
||||
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
for cat in cats:
|
||||
@@ -242,12 +240,12 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
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=4'))
|
||||
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1'))
|
||||
for genre in genres:
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
if 'Warnings' in label:
|
||||
warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2'))
|
||||
warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=3'))
|
||||
for warning in warnings:
|
||||
self.story.addToList('warnings',warning.string)
|
||||
|
||||
@@ -261,35 +259,27 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
if 'Updated' in label:
|
||||
# there's a stray [ at the end.
|
||||
#value = value[0:-1]
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
try:
|
||||
# Find Series name from series URL.
|
||||
a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+"))
|
||||
series_name = a.string
|
||||
series_url = 'http://'+self.host+'/'+self.section+'/'+a['href']
|
||||
series_url = 'http://'+self.host+'/'+a['href']
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
seriessoup = self.make_soup(self._fetchUrl(series_url))
|
||||
# can't use ^viewstory...$ in case of higher rated stories with javascript href.
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+'))
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$'))
|
||||
i=1
|
||||
for a in storyas:
|
||||
# skip 'report this' and 'TOC' links
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
except:
|
||||
# I find it hard to care if the series parsing fails
|
||||
pass
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
try:
|
||||
self.story.setMetadata('reviews',
|
||||
stripHTML(soup.find('div',{'id':'sort'}).
|
||||
findAll('a', href=re.compile(r'^reviews.php'))[1]))
|
||||
except:
|
||||
# I find it hard to care if the series parsing fails
|
||||
pass
|
||||
@@ -301,7 +291,7 @@ class GrangerEnchantedCom(BaseSiteAdapter):
|
||||
|
||||
soup = self.make_soup(self._fetchUrl(url))
|
||||
|
||||
div = soup.find('div', {'id' : 'story1'})
|
||||
div = soup.find('div', {'id' : 'story'})
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
@@ -106,7 +106,7 @@ class LumosSycophantHexComAdapter(BaseSiteAdapter):
|
||||
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.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -554,7 +554,6 @@ class Chapter(object):
|
||||
|
||||
def _parseRatingFromImage(self, element):
|
||||
"""Given an image element, try to parse story rating from it."""
|
||||
# Although deprecated, `has_key()' is required here.
|
||||
if not element.has_attr('src'):
|
||||
return
|
||||
source = element['src']
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2013 Fanficdownloader team, 2015 FanFicFare team
|
||||
# Copyright 2013 Fanficdownloader team, 2016 FanFicFare team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -91,7 +91,7 @@ class MCStoriesComSiteAdapter(BaseSiteAdapter):
|
||||
data1 = self._fetchUrl(self.url)
|
||||
soup1 = self.make_soup(data1)
|
||||
#strip comments from soup
|
||||
[comment.extract() for comment in soup1.findAll(text=lambda text:isinstance(text, Comment))]
|
||||
[comment.extract() for comment in soup1.find_all(text=lambda text:isinstance(text, Comment))]
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
@@ -112,18 +112,18 @@ class MCStoriesComSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
# Description
|
||||
synopsis = soup1.find('section', class_='synopsis')
|
||||
description = "\n\n".join([p.text for p in synopsis.findAll('p')])
|
||||
description = "\n\n".join([p.text for p in synopsis.find_all('p')])
|
||||
self.story.setMetadata('description', description)
|
||||
|
||||
# Tags
|
||||
codesDiv = soup1.find('div', class_="storyCodes")
|
||||
for a in codesDiv.findAll('a'):
|
||||
for a in codesDiv.find_all('a'):
|
||||
self.story.addToList('eroticatags', a.text)
|
||||
|
||||
# Publish and update dates
|
||||
publishdate = None
|
||||
updatedate = None
|
||||
datelines = soup1.findAll('h3', class_='dateline')
|
||||
datelines = soup1.find_all('h3', class_='dateline')
|
||||
for dateline in datelines:
|
||||
if dateline.text.startswith('Added '):
|
||||
publishdate = makeDate(dateline.text, "Added " + self.dateformat)
|
||||
@@ -139,7 +139,7 @@ class MCStoriesComSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
if chapterTable is not None:
|
||||
# Multi-chapter story
|
||||
chapterRows = chapterTable.findAll('tr')
|
||||
chapterRows = chapterTable.find_all('tr')
|
||||
|
||||
for row in chapterRows:
|
||||
chapterCell = row.td
|
||||
@@ -172,13 +172,13 @@ class MCStoriesComSiteAdapter(BaseSiteAdapter):
|
||||
soup1 = self.make_soup(data1)
|
||||
|
||||
#strip comments from soup
|
||||
[comment.extract() for comment in soup1.findAll(text=lambda text:isinstance(text, Comment))]
|
||||
[comment.extract() for comment in soup1.find_all(text=lambda text:isinstance(text, Comment))]
|
||||
|
||||
# get story text
|
||||
story1 = soup1.find('article', id='mcstories')
|
||||
|
||||
# Remove duplicate name and author headers
|
||||
[h3.extract() for h3 in story1.findAll('h3')]
|
||||
[h3.extract() for h3 in story1.find_all('h3',class_=re.compile(r'(title|chapter|byline)'))]
|
||||
|
||||
storytext = self.utf8FromSoup(url, story1)
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ class MerlinFicDtwinsCoUk(BaseSiteAdapter):
|
||||
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.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -144,7 +144,7 @@ class MidnightwhispersCaAdapter(BaseSiteAdapter): # XXX
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2011 Fanficdownloader team, 2015 FanFicFare team
|
||||
# Copyright 2011 Fanficdownloader team, 2016 FanFicFare team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -16,58 +16,12 @@
|
||||
#
|
||||
|
||||
# Software: eFiction
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
from base_efiction_adapter import BaseEfictionAdapter
|
||||
|
||||
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
# This function is called by the downloader in all adapter_*.py files
|
||||
# in this dir to register the adapter class. So it needs to be
|
||||
# updated to reflect the class below it. That, plus getSiteDomain()
|
||||
# take care of 'Registering'.
|
||||
def getClass():
|
||||
return MuggleNetComAdapter # XXX
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class MuggleNetComAdapter(BaseSiteAdapter): # XXX
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.decode = ["Windows-1252",
|
||||
"utf8"] # 1252 is a superset of iso-8859-1.
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
self.is_adult=False
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','mgln') # XXX
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%m/%d/%y" # XXX
|
||||
class MuggleNetComAdapter(BaseEfictionAdapter):
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain.
|
||||
return 'fanfiction.mugglenet.com'
|
||||
|
||||
@classmethod
|
||||
@@ -75,251 +29,12 @@ class MuggleNetComAdapter(BaseSiteAdapter): # XXX
|
||||
return ['fanfiction.mugglenet.com','fanfic.mugglenet.com']
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234"
|
||||
def getSiteAbbrev(self):
|
||||
return 'mgln'
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://")+r"fanfic(tion)?\.mugglenet\.com"+re.escape("/viewstory.php?sid=")+r"\d+$"
|
||||
@classmethod
|
||||
def getDateFormat(self):
|
||||
return "%m/%d/%y"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
if "class='errortext'>Registered Users Only" in data \
|
||||
or 'There is no such account on our website' in data \
|
||||
or "That password doesn't match the one in our database" in data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def performLogin(self, url):
|
||||
params = {}
|
||||
|
||||
if self.password:
|
||||
params['penname'] = self.username
|
||||
params['password'] = self.password
|
||||
else:
|
||||
params['penname'] = self.getConfig("username")
|
||||
params['password'] = self.getConfig("password")
|
||||
params['cookiecheck'] = '1'
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/user.php?action=login&sid='+self.story.getMetadata('storyId')
|
||||
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.
|
||||
# http://fanfiction.mugglenet.com/viewstory.php?sid=91079&ageconsent=ok&warning=3
|
||||
addurl = "&ageconsent=ok&warning=3" # XXX &warning=5
|
||||
else:
|
||||
addurl=""
|
||||
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
if self.needToLoginCheck(data):
|
||||
# need to log in for this one.
|
||||
self.performLogin(url)
|
||||
data = self._fetchUrl(url)
|
||||
#print("\nurl:%s\ndata:\n%s\n"%(url,data))
|
||||
|
||||
# The actual text that is used to announce you need to be an
|
||||
# adult varies from site to site. Again, print data before
|
||||
# the title search to troubleshoot.
|
||||
|
||||
# Since the warning text can change by warning level, let's
|
||||
# look for the warning pass url. nfacommunity uses
|
||||
# &warning= -- actually, so do other sites. Must be an
|
||||
# eFiction book.
|
||||
|
||||
# viewstory.php?sid=1882&warning=4
|
||||
# viewstory.php?sid=1654&ageconsent=ok&warning=5
|
||||
#print data
|
||||
#m = re.search(r"'viewstory.php\?sid=1882(&warning=4)'",data)
|
||||
m = re.search(r"'viewstory.php\?sid=%s((?:&ageconsent=ok)?&warning=\d+)'"%self.story.getMetadata('storyId'),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 = self.make_soup(data)
|
||||
# print data
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
## Title
|
||||
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
|
||||
self.story.setMetadata('title',stripHTML(a))
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
|
||||
self.story.setMetadata('authorId',a['href'].split('=')[1])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href'])
|
||||
self.story.setMetadata('author',a.string)
|
||||
|
||||
# Find the chapters:
|
||||
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")):
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href']+addurl))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
|
||||
# Not good enough-- content can contain a ('), which ends the content prematurely.
|
||||
# metadesc = soup.find('meta',{'name':'description'})
|
||||
# print("removeAllEntities(metadesc['content']):\n%s\n"%removeAllEntities(metadesc['content']))
|
||||
start='<span class="label">Summary: </span>'
|
||||
end='<span class="label">Rated:</span>'
|
||||
summarydata = data[data.index(start)+len(start):data.index(end)]
|
||||
#print("summarydata:\n%s\n"%summarydata)
|
||||
self.setDescription(url,self.make_soup(summarydata))
|
||||
|
||||
# eFiction sites don't help us out a lot with their meta data
|
||||
# formating, so it's a little ugly.
|
||||
|
||||
# utility method
|
||||
def defaultGetattr(d,k):
|
||||
try:
|
||||
return d[k]
|
||||
except:
|
||||
return ""
|
||||
|
||||
# <span class="label">Rated:</span> NC-17<br /> etc
|
||||
labels = soup.findAll('span',{'class':'label'})
|
||||
for labelspan in labels:
|
||||
value = labelspan.nextSibling
|
||||
label = labelspan.string
|
||||
|
||||
if 'Rated' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
if 'Categories' in label:
|
||||
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
catstext = [cat.string for cat in cats]
|
||||
for cat in catstext:
|
||||
self.story.addToList('category',cat.string)
|
||||
|
||||
if 'Characters' in label:
|
||||
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
|
||||
charstext = [char.string for char in chars]
|
||||
for char in charstext:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
## Not all sites use Genre, but there's no harm to
|
||||
## leaving it in. Check to make sure the type_id number
|
||||
## is correct, though--it's site specific.
|
||||
if 'Genre' in label:
|
||||
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX
|
||||
genrestext = [genre.string for genre in genres]
|
||||
self.genre = ', '.join(genrestext)
|
||||
for genre in genrestext:
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
## Not all sites use Warnings, but there's no harm to
|
||||
## leaving it in. Check to make sure the type_id number
|
||||
## is correct, though--it's site specific.
|
||||
if 'Warnings' in label:
|
||||
warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX
|
||||
warningstext = [warning.string for warning in warnings]
|
||||
self.warning = ', '.join(warningstext)
|
||||
for warning in warningstext:
|
||||
self.story.addToList('warnings',warning.string)
|
||||
|
||||
if 'Completed' in label:
|
||||
if 'Yes' in value:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
if 'Published' in label:
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
if 'Updated' in label:
|
||||
# there's a stray [ at the end.
|
||||
#value = value[0:-1]
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
try:
|
||||
# Find Series name from series URL.
|
||||
a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+"))
|
||||
series_name = a.string
|
||||
series_url = 'http://'+self.host+'/'+a['href']
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
seriessoup = self.make_soup(self._fetchUrl(series_url))
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+'))
|
||||
i=1
|
||||
for a in storyas:
|
||||
# skip 'report this' and 'TOC' links
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
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 = self.make_soup(self._fetchUrl(url))
|
||||
|
||||
div = soup.find('div', {'id' : 'story'})
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
def getClass():
|
||||
return MuggleNetComAdapter
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2016 FanFicFare 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.
|
||||
#
|
||||
|
||||
# Software: eFiction
|
||||
from base_efiction_adapter import BaseEfictionAdapter
|
||||
|
||||
class NaiceaNilmeNetAdapter(BaseEfictionAdapter):
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return 'www.naiceanilme.net'
|
||||
|
||||
@classmethod
|
||||
def getSiteAbbrev(self):
|
||||
return 'nnnet'
|
||||
|
||||
@classmethod
|
||||
def getDateFormat(self):
|
||||
return "%m/%d/%y"
|
||||
|
||||
def getClass():
|
||||
return NaiceaNilmeNetAdapter
|
||||
@@ -95,7 +95,7 @@ class NationalLibraryNetAdapter(BaseSiteAdapter):
|
||||
raise e
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -94,7 +94,7 @@ class NCISFicComAdapter(BaseSiteAdapter):
|
||||
raise e
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2012 Fanficdownloader team, 2015 FanFicFare team
|
||||
# Copyright 2012 Fanficdownloader team, 2016 FanFicFare team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -15,195 +15,22 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
# Software: eFiction
|
||||
from base_efiction_adapter import BaseEfictionAdapter
|
||||
|
||||
class NCISFictionNetAdapter(BaseEfictionAdapter):
|
||||
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return 'ncisfiction.net'
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
@classmethod
|
||||
def getSiteAbbrev(self):
|
||||
return 'ncisfn'
|
||||
|
||||
@classmethod
|
||||
def getDateFormat(self):
|
||||
return "%m/%d/%Y"
|
||||
|
||||
def getClass():
|
||||
return NCISFictionNetAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class NCISFictionNetAdapter(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()\
|
||||
+"/chapters.php?stid="+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','ncisfn')
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%d/%m/%Y"
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'www.ncisfiction.net'
|
||||
|
||||
## Changed from www.ncisfiction.com to www.ncisfiction.net Oct
|
||||
## 2012 due to the ncisfiction.com domain expiring. Still accept
|
||||
## .com domains for existing updates, etc.
|
||||
|
||||
@classmethod
|
||||
def getAcceptDomains(cls):
|
||||
return ['www.ncisfiction.net','www.ncisfiction.com']
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/story.php?stid=01234 http://"+cls.getSiteDomain()+"/chapters.php?stid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return r'http://www\.ncisfiction\.(net|com)/(chapters|story)?.php\?stid=\d+'
|
||||
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
|
||||
if "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 = self.make_soup(data)
|
||||
# print data
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
## Title and author
|
||||
a = soup.find('div', {'class' : 'main_title'})
|
||||
|
||||
aut = a.find('a')
|
||||
self.story.setMetadata('authorId',aut['href'].split('=')[1])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/'+aut['href'])
|
||||
self.story.setMetadata('author',aut.string)
|
||||
|
||||
aut.extract()
|
||||
self.story.setMetadata('title',stripHTML(a)[:len(stripHTML(a))-2])
|
||||
|
||||
# Find the chapters:
|
||||
i=0
|
||||
chapters=soup.findAll('table', {'class' : 'story_table'})
|
||||
for chapter in chapters:
|
||||
ch=chapter.find('a')
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
self.chapterUrls.append((stripHTML(ch),'http://'+self.host+'/'+ch['href']))
|
||||
if i == 0:
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(chapter.find('td')).split('Added: ')[1], self.dateformat))
|
||||
if i == len(chapters)-1:
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(chapter.find('td')).split('Added: ')[1], self.dateformat))
|
||||
i=i+1
|
||||
|
||||
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.
|
||||
|
||||
info = soup.find('table', {'class' : 'story_info'})
|
||||
|
||||
# no convenient way to calculate word count as it is logged differently for stories with and without series
|
||||
|
||||
labels = info.findAll('tr')
|
||||
for tr in labels:
|
||||
value = tr.find('td')
|
||||
label = tr.find('th').string
|
||||
|
||||
if 'Summary' in label:
|
||||
self.setDescription(url,value)
|
||||
|
||||
if 'Rating' in label:
|
||||
self.story.setMetadata('rating', value.string)
|
||||
|
||||
if 'Category' in label:
|
||||
cats = value.findAll('a')
|
||||
for cat in cats:
|
||||
self.story.addToList('category',cat.string)
|
||||
|
||||
if 'Characters' in label:
|
||||
chars = value.findAll('a')
|
||||
for char in chars:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
if 'Pairing' in label:
|
||||
ships = value.findAll('a')
|
||||
for ship in ships:
|
||||
self.story.addToList('ships',ship.string)
|
||||
|
||||
if 'Genre' in label:
|
||||
genres = value.findAll('a')
|
||||
for genre in genres:
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
if 'Warnings' in label:
|
||||
warnings = value.findAll('a')
|
||||
for warning in warnings:
|
||||
self.story.addToList('warnings',warning.string)
|
||||
|
||||
if 'Status' in label:
|
||||
if 'not completed' in value.text:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
else:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
|
||||
try:
|
||||
# Find Series name from series URL.
|
||||
a = soup.find('div',{'class' : 'sub_header'})
|
||||
series_name = a.find('a').string
|
||||
i = a.text.split('#')[1]
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl','http://'+self.host+'/'+a.find('a')['href'])
|
||||
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 = self.make_soup(self._fetchUrl(url))
|
||||
|
||||
div = soup.find('div', {'class' : 'story_text'})
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
|
||||
@@ -148,7 +148,7 @@ class NfaCommunityComAdapter(BaseSiteAdapter): # XXX
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2013 Fanficdownloader team, 2015 FanFicFare 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.
|
||||
#
|
||||
|
||||
# Software: eFiction
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
def getClass():
|
||||
return NickAndGregNetAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class NickAndGregNetAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.decode = ["Windows-1252",
|
||||
"utf8"] # 1252 is a superset of iso-8859-1.
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
self.is_adult=False
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
# XXX Most sites don't have the /fanfic part. Replace all to remove it usually.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/desert_archive/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','nag')
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%Y/%m/%d"
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'www.nickngreg.nl'
|
||||
|
||||
@classmethod
|
||||
def getAcceptDomains(cls):
|
||||
return ['www.nickngreg.nl','www.nickandgreg.net']
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/desert_archive/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return "http://("+self.getSiteDomain()+"|www.nickandgreg.net)"+re.escape("/desert_archive/viewstory.php?sid=")+r"\d+$"
|
||||
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&i=1'
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = self.make_soup(data)
|
||||
# print data
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
## Title
|
||||
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
|
||||
self.story.setMetadata('title',stripHTML(a))
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
|
||||
self.story.setMetadata('authorId',a['href'].split('=')[1])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/desert_archive/'+a['href'])
|
||||
self.story.setMetadata('author',a.string)
|
||||
|
||||
# Find the chapters:
|
||||
chapters = soup.find('select')
|
||||
for chapter in chapters.findAll('option'):
|
||||
if chapter.text != 'Story Index' and chapter.text != 'Chapters':
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/desert_archive/'+chapter['value']))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
asoup = self.make_soup(self._fetchUrl(self.story.getMetadata('authorUrl')))
|
||||
|
||||
for div in asoup.findAll('td', {'class' : 'tblborder6'}):
|
||||
a = div.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
|
||||
if a != None:
|
||||
break
|
||||
|
||||
self.setDescription(url,div.find('br').nextSibling)
|
||||
|
||||
a=div.text.split('Rating:')
|
||||
if len(a) == 2: self.story.setMetadata('rating', a[1].split(' -')[0])
|
||||
|
||||
a=div.text.split('Characters:')
|
||||
if len(a) == 2:
|
||||
for char in a[1].split(' -')[0].split(', '):
|
||||
self.story.addToList('characters',char)
|
||||
|
||||
a=div.text.split('Genres:')
|
||||
if len(a) == 2:
|
||||
for genre in a[1].split(' -')[0].split(', '):
|
||||
self.story.addToList('genre',genre)
|
||||
|
||||
a=div.text.split('Warnings:')
|
||||
if len(a) == 2:
|
||||
for warn in a[1].split(' -')[0].split(', '):
|
||||
if 'none' not in warn:
|
||||
self.story.addToList('warnings',warn)
|
||||
|
||||
a=div.text.split('Completed:')
|
||||
if len(a) ==2:
|
||||
if 'Yes' in a[1]:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
a=div.text.split('Published:')
|
||||
if len(a) == 2: self.story.setMetadata('datePublished', makeDate(stripHTML(a[1].split(' -')[0]), self.dateformat))
|
||||
|
||||
a=div.text.split('Updated:')
|
||||
if len(a) == 2: self.story.setMetadata('dateUpdated', makeDate(stripHTML(a[1].split(' -')[0]), self.dateformat))
|
||||
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
soup = self.make_soup(self._fetchUrl(url))
|
||||
|
||||
# wrap a div around it.
|
||||
divsoup = self.make_soup('<div class="story"></div>')
|
||||
div = divsoup.find('div')
|
||||
div.append(soup.find('table', {'class' : 'tblborder6'}))
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
@@ -131,7 +131,7 @@ class OcclumencySycophantHexComAdapter(BaseSiteAdapter):
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -150,7 +150,7 @@ class OneDirectionFanfictionComAdapter(BaseSiteAdapter):
|
||||
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.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -171,7 +171,7 @@ class PommeDeSangComAdapter(BaseSiteAdapter):
|
||||
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.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -127,7 +127,7 @@ class PonyFictionArchiveNetAdapter(BaseSiteAdapter):
|
||||
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.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2011 Fanficdownloader team, 2015 FanFicFare team
|
||||
# Copyright 2011 Fanficdownloader team, 2016 FanFicFare team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -109,7 +109,7 @@ class PortkeyOrgAdapter(BaseSiteAdapter): # XXX
|
||||
self.get_cookiejar().set_cookie(cookie)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
data = self._fetchUrl(url,usecache=False)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
@@ -270,7 +270,10 @@ class PortkeyOrgAdapter(BaseSiteAdapter): # XXX
|
||||
tag = soup.find('td', {'class' : 'story'})
|
||||
if tag == None and "<center><b>Chapter does not exist!</b></center>" in data:
|
||||
logger.error("Chapter is missing at: %s"%url)
|
||||
return self.utf8FromSoup(url,self.make_soup("<div><p><center><b>Chapter does not exist!</b></center></p><p>Chapter is missing at: <a href='%s'>%s</a></p></div>"%(url,url)))
|
||||
return self.utf8FromSoup(url,self.make_soup("<div><p><center><b>Site says: Chapter does not exist!</b></center></p><p>Chapter is missing at: <a href='%s'>%s</a></p></div>"%(url,url)))
|
||||
elif tag == None and "<center><b>This chapter has corrupted or a blank chapter was uploaded. Please contact the author and request that they re-upload the chapter</b></center>" in data:
|
||||
logger.error("Chapter is missing at: %s"%url)
|
||||
return self.utf8FromSoup(url,self.make_soup("<div><p><center><b>Site says: This chapter has corrupted or a blank chapter was uploaded.</b></center></p><p>Chapter is missing at: <a href='%s'>%s</a></p></div>"%(url,url)))
|
||||
tag.name='div' # force to be a div to avoid problems with nook.
|
||||
|
||||
centers = tag.findAll('center')
|
||||
|
||||
@@ -77,7 +77,7 @@ class PotionsAndSnitchesOrgSiteAdapter(BaseSiteAdapter):
|
||||
raise e
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -170,7 +170,7 @@ class PotterHeadsAnonymousComAdapter(BaseSiteAdapter):
|
||||
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.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -125,7 +125,7 @@ class PretenderCenterComAdapter(BaseSiteAdapter):
|
||||
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.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -121,7 +121,7 @@ class PsychFicComAdapter(BaseSiteAdapter):
|
||||
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.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -122,7 +122,7 @@ class QafFicComAdapter(BaseSiteAdapter):
|
||||
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.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import re
|
||||
import urlparse
|
||||
import urllib2
|
||||
@@ -58,23 +60,25 @@ class QuotevComAdapter(BaseSiteAdapter):
|
||||
if not element:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
|
||||
self.story.setMetadata('title', element.find('h1').get_text())
|
||||
|
||||
# quotev html is all about formatting without any content tagging
|
||||
authdiv = soup.find('div', {'style':"text-align:left;"})
|
||||
title = element.find('h1')
|
||||
self.story.setMetadata('title', title.get_text())
|
||||
|
||||
authdiv = soup.find('div', {'class':"quizAuthorList"})
|
||||
if authdiv:
|
||||
#print("div:%s"%authdiv.find_all('a'))
|
||||
print("div:%s"%authdiv)
|
||||
for a in authdiv.find_all('a'):
|
||||
self.story.addToList('author', a.get_text())
|
||||
self.story.addToList('authorId', a['href'].split('/')[-1])
|
||||
self.story.addToList('authorUrl', urlparse.urljoin(self.url, a['href']))
|
||||
else:
|
||||
self.story.setMetadata('author','Anonymous')
|
||||
self.story.setMetadata('authorUrl','http://www.quotev.com')
|
||||
self.story.setMetadata('authorId','0')
|
||||
if not self.story.getList('author'):
|
||||
self.story.addToList('author','Anonymous')
|
||||
self.story.addToList('authorUrl','http://www.quotev.com')
|
||||
self.story.addToList('authorId','0')
|
||||
|
||||
self.setDescription(self.url, soup.find('div', id='qdesct'))
|
||||
self.setCoverImage(self.url, urlparse.urljoin(self.url, soup.find('img', {'class': 'logo'})['src']))
|
||||
imgmeta = soup.find('meta',{'property':"og:image" })
|
||||
if imgmeta:
|
||||
self.coverurl = self.setCoverImage(self.url, urlparse.urljoin(self.url, imgmeta['content']))[1]
|
||||
|
||||
for a in soup.find_all('a', {'href': re.compile(SITE_DOMAIN+'/stories/c/')}):
|
||||
self.story.addToList('category', a.get_text())
|
||||
@@ -87,9 +91,9 @@ class QuotevComAdapter(BaseSiteAdapter):
|
||||
if len(elements) > 1:
|
||||
self.story.setMetadata('dateUpdated', datetime.datetime.fromtimestamp(float(elements[1]['ts'])))
|
||||
|
||||
metadiv = elements[0].parent
|
||||
|
||||
if 'completed' in stripHTML(metadiv):
|
||||
metadiv = elements[0].parent.parent
|
||||
# print stripHTML(metadiv)
|
||||
if u'· completed ·' in stripHTML(metadiv):
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
@@ -100,16 +104,13 @@ class QuotevComAdapter(BaseSiteAdapter):
|
||||
parts = datum.split()
|
||||
if len(parts) < 2 or parts[1] not in self.getConfig('extra_valid_entries'):
|
||||
continue
|
||||
# Not a valid metadatum
|
||||
# if not len(parts) == 2:
|
||||
# continue
|
||||
|
||||
key, value = parts[1], parts[0]
|
||||
self.story.setMetadata(key, value.replace(',', '').replace('.', ''))
|
||||
|
||||
favspans = soup.find('a',{'id':'fav_btn'}).find_all('span')
|
||||
if len(favspans) > 1:
|
||||
self.story.setMetadata('favorites', stripHTML(favspans[1]).replace(',', ''))
|
||||
self.story.setMetadata('favorites', stripHTML(favspans[-1]).replace(',', ''))
|
||||
|
||||
commentspans = soup.find('a',{'id':'comment_btn'}).find_all('span')
|
||||
#print("commentspans:%s"%commentspans)
|
||||
@@ -117,7 +118,8 @@ class QuotevComAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('comments', stripHTML(commentspans[0]).replace(',', ''))
|
||||
|
||||
for a in soup.find('div', id='rselect')('a'):
|
||||
self.chapterUrls.append((a.get_text(), urlparse.urljoin(self.url, a['href'])))
|
||||
if 'javascript' not in a['href']:
|
||||
self.chapterUrls.append((a.get_text(), urlparse.urljoin(self.url, a['href'])))
|
||||
|
||||
self.story.setMetadata('numChapters', len(self.chapterUrls))
|
||||
|
||||
@@ -125,8 +127,15 @@ class QuotevComAdapter(BaseSiteAdapter):
|
||||
data = self._fetchUrl(url)
|
||||
soup = self.make_soup(data)
|
||||
|
||||
element = soup.find('div', id='rescontent')
|
||||
for a in element('a'):
|
||||
rescontent = soup.find('div', id='rescontent')
|
||||
|
||||
# attempt to find and include chapter specific images.
|
||||
img = soup.find('div',{'id':'quizHeader'}).find('img')
|
||||
#print("img['src'](%s) != self.coverurl(%s)"%(img['src'],self.coverurl))
|
||||
if img['src'] != self.coverurl:
|
||||
rescontent.insert(0,img)
|
||||
|
||||
for a in rescontent('a'):
|
||||
a.unwrap()
|
||||
|
||||
return self.utf8FromSoup(url, element)
|
||||
return self.utf8FromSoup(url, rescontent)
|
||||
|
||||
@@ -198,7 +198,7 @@ class SamAndJackNetAdapter(BaseSiteAdapter): # XXX
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -94,7 +94,7 @@ class SamDeanArchiveNuAdapter(BaseSiteAdapter):
|
||||
raise e
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -130,7 +130,7 @@ class ScarvesAndCoffeeNetAdapter(BaseSiteAdapter):
|
||||
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.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -169,7 +169,7 @@ class SheppardWeirComAdapter(BaseSiteAdapter): # XXX
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -121,7 +121,7 @@ class SinfulDesireOrgAdapter(BaseSiteAdapter):
|
||||
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.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -103,6 +103,8 @@ class SiyeCoUkAdapter(BaseSiteAdapter): # XXX
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
|
||||
if a is None:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
self.story.setMetadata('authorId',a['href'].split('=')[1])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/siye/'+a['href'])
|
||||
self.story.setMetadata('author',a.string)
|
||||
|
||||
@@ -36,6 +36,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
logger.debug("StoriesOnlineNetAdapter.__init__ - url='%s'" % url)
|
||||
|
||||
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
@@ -50,12 +51,16 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
self._setURL('http://' + self.getSiteDomain() + '/s/'+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','strol')
|
||||
self.story.setMetadata('siteabbrev',self.getSiteAbbrev())
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%Y-%m-%d"
|
||||
|
||||
@classmethod
|
||||
def getSiteAbbrev(self):
|
||||
return 'strol'
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
@@ -66,7 +71,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
return "http://"+cls.getSiteDomain()+"/s/1234 http://"+cls.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|library)/(storyInfo.php\?id=)?(?P<id>\d+)((:\d+)?(;\d+)?$|(:i)?$)?"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
@@ -139,9 +144,11 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
data = self._fetchUrl(url+":i",usecache=False)
|
||||
|
||||
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.")
|
||||
raise exceptions.AccessDenied(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
elif "Error! The story you're trying to access is being filtered by your choice of contents filtering." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Error! The story you're trying to access is being filtered by your choice of contents filtering.")
|
||||
elif "Error! Daily Limit Reached" in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Error! Daily Limit Reached")
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = self.make_soup(data)
|
||||
@@ -178,7 +185,8 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
page=0
|
||||
i=0
|
||||
while i == 0:
|
||||
asoup = self.make_soup(self._fetchUrl(self.story.getList('authorUrl')[0]+"/"+unicode(page)))
|
||||
data = self._fetchUrl(self.story.getList('authorUrl')[0]+"/"+unicode(page))
|
||||
asoup = self.make_soup(data)
|
||||
|
||||
a = asoup.findAll('td', {'class' : 'lc2'})
|
||||
for lc2 in a:
|
||||
@@ -203,6 +211,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
|
||||
try:
|
||||
a = lc4.find('a', href=re.compile(r"/series/\d+/.*"))
|
||||
# logger.debug("Looking for series - a='{0}'".format(a))
|
||||
if a:
|
||||
# if there's a number after the series name, series_contents is a two element list:
|
||||
# [<a href="...">Title</a>, u' (2)']
|
||||
@@ -211,59 +220,62 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
seriesUrl = 'http://'+self.host+a['href']
|
||||
self.story.setMetadata('seriesUrl',seriesUrl)
|
||||
series_name = stripHTML(a)
|
||||
logger.debug("Series name= %s" % series_name)
|
||||
# logger.debug("Series name= %s" % series_name)
|
||||
series_soup = self.make_soup(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))
|
||||
# logger.debug("Retrieving Series - looking for name")
|
||||
series_name = stripHTML(series_soup.find('span', {'id' : 'ptitle'}))
|
||||
series_name = re.sub(r' . a series by.*$','',series_name)
|
||||
# logger.debug("Series name: '%s'" % series_name)
|
||||
self.setSeries(series_name, i)
|
||||
desc = lc4.contents[2]
|
||||
# Check if series is in a universe
|
||||
universe_url = self.story.getList('authorUrl')[0] + "&type=uni"
|
||||
universes_soup = self.make_soup(self._fetchUrl(universe_url) )
|
||||
logger.debug("Universe url='{0}'".format(universe_url))
|
||||
# logger.debug("Universe url='{0}'".format(universe_url))
|
||||
if universes_soup:
|
||||
universes = universes_soup.findAll('div', {'class' : 'ser-box'})
|
||||
logger.debug("Number of Universes: %d" % len(universes))
|
||||
# logger.debug("Number of Universes: %d" % len(universes))
|
||||
for universe in universes:
|
||||
logger.debug("universe.find('a')={0}".format(universe.find('a')))
|
||||
# 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)
|
||||
# logger.debug("universe_id='%s'" % universe_id)
|
||||
universe_name = stripHTML(universe.find('div', {'class' : 'ser-name'})).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=re.compile('/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)
|
||||
# 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
|
||||
else:
|
||||
logger.debug("No universe page")
|
||||
except:
|
||||
raise
|
||||
pass
|
||||
try:
|
||||
a = lc4.find('a', href=re.compile(r"/universe/\d+/.*"))
|
||||
logger.debug("Looking for universe - a='{0}'".format(a))
|
||||
# logger.debug("Looking for universe - a='{0}'".format(a))
|
||||
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 - universeUrl='{0}".format(universeUrl))
|
||||
# logger.debug("Retrieving Universe - about to get page - universeUrl='{0}".format(universeUrl))
|
||||
universe_soup = self.make_soup(self._fetchUrl(universeUrl))
|
||||
logger.debug("Retrieving Universe - have page")
|
||||
if universe_soup:
|
||||
logger.debug("Retrieving Universe - looking for name")
|
||||
universe_name = universe_soup.find('h1', {'id' : 'ptitle'}).text.partition('—')[0]
|
||||
logger.debug("Universes name: '{0}'".format(universe_name))
|
||||
universe_name = stripHTML(universe_soup.find('h1', {'id' : 'ptitle'}))
|
||||
universe_name = re.sub(r' . A Universe from the Mind.*$','',universe_name)
|
||||
# logger.debug("Universes name: '{0}'".format(universe_name))
|
||||
|
||||
self.story.setMetadata('universeUrl',universeUrl)
|
||||
logger.debug("Setting universe name: '{0}'".format(universe_name))
|
||||
# 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)
|
||||
@@ -271,6 +283,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
else:
|
||||
logger.debug("Do not have a universe")
|
||||
except:
|
||||
raise
|
||||
pass
|
||||
|
||||
self.setDescription('http://'+self.host+'/s/'+self.story.getMetadata('storyId'),desc)
|
||||
@@ -290,7 +303,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
|
||||
if 'Tags' in label or 'Codes' in label:
|
||||
for code in re.split(r'\s*,\s*', value.strip()):
|
||||
self.story.addToList('sitetags',code)
|
||||
self.story.addToList('sitetags',code)
|
||||
|
||||
if 'Posted' in label:
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
|
||||
@@ -307,18 +320,21 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
# http://storiesonline.net/s/11999
|
||||
# http://storiesonline.net/s/10823
|
||||
if get_cover:
|
||||
logger.debug("Looking for the cover image...")
|
||||
# logger.debug("Looking for the cover image...")
|
||||
cover_url = ""
|
||||
img = soup.find('img')
|
||||
if img:
|
||||
cover_url=img['src']
|
||||
logger.debug("cover_url: %s"%cover_url)
|
||||
# logger.debug("cover_url: %s"%cover_url)
|
||||
if cover_url:
|
||||
self.setCoverImage(url,cover_url)
|
||||
|
||||
status = lc4.find('span', {'class' : 'ab'})
|
||||
if status != None:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
if 'Incomplete and Inactive' in status.text:
|
||||
self.story.setMetadata('status', 'Incomplete')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
if "Last Activity" in status.text:
|
||||
# date is passed as a timestamp and converted in JS.
|
||||
value = status.findNext('noscript').text
|
||||
@@ -347,7 +363,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
|
||||
urls=pager.findAll('a')
|
||||
urls=urls[:len(urls)-1]
|
||||
logger.debug("pager urls:%s"%urls)
|
||||
# logger.debug("pager urls:%s"%urls)
|
||||
pager.extract()
|
||||
chaptertag.contents = chaptertag.contents[2:]
|
||||
|
||||
@@ -356,7 +372,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
||||
|
||||
pagetag = soup.find('div', {'id' : 'story'})
|
||||
if not pagetag:
|
||||
logger.debug("div id=story not found, try article")
|
||||
# logger.debug("div id=story not found, try article")
|
||||
pagetag = soup.find('article', {'id' : 'story'})
|
||||
|
||||
self.cleanPage(pagetag)
|
||||
|
||||
@@ -134,7 +134,7 @@ class TenhawkPresentsComSiteAdapter(BaseSiteAdapter):
|
||||
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.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -34,6 +34,8 @@ class TestSiteAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
self.username=''
|
||||
self.is_adult=False
|
||||
# happens inside BaseSiteAdapter.__init__
|
||||
# self._setURL(url)
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
@@ -117,7 +119,6 @@ class TestSiteAdapter(BaseSiteAdapter):
|
||||
else:
|
||||
self.story.setMetadata(u'title',"Test Story Title "+idstr)
|
||||
self.story.setMetadata('author','Test Author aa')
|
||||
self.story.setMetadata('storyUrl',self.url)
|
||||
self.setDescription(self.url,u'Description '+self.crazystring+u''' Done
|
||||
<p>
|
||||
Some more longer description. "I suck at summaries!" "Better than it sounds!" "My first fic"
|
||||
|
||||
@@ -90,7 +90,7 @@ class TheAlphaGateComAdapter(BaseSiteAdapter):
|
||||
raise e
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -193,14 +193,10 @@ class TheHexFilesNetAdapter(BaseSiteAdapter):
|
||||
if None == soup:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
# Ugh. chapter html doesn't haven't anything useful around it to demarcate.
|
||||
for a in soup.findAll('table'):
|
||||
content = soup.find('table',{'class':'table'}).find('td') # td inside <table class='table'>
|
||||
content.name='div'
|
||||
|
||||
for a in content.findAll('table'):
|
||||
a.extract()
|
||||
|
||||
for a in soup.findAll('head'):
|
||||
a.extract()
|
||||
|
||||
html = soup.find('html')
|
||||
html.name='div'
|
||||
|
||||
return self.utf8FromSoup(url,soup)
|
||||
return self.utf8FromSoup(url,content)
|
||||
|
||||
@@ -168,7 +168,7 @@ class TheMasqueNetAdapter(BaseSiteAdapter):
|
||||
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.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -129,7 +129,7 @@ class ThePetulantPoetessComAdapter(BaseSiteAdapter):
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2013 Fanficdownloader team, 2015 FanFicFare 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.
|
||||
#
|
||||
|
||||
# Software: eFiction
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
def getClass():
|
||||
return TokraFandomnetComAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class TokraFandomnetComAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.decode = ["Windows-1252",
|
||||
"utf8"] # 1252 is a superset of iso-8859-1.
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
self.is_adult=False
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','tokra')
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%m/%d/%Y"
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it. But it
|
||||
# doesn't matter too much anymore.
|
||||
return 'tokra.fandomnet.com'
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$"
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
# Weirdly, different sites use different warning numbers.
|
||||
# If the title search below fails, there's a good chance
|
||||
# you need a different number. print data at that point
|
||||
# and see what the 'click here to continue' url says.
|
||||
addurl = "&ageconsent=ok&warning=3"
|
||||
else:
|
||||
addurl=""
|
||||
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data)
|
||||
if m != None:
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
# We tried the default and still got a warning, so
|
||||
# let's pull the warning number from the 'continue'
|
||||
# link and reload data.
|
||||
addurl = m.group(1)
|
||||
# correct stupid & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
url = self.url+'&index=1'+addurl
|
||||
logger.debug("URL 2nd try: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
else:
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = self.make_soup(data)
|
||||
#print data
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
## Title
|
||||
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
|
||||
self.story.setMetadata('title',stripHTML(a))
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
|
||||
self.story.setMetadata('authorId',a['href'].split('=')[1])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href'])
|
||||
self.story.setMetadata('author',a.string)
|
||||
|
||||
# Rating
|
||||
rate = stripHTML(soup.find('div',{'id':'pagetitle'}))
|
||||
rate = rate[rate.rindex('[')+1:rate.rindex(']')]
|
||||
self.story.setMetadata('rating', rate)
|
||||
|
||||
# Find the chapters:
|
||||
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")):
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href']+addurl))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
# eFiction sites don't help us out a lot with their meta data
|
||||
# formating, so it's a little ugly.
|
||||
|
||||
metadiv = soup.find('div',{'class':'content'})
|
||||
smalldiv = metadiv.find('div',{'class':'small'})
|
||||
|
||||
# tokra categories -> genre
|
||||
# categories will be filled from ini.
|
||||
genres = smalldiv.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
for genre in genres:
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
chars = smalldiv.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
|
||||
for char in chars:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
metatext = stripHTML(smalldiv)
|
||||
|
||||
if 'Completed: Yes' in metatext:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
wordstart=metatext.rindex('Word count:')+12
|
||||
words = metatext[wordstart:metatext.index(' ',wordstart)]
|
||||
self.story.setMetadata('numWords', words)
|
||||
|
||||
datesdiv = soup.find('div',{'class':'bottom'})
|
||||
dates = stripHTML(datesdiv).split()
|
||||
# Published: 04/26/2011 Updated: 03/06/2013
|
||||
self.story.setMetadata('datePublished', makeDate(dates[1], self.dateformat))
|
||||
self.story.setMetadata('dateUpdated', makeDate(dates[3], self.dateformat))
|
||||
|
||||
try:
|
||||
# Find Series name from series URL.
|
||||
a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+"))
|
||||
series_name = a.string
|
||||
series_url = 'http://'+self.host+'/'+a['href']
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
seriessoup = self.make_soup(self._fetchUrl(series_url))
|
||||
# can't use ^viewstory...$ in case of higher rated stories with javascript href.
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+'))
|
||||
i=1
|
||||
for a in storyas:
|
||||
# skip 'report this' and 'TOC' links
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
except:
|
||||
# I find it hard to care if the series parsing fails
|
||||
pass
|
||||
|
||||
# remove 'small' leaving only summary.
|
||||
smalldiv.extract()
|
||||
self.setDescription(url,metadiv)
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
soup = self.make_soup(self._fetchUrl(url))
|
||||
|
||||
div = soup.find('div', {'class' : 'content'})
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
# remove some decorations while keeping notes.
|
||||
remove = div.find('div', {'id' : 'pagetitle'})
|
||||
remove.extract()
|
||||
|
||||
for remove in div.findAll('div', {'class' : 'right'}):
|
||||
remove.extract()
|
||||
|
||||
for remove in div.findAll('div', {'class' : 'left'}):
|
||||
remove.extract()
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
@@ -165,7 +165,7 @@ class TrekiverseOrgAdapter(BaseSiteAdapter):
|
||||
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.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -131,7 +131,7 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
|
||||
#print("data:%s"%data)
|
||||
soup = self.make_soup(data)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
if e.code in (404,410):
|
||||
raise exceptions.StoryDoesNotExist(url)
|
||||
else:
|
||||
raise e
|
||||
@@ -172,17 +172,22 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
|
||||
descurl=authorurl
|
||||
authorsoup = self.make_soup(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')+'/')) ):
|
||||
# find('a', href=re.compile(r"^/Story-"+self.story.getMetadata('storyId')+'/')) ):
|
||||
#logger.info("authsoup:%s"%authorsoup)
|
||||
while( not authorsoup.find('div', {'id':'st'+self.story.getMetadata('storyId'), 'class':re.compile(r"storylistitem")}) ):
|
||||
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)
|
||||
logger.info("Story Not Found on Author List--Assuming needs Adult.")
|
||||
raise exceptions.FailedToDownload("Story Not Found on Author List--Assume needs Adult?")
|
||||
# raise exceptions.AdultCheckRequired(self.url)
|
||||
nextpage = 'http://'+self.host+nextarrow['href']
|
||||
logger.debug("**AUTHOR** nextpage URL: "+nextpage)
|
||||
authordata = self._fetchUrl(nextpage)
|
||||
#logger.info("authsoup:%s"%authorsoup)
|
||||
descurl=nextpage
|
||||
authorsoup = self.make_soup(authordata)
|
||||
except urllib2.HTTPError, e:
|
||||
@@ -252,13 +257,15 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
|
||||
for cat in verticaltable.findAll('a', href=re.compile(r"^/Category-")):
|
||||
# assumes only one -Centered and one Pairing: cat can ever
|
||||
# be applied to one story.
|
||||
if self.getConfig('centeredcat_to_characters') and cat.string.endswith('-Centered'):
|
||||
# Seen at least once: incorrect (empty) cat link, thus "and cat.string"
|
||||
if self.getConfig('centeredcat_to_characters') and cat.string and cat.string.endswith('-Centered'):
|
||||
char = cat.string[:-len('-Centered')]
|
||||
self.story.addToList('characters',char)
|
||||
elif self.getConfig('pairingcat_to_characters_ships') and cat.string.startswith('Pairing: '):
|
||||
elif self.getConfig('pairingcat_to_characters_ships') and cat.string and cat.string.startswith('Pairing: '):
|
||||
pair = cat.string[len('Pairing: '):]
|
||||
self.story.addToList('characters',pair)
|
||||
self.story.addToList('ships',char+'/'+pair)
|
||||
if char:
|
||||
self.story.addToList('ships',char+'/'+pair)
|
||||
elif cat.string not in ['General', 'Non-BtVS/AtS Stories', 'Non-BTVS/AtS Stories', 'BtVS/AtS Non-Crossover', 'Non-BtVS Crossovers']:
|
||||
# assumed only ship category after Romance cat.
|
||||
if self.getConfig('romancecat_to_characters_ships') and romance:
|
||||
|
||||
@@ -127,7 +127,7 @@ class TheWritersCoffeeShopComSiteAdapter(BaseSiteAdapter):
|
||||
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.")
|
||||
raise exceptions.AccessDenied(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
|
||||
# problems with some stories, but only in calibre. I suspect
|
||||
# issues with different SGML parsers in python. This is a
|
||||
@@ -256,9 +256,9 @@ class TheWritersCoffeeShopComSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
found=False
|
||||
for div in soup.findAll('div'):
|
||||
if div.has_key('class') and div['class'] == 'notes':
|
||||
if div.has_attr('class') and div['class'] == 'notes':
|
||||
chapter.append(div)
|
||||
if div.has_key('id') and div['id'] == 'story':
|
||||
if div.has_attr('id') and div['id'] == 'story':
|
||||
chapter.append(div)
|
||||
found=True
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ class TwilightArchivesComAdapter(BaseSiteAdapter):
|
||||
raise e
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
@@ -118,7 +118,7 @@ class TwilightedNetSiteAdapter(BaseSiteAdapter):
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
raise exceptions.AccessDenied(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
|
||||
# problems with some stories, but only in calibre. I suspect
|
||||
# issues with different SGML parsers in python. This is a
|
||||
|
||||
@@ -106,7 +106,7 @@ class WalkingThePlankOrgAdapter(BaseSiteAdapter):
|
||||
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.")
|
||||
raise exceptions.AccessDenied(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 = self.make_soup(data)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user