Compare commits

...
Author SHA1 Message Date
Jim Miller f276ca081f Bump versions. 2013-07-09 19:29:16 -05:00
iatheia dc2116dd3e New adapter for scarhead.net 2013-07-08 22:19:35 -04:00
Jim Miller 737bed8bfa GUI improvements to Basic config and Add/Update dialogs. 2013-07-08 15:58:04 -05:00
Jim Miller 178d83d733 Redo Basic config layout into better groupings. 2013-07-05 16:08:54 -05:00
Jim Miller ccc07676fb Phoenixsong can have individual chapters req login. Add force_login parameter. 2013-07-03 09:51:39 -05:00
Jim Miller a6b9ca52bb Improved error handling when ffnet is having problems. 2013-07-02 23:40:10 -05:00
Jim Miller cbb8eb14db Added tag FanFictionDownLoader-4.4.62 for changeset 1fd9bacc4f62 2013-06-28 11:06:25 -05:00
Jim Miller b472f8d216 Added tag calibre-plugin-1.7.29 for changeset 1fd9bacc4f62 2013-06-28 11:06:08 -05:00
Jim Miller a4d97500b5 Bump versions. 2013-06-28 11:05:30 -05:00
Jim Miller 22d9a674a3 Allow domain fanfic.mugglenet.com for mugglenet.com. 2013-06-28 10:09:26 -05:00
iatheia 1e908c3440 New adapter for fanfic.potterheadsanonymous.com and www.simplyundeniable.com (latter needs user input for restricted stories) 2013-06-28 00:06:29 -04:00
Jim Miller ec7c02f2ec Added tag calibre-plugin-1.7.28 for changeset 1a40ab7c9ba4 2013-06-22 19:20:18 -05:00
Jim Miller 2049910bb0 Added tag FanFictionDownLoader-4.4.61 for changeset 1a40ab7c9ba4 2013-06-22 19:20:03 -05:00
Jim Miller 1e61bebf21 Fix for siye.co.uk stories with HTML in the title, bump versions. 2013-06-22 19:19:42 -05:00
Jim Miller 7f47100db4 Couple minor fixes. 2013-06-18 13:53:21 -05:00
Jim Miller 83b48d15d8 Added tag FanFictionDownLoader-4.4.60 for changeset de6789e70c76 2013-06-10 21:12:06 -05:00
Jim Miller 5aec8188a2 Added tag calibre-plugin-1.7.27 for changeset de6789e70c76 2013-06-10 21:11:53 -05:00
Jim Miller 5a34f4c86a Bump versions, update index.html. 2013-06-10 21:11:39 -05:00
Jim Miller 8e5ba7b634 Restrict url list search on scarvesandcoffee.net. 2013-06-10 19:46:25 -05:00
Jim Miller 7a47d1fad2 Fix for scarvesandcoffee.net taking author from featured stories. 2013-06-10 19:45:24 -05:00
Jim Miller 7f4749a022 Fix for nha.magical-worlds.us: reviews link disappeared, fix finding story url for
meta section, fix highbyte chars in description.
2013-06-10 19:44:58 -05:00
Jim Miller b44f059e57 Added tag calibre-plugin-1.7.26 for changeset 127933a4d5f3 2013-06-09 11:54:53 -05:00
Jim Miller 6b9058a9eb Added tag FanFictionDownLoader-4.4.59 for changeset 127933a4d5f3 2013-06-09 11:54:30 -05:00
Jim Miller 5acf9a8d0b Add 'Download as New Book?' dialog after 'Change Story URL?', fix author URLs when new author. 2013-06-08 23:11:55 -05:00
Jim Miller 454c7ffb2f Fall back category parse for ffnet when broken crossover cat link. 2013-06-08 23:10:51 -05:00
Jim Miller f6dcb447b0 Bump versions, update index.html. 2013-05-30 21:37:48 -05:00
Jim Miller 5ce064bf92 Fix so non-anthology numeric custom columns populate correctly. 2013-05-30 19:37:50 -05:00
Jim Miller c9a1537190 Fix for numeric site specific values into float/int custom columns. 2013-05-30 12:52:57 -05:00
Jim Miller 36e192e82c Add Read & Review counts to dramione.org and grangerenchanted.com 2013-05-30 12:52:27 -05:00
iatheia 42058d02b3 Adapter for nickandgreg.net 2013-05-30 01:18:53 -04:00
Jim Miller e0832b9deb Update index.html 2013-05-26 16:15:19 -05:00
Jim Miller 4ccb94fca0 Added tag FanFictionDownLoader-4.4.58 for changeset b53a5015e7b3 2013-05-26 16:07:46 -05:00
Jim Miller 0e5b64bee0 Added tag calibre-plugin-1.7.25 for changeset b53a5015e7b3 2013-05-26 16:07:30 -05:00
Jim Miller 53a325ffa3 Bump versions. 2013-05-26 16:07:19 -05:00
Jim Miller 29f900199e Configurable testing stories in test1.com. 2013-05-22 21:18:36 -05:00
Jim Miller f56fb0efed Add "keep_style_attr" option. 2013-05-22 21:14:56 -05:00
Jim Miller a703011aef Correct a misspelling. 2013-05-02 13:16:53 -05:00
Jim Miller a635272bcf Added tag FanFictionDownLoader-4.4.57 for changeset c5d34faaec8f 2013-04-28 11:35:48 -05:00
Jim Miller 1079ed565c Added tag calibre-plugin-1.7.24 for changeset c5d34faaec8f 2013-04-28 11:35:33 -05:00
Jim Miller 17ab4a3d25 Bump versions. 2013-04-28 11:35:23 -05:00
Jim Miller 2566e434ee Tweak 'skip update when the story URL would be changed' feature. 2013-04-28 11:34:46 -05:00
Jim Miller 91f5269453 Offer user chance to skip update when the story URL would be changed. 2013-04-27 18:17:41 -05:00
Jim Miller 235f00a5e6 After adding to reject list, delete only rejects, not those removed from list. 2013-04-27 17:36:28 -05:00
Jim Miller aad4a26131 Don't strip lead/trail whitespace from replace_metadata, add \s->' ' in replacements 2013-04-26 10:40:13 -05:00
Jim Miller f20a02a2bc Catch errors on db.set_cover(). 2013-04-25 22:00:58 -05:00
Jim Miller 1ba1da4d65 Moved tag FanFictionDownLoader-4.4.56 to changeset 6ea4b9910113 (from changeset 0252e03d11c4) 2013-04-12 10:35:27 -05:00
Jim Miller a951fc3d6c Fix index.html. Really this time. 2013-04-12 10:35:08 -05:00
Jim Miller 606785f6e7 Moved tag FanFictionDownLoader-4.4.56 to changeset 0252e03d11c4 (from changeset 14a9a3ca53c1) 2013-04-12 10:32:37 -05:00
Jim Miller 842ab4feeb Fix index.html. 2013-04-12 10:32:18 -05:00
Jim Miller 029af794cf Added tag FanFictionDownLoader-4.4.56 for changeset 14a9a3ca53c1 2013-04-12 10:25:11 -05:00
Jim Miller 87033bd4a3 Added tag calibre-plugin-1.7.23 for changeset 14a9a3ca53c1 2013-04-12 10:24:59 -05:00
Jim Miller c8577893c4 Bump versions. 2013-04-12 10:24:48 -05:00
Jim Miller a822064da0 Remove dead site jlaunlimited.com 2013-04-11 19:51:29 -05:00
Jim Miller af7c717c20 Add new sites to index.html 2013-04-11 17:11:54 -05:00
Jim Miller 56c75350a6 anthology_title_pattern feature 2013-04-11 17:04:45 -05:00
Jim Miller 34b5076753 Add 'url' to chapter custom formats and class="skip_on_ffdl_update" for updates. 2013-04-10 13:30:05 -05:00
Jim Miller f0847809ad Improved error handling for mobi issues. 2013-04-09 20:17:37 -05:00
Jim Miller 976ddf827e Fix for no chapter name for one chapter stories on TtH. 2013-04-09 20:17:16 -05:00
Jim Miller da188234ac Sync up plugin-defaults.ini 2013-04-09 17:38:18 -05:00
Jim Miller 89a676d0b6 Add site: asr3.slashzone.org 2013-04-09 17:36:54 -05:00
Jim Miller 1d48b7c72c Add site: netraptor.org 2013-04-09 17:12:23 -05:00
Jim Miller ce82d96163 Fix author URLs for sites with leading 'dir' in URL. 2013-04-09 16:58:36 -05:00
Jim Miller 049486a059 Add site: tokra.fandomnet.com. 2013-04-09 16:30:24 -05:00
Jim Miller d098bdbdc8 Improve error reporting on CLI. 2013-04-09 16:29:56 -05:00
Jim Miller 2dd85dd044 Added tag FanFictionDownLoader-4.4.55 for changeset 69dd14744c87 2013-04-08 12:28:10 -05:00
Jim Miller fcc5c7ffcc Added tag calibre-plugin-1.7.22 for changeset 69dd14744c87 2013-04-08 12:27:58 -05:00
Jim Miller 9c25271a59 Remove dead sites prisonbreakfic.net, svufiction.com; bump versions. 2013-04-08 12:27:13 -05:00
Jim Miller 6dd5522b7a Include notes with chapter text for thewriterscoffeeshop.com. 2013-04-07 12:13:16 -05:00
Jim Miller aaccb45df5 Fix AO3 series url. 2013-04-07 12:12:51 -05:00
Jim Miller 7b052d353a Added tag FanFictionDownLoader-4.4.54 for changeset fa9a7e44cf2f 2013-04-05 12:12:22 -05:00
Jim Miller d969fbd251 Added tag calibre-plugin-1.7.21 for changeset fa9a7e44cf2f 2013-04-05 12:11:59 -05:00
Jim Miller bcc4ee5efd Bump versions. 2013-04-05 12:11:49 -05:00
Jim Miller 6b2a03dc8f Use ini sections both w/ and w/o www. for all sites. Without takes precedence. 2013-04-05 12:08:55 -05:00
Jim Miller a5a3d284b5 Fix persisting AddDialog size. PI. 2013-04-04 21:30:57 -05:00
Jim Miller b3bf2fc50a Update dark-solace.org/elysian adapter to properly do is_adult. 2013-04-04 17:04:12 -05:00
Jim Miller 22f4cc76fc Use ini sections both w/ and w/o www. for all sites. Canonical takes precedence. 2013-04-04 17:03:44 -05:00
Jim Miller c02ad3f6d5 Update dark-solace.org/elysian adapter for site changes. 2013-04-03 12:15:47 -05:00
Jim Miller e65e209ab3 Fix for drag & drop of text emails. 2013-04-03 12:12:34 -05:00
Jim Miller d940936713 Added tag FanFictionDownLoader-4.4.53 for changeset 5b6e38359ca4 2013-04-01 10:06:06 -05:00
Jim Miller 423e1d2840 Added tag calibre-plugin-1.7.20 for changeset 5b6e38359ca4 2013-04-01 10:05:47 -05:00
Jim Miller e1c2c1a2a2 Another fix for fanfiction.net changes. 2013-04-01 10:05:30 -05:00
Jim Miller 0277ef7590 Added tag FanFictionDownLoader-4.4.52 for changeset b7d0e24dc68a 2013-03-28 20:42:13 -05:00
Jim Miller 63f30c12ab Added tag calibre-plugin-1.7.19 for changeset b7d0e24dc68a 2013-03-28 20:41:57 -05:00
Jim Miller c6875d472d Bump cli/web version too. 2013-03-28 20:41:43 -05:00
Jim Miller 5758e364e4 Bump version. 2013-03-28 20:39:18 -05:00
Jim Miller d9a99fb7e9 Another fix for fanfiction.net changes. 2013-03-28 16:40:59 -05:00
Jim Miller 0a32cdb277 Added tag FanFictionDownLoader-4.4.51 for changeset aaf38d4e578b 2013-03-28 12:31:23 -05:00
Jim Miller 00c1fe8704 Added tag calibre-plugin-1.7.18 for changeset aaf38d4e578b 2013-03-28 12:31:10 -05:00
Jim Miller bb96ecd5fc Bump versions. 2013-03-28 12:30:57 -05:00
Jim Miller 64866f7da6 Add author to chapter TOC for multi-author stories on TtH and WraithBait.
AO3 doesn't reliably report chapter author.
2013-03-28 12:19:32 -05:00
Jim Miller 73815bda43 CLI: Add normalized option when getting story URL list from a URL. 2013-03-28 11:01:52 -05:00
Jim Miller d54acd936e PI: Allow partial download with <url>[1-5] in addition to <url>[1,5]. 2013-03-28 11:01:14 -05:00
Jim Miller e88244bebd Fix for failure to correctly load old JSON settings. 2013-03-28 11:00:21 -05:00
Jim Miller 02bdaf1086 Added tag FanFictionDownLoader-4.4.50 for changeset 3452b4bc5b3e 2013-03-26 13:46:20 -05:00
Jim Miller 21d5a39958 Added tag calibre-plugin-1.7.17 for changeset 3452b4bc5b3e 2013-03-26 13:46:08 -05:00
51 changed files with 3330 additions and 1772 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# ffd-retief-hrd fanfictiondownloader
application: fanfictiondownloader
version: 4-4-50
version: 4-4-63
runtime: python27
api_version: 1
threadsafe: true
+2 -3
View File
@@ -1,11 +1,10 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2011, Jim Miller'
__copyright__ = '2013, Jim Miller'
__docformat__ = 'restructuredtext en'
# The class that all Interface Action plugin wrappers must inherit from
@@ -27,7 +26,7 @@ class FanFictionDownLoaderBase(InterfaceActionBase):
description = 'UI plugin to download FanFiction stories from various sites.'
supported_platforms = ['windows', 'osx', 'linux']
author = 'Jim Miller'
version = (1, 7, 17)
version = (1, 7, 30)
minimum_calibre_version = (0, 8, 57)
#: This field defines the GUI plugin class that contains all the code
+2 -2
View File
@@ -239,8 +239,8 @@ class SizePersistedDialog(QDialog):
self.restoreGeometry(self.geom)
def dialog_closing(self, result):
geom = bytearray(self.saveGeometry())
gprefs[self.unique_pref_name] = geom
self.geom = bytearray(self.saveGeometry())
gprefs[self.unique_pref_name] = self.geom
class ReadOnlyTableWidgetItem(QTableWidgetItem):
+46 -29
View File
@@ -13,10 +13,10 @@ from collections import OrderedDict
from PyQt4.Qt import (QDialog, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QLineEdit, QFont, QWidget, QTextEdit, QComboBox,
QCheckBox, QPushButton, QTabWidget, QVariant, QScrollArea,
QDialogButtonBox )
QDialogButtonBox, QGroupBox )
from calibre.utils.config import JSONConfig
from calibre.gui2.ui import get_gui
from calibre.gui2 import dynamic, info_dialog
from calibre_plugins.fanfictiondownloader_plugin.prefs import prefs, PREFS_NAMESPACE
from calibre_plugins.fanfictiondownloader_plugin.dialogs \
@@ -266,13 +266,16 @@ class BasicTab(QWidget):
self.plugin_action = plugin_action
QWidget.__init__(self)
self.l = QVBoxLayout()
self.setLayout(self.l)
topl = QVBoxLayout()
self.setLayout(topl)
label = QLabel('These settings control the basic features of the plugin--downloading FanFiction.')
label.setWordWrap(True)
self.l.addWidget(label)
self.l.addSpacing(5)
topl.addWidget(label)
defs_gb = groupbox = QGroupBox("Defaults Options on Download")
self.l = QVBoxLayout()
groupbox.setLayout(self.l)
tooltip = "On each download, FFDL offers an option to select the output format. <br />This sets what that option will default to."
horz = QHBoxLayout()
@@ -317,7 +320,9 @@ class BasicTab(QWidget):
self.updateepubcover.setChecked(prefs['updateepubcover'])
self.l.addWidget(self.updateepubcover)
self.l.addSpacing(10)
cali_gb = groupbox = QGroupBox("Updating Calibre Options")
self.l = QVBoxLayout()
groupbox.setLayout(self.l)
self.deleteotherforms = QCheckBox('Delete other existing formats?',self)
self.deleteotherforms.setToolTip('Check this to automatically delete all other ebook formats when updating an existing book.\nHandy if you have both a Nook(epub) and Kindle(mobi), for example.')
@@ -334,14 +339,24 @@ class BasicTab(QWidget):
self.keeptags.setChecked(prefs['keeptags'])
self.l.addWidget(self.keeptags)
self.l.addSpacing(10)
self.checkforseriesurlid = QCheckBox("Check for existing Series Anthology books?",self)
self.checkforseriesurlid.setToolTip("Check for existings Series Anthology books using each new story's series URL before downloading.\nOffer to skip downloading if a Series Anthology is found.")
self.checkforseriesurlid.setChecked(prefs['checkforseriesurlid'])
self.l.addWidget(self.checkforseriesurlid)
self.lookforurlinhtml = QCheckBox("Search EPUB text for Story URL?",self)
self.lookforurlinhtml.setToolTip("Look for first valid story URL inside EPUB text if not found in metadata.\nSomewhat risky, could find wrong URL depending on EPUB content.\nAlso finds and corrects bad ffnet URLs from ficsaver.com files.")
self.lookforurlinhtml.setChecked(prefs['lookforurlinhtml'])
self.l.addWidget(self.lookforurlinhtml)
self.showmarked = QCheckBox("Show added/updated books when finished?",self)
self.showmarked.setToolTip("Show added/updated books only when finished.\nYou can also manually search for 'marked:ffdl_success'.\n'marked:ffdl_failed' is also available, or search 'marked:ffdl' for both.")
self.showmarked.setChecked(prefs['showmarked'])
self.l.addWidget(self.showmarked)
self.l.addSpacing(10)
gui_gb = groupbox = QGroupBox("GUI Options")
self.l = QVBoxLayout()
groupbox.setLayout(self.l)
self.urlsfromclip = QCheckBox('Take URLs from Clipboard?',self)
self.urlsfromclip.setToolTip('Prefill URLs from valid URLs in Clipboard when Adding New.')
@@ -359,7 +374,9 @@ class BasicTab(QWidget):
self.adddialogstaysontop.setChecked(prefs['adddialogstaysontop'])
self.l.addWidget(self.adddialogstaysontop)
self.l.addSpacing(10)
misc_gb = groupbox = QGroupBox("Misc Options")
self.l = QVBoxLayout()
groupbox.setLayout(self.l)
# this is a cheat to make it easier for users to realize there's a new include_images features.
self.includeimages = QCheckBox("Include images in EPUBs?",self)
@@ -367,43 +384,43 @@ class BasicTab(QWidget):
self.includeimages.setChecked(prefs['includeimages'])
self.l.addWidget(self.includeimages)
self.lookforurlinhtml = QCheckBox("Search EPUB text for Story URL?",self)
self.lookforurlinhtml.setToolTip("Look for first valid story URL inside EPUB text if not found in metadata.\nSomewhat risky, could find wrong URL depending on EPUB content.\nAlso finds and corrects bad ffnet URLs from ficsaver.com files.")
self.lookforurlinhtml.setChecked(prefs['lookforurlinhtml'])
self.l.addWidget(self.lookforurlinhtml)
self.checkforseriesurlid = QCheckBox("Check for existing Series Anthology books?",self)
self.checkforseriesurlid.setToolTip("Check for existings Series Anthology books using each new story's series URL before downloading.\nOffer to skip downloading if a Series Anthology is found.")
self.checkforseriesurlid.setChecked(prefs['checkforseriesurlid'])
self.l.addWidget(self.checkforseriesurlid)
self.injectseries = QCheckBox("Inject calibre Series when none found?",self)
self.injectseries.setToolTip("If no series is found, inject the calibre series (if there is one) so it appears on the FFDL title page(not cover).")
self.injectseries.setChecked(prefs['injectseries'])
self.l.addWidget(self.injectseries)
self.l.addSpacing(10)
rej_gb = groupbox = QGroupBox("Reject List")
self.l = QVBoxLayout()
groupbox.setLayout(self.l)
horz = QHBoxLayout()
self.rejectlist = QPushButton('Edit Reject URL List', self)
self.rejectlist.setToolTip("Edit list of URLs FFDL will automatically Reject.")
self.rejectlist.clicked.connect(self.show_rejectlist)
horz.addWidget(self.rejectlist)
self.l.addWidget(self.rejectlist)
self.reject_urls = QPushButton('Add Reject URLs', self)
self.reject_urls.setToolTip("Add additional URLs to Reject as text.")
self.reject_urls.clicked.connect(self.add_reject_urls)
horz.addWidget(self.reject_urls)
self.l.addWidget(self.reject_urls)
self.reject_reasons = QPushButton('Edit Reject Reasons List', self)
self.reject_reasons.setToolTip("Customize the Reasons presented when Rejecting URLs")
self.reject_reasons.clicked.connect(self.show_reject_reasons)
horz.addWidget(self.reject_reasons)
self.l.addWidget(self.reject_reasons)
topl.addWidget(defs_gb)
horz = QHBoxLayout()
topl.addLayout(horz)
horz.addWidget(cali_gb)
horz.addWidget(rej_gb)
self.l.addLayout(horz)
self.l.insertStretch(-1)
horz = QHBoxLayout()
topl.addLayout(horz)
horz.addWidget(gui_gb)
horz.addWidget(misc_gb)
topl.insertStretch(-1)
def set_collisions(self):
prev=self.collision.currentText()
+69 -15
View File
@@ -17,7 +17,8 @@ from PyQt4 import QtGui
from PyQt4.Qt import (QDialog, QTableWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
QPushButton, QString, QLabel, QCheckBox, QIcon, QLineEdit,
QComboBox, QVariant, QProgressDialog, QTimer, QDialogButtonBox,
QPixmap, Qt, QAbstractItemView, SIGNAL, QTextEdit, pyqtSignal)
QPixmap, Qt, QAbstractItemView, SIGNAL, QTextEdit, pyqtSignal,
QGroupBox, QFrame)
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.complete2 import EditWithComplete
@@ -48,18 +49,21 @@ anthology_collision_order=[UPDATE,
UPDATEALWAYS,
OVERWRITEALWAYS]
gpstyle='QGroupBox {border:0; padding-top:10px; padding-bottom:0px; margin-bottom:0px;}' # background-color:red;
class RejectUrlEntry:
matchpat=re.compile(r"^(?P<url>[^,]+)(,(?P<fullnote>(((?P<title>.+) by (?P<auth>.+?)( - (?P<note>.+))?)|.*)))?$")
def __init__(self,url_or_line,note=None,title=None,auth=None,
addreasontext=None,fromline=False):
addreasontext=None,fromline=False,book_id=None):
self.url=url_or_line
self.note=note
self.title=title
self.auth=auth
self.valid=False
self.book_id=book_id
if fromline:
mc = re.match(self.matchpat,url_or_line)
@@ -199,6 +203,24 @@ class AddNewDialog(SizePersistedDialog):
# elements to show again when doing *update* merge
self.mergeupdateshow = []
self.groupbox = QGroupBox("Show Download Options")
self.groupbox.setCheckable(True)
self.groupbox.setChecked(False)
self.groupbox.setFlat(True)
print("style:%s"%self.groupbox.styleSheet())
self.groupbox.setStyleSheet(gpstyle);
self.gbf = QFrame()
self.gbl = QVBoxLayout()
self.gbl.addWidget(self.gbf)
self.groupbox.setLayout(self.gbl)
self.gbl = QVBoxLayout()
self.gbf.setLayout(self.gbl)
self.l.addWidget(self.groupbox)
self.gbf.setVisible(False)
self.groupbox.toggled.connect(self.gbf.setVisible)
horz = QHBoxLayout()
label = QLabel('Output &Format:')
self.mergehide.append(label)
@@ -214,7 +236,7 @@ class AddNewDialog(SizePersistedDialog):
horz.addWidget(label)
label.setBuddy(self.fileform)
horz.addWidget(self.fileform)
self.l.addLayout(horz)
self.gbl.addLayout(horz)
self.mergehide.append(self.fileform)
horz = QHBoxLayout()
@@ -229,7 +251,7 @@ class AddNewDialog(SizePersistedDialog):
self.collision.setCurrentIndex(i)
self.collisionlabel.setBuddy(self.collision)
horz.addWidget(self.collision)
self.l.addLayout(horz)
self.gbl.addLayout(horz)
self.mergehide.append(self.collisionlabel)
self.mergehide.append(self.collision)
self.mergeupdateshow.append(self.collisionlabel)
@@ -249,7 +271,7 @@ class AddNewDialog(SizePersistedDialog):
horz.addWidget(self.updateepubcover)
self.mergehide.append(self.updateepubcover)
self.l.addLayout(horz)
self.gbl.addLayout(horz)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.ok_clicked)
@@ -258,8 +280,8 @@ class AddNewDialog(SizePersistedDialog):
# invoke the
def ok_clicked(self):
self.dialog_closing(None) # save persistent size.
self.hide()
print("ok_clicked called")
self.go_signal.emit( self.get_ffdl_options(),
self.get_urlstext(),
self.merge,
@@ -287,6 +309,8 @@ class AddNewDialog(SizePersistedDialog):
self.newmerge = newmerge
self.extraoptions = extraoptions
self.extrapayload = extrapayload
self.groupbox.setVisible(not(self.merge and self.newmerge))
if self.merge:
self.toplabel.setText('Story URL(s) for anthology, one per line:')
@@ -624,8 +648,25 @@ class UpdateExistingDialog(SizePersistedDialog):
options_layout = QHBoxLayout()
groupbox = QGroupBox("Show Download Options")
groupbox.setCheckable(True)
groupbox.setChecked(False)
groupbox.setFlat(True)
groupbox.setStyleSheet(gpstyle)
gbf = QFrame()
gbl = QVBoxLayout()
gbl.addWidget(gbf)
groupbox.setLayout(gbl)
gbl = QHBoxLayout()
gbf.setLayout(gbl)
options_layout.addWidget(groupbox)
gbf.setVisible(False)
groupbox.toggled.connect(gbf.setVisible)
label = QLabel('Output &Format:')
options_layout.addWidget(label)
gbl.addWidget(label)
self.fileform = QComboBox(self)
self.fileform.addItem('epub')
self.fileform.addItem('mobi')
@@ -635,10 +676,10 @@ class UpdateExistingDialog(SizePersistedDialog):
self.fileform.setToolTip('Choose output format to create. May set default from plugin configuration.')
self.fileform.activated.connect(self.set_collisions)
label.setBuddy(self.fileform)
options_layout.addWidget(self.fileform)
gbl.addWidget(self.fileform)
label = QLabel('Update Mode:')
options_layout.addWidget(label)
gbl.addWidget(label)
self.collision = QComboBox(self)
self.collision.setToolTip("What sort of update to perform. May set default from plugin configuration.")
# add collision options
@@ -647,17 +688,19 @@ class UpdateExistingDialog(SizePersistedDialog):
if i > -1:
self.collision.setCurrentIndex(i)
label.setBuddy(self.collision)
options_layout.addWidget(self.collision)
gbl.addWidget(self.collision)
self.updatemeta = QCheckBox('Update Calibre &Metadata?',self)
self.updatemeta.setToolTip("Update metadata for existing stories in Calibre from web site?\n(Columns set to 'New Only' in the column tabs will only be set for new books.)")
self.updatemeta.setChecked(prefs['updatemeta'])
options_layout.addWidget(self.updatemeta)
gbl.addWidget(self.updatemeta)
self.updateepubcover = QCheckBox('Update EPUB Cover?',self)
self.updateepubcover.setToolTip('Update book cover image from site or defaults (if found) <i>inside</i> the EPUB when EPUB is updated.')
self.updateepubcover.setChecked(prefs['updateepubcover'])
options_layout.addWidget(self.updateepubcover)
gbl.addWidget(self.updateepubcover)
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
button_box.accepted.connect(self.accept)
@@ -828,7 +871,9 @@ class RejectListTableWidget(QTableWidget):
def populate_table_row(self, row, rej):
self.setItem(row, 0, ReadOnlyTableWidgetItem(rej.url))
url_cell = ReadOnlyTableWidgetItem(rej.url)
url_cell.setData(Qt.UserRole, QVariant(rej.book_id))
self.setItem(row, 0, url_cell)
self.setItem(row, 1, ReadOnlyTableWidgetItem(rej.title))
self.setItem(row, 2, ReadOnlyTableWidgetItem(rej.auth))
@@ -950,10 +995,19 @@ class RejectListDialog(SizePersistedDialog):
rejectrows = []
for row in range(self.rejects_table.rowCount()):
url = unicode(self.rejects_table.item(row, 0).text()).strip()
book_id = self.rejects_table.item(row, 0).data(Qt.UserRole).toPyObject()
title = unicode(self.rejects_table.item(row, 1).text()).strip()
auth = unicode(self.rejects_table.item(row, 2).text()).strip()
note = unicode(self.rejects_table.cellWidget(row, 3).currentText()).strip()
rejectrows.append(RejectUrlEntry(url,note,title,auth,self.get_reason_text()))
rejectrows.append(RejectUrlEntry(url,note,title,auth,self.get_reason_text(),book_id=book_id))
return rejectrows
def get_reject_list_ids(self):
rejectrows = []
for row in range(self.rejects_table.rowCount()):
book_id = self.rejects_table.item(row, 0).data(Qt.UserRole).toPyObject()
if book_id:
rejectrows.append(book_id)
return rejectrows
def get_reason_text(self):
+84 -23
View File
@@ -140,7 +140,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
file_path = os.path.join(calibre_config_dir,
*("plugins/fanfictiondownloader_macmenuhack.txt".split('/')))
file_path = os.path.abspath(file_path)
print("macmenuhack file_path:%s"%file_path)
print("Plugin %s macmenuhack file_path:%s"%(self.name,file_path))
self.macmenuhack = os.access(file_path, os.F_OK)
return self.macmenuhack
@@ -408,12 +408,12 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
def reject_list_urls_finish(self, book_list):
# construct reject list of tuples:
# (calibre_id, url, "title, authors", old reject note).
# construct reject list of objects
reject_list = [ RejectUrlEntry(x['url'],
x['oldrejnote'],
x['title'],
', '.join(x['author']))
', '.join(x['author']),
book_id=x['calibre_id'])
for x in book_list if x['good'] ]
if reject_list:
d = RejectListDialog(self.gui,reject_list,
@@ -426,7 +426,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
rejecturllist.add(d.get_reject_list())
if d.get_deletebooks():
self.gui.iactions['Remove Books'].delete_books()
self.gui.iactions['Remove Books'].do_library_delete(d.get_reject_list_ids())
else:
message="<p>Rejecting FFDL URLs: None of the books selected have FanFiction URLs.</p><p>Proceed to Remove?</p>"
@@ -661,6 +661,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
url = book['url']
print("url:%s"%url)
mi = None
if not merge: # skip reject list when merging.
if rejecturllist.check(url):
@@ -845,6 +846,42 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
book['icon'] = 'edit-redo.png'
book['status'] = 'Update'
if book_id and mi: # book_id and mi only set if matched by title/author.
liburl = self.get_story_url(db,book_id)
if book['url'] != liburl:
if collision in (OVERWRITE,OVERWRITEALWAYS):
updat="overwrit"
else:
updat="updat"
if not question_dialog(self.gui, 'Change Story URL?',
'<h3>Change Story URL?</h3>'+
'<p><b>%s</b> by <b>%s</b> is already in your library with a different source URL:</p>'%
(mi.title,', '.join(mi.author))+
'<p>In library: <a href="%(liburl)s">%(liburl)s</a></p><p>New URL: <a href="%(newurl)s">%(newurl)s</a></p>'%
{'liburl':liburl,'newurl':book['url']}+
"<p>Click '<b>Yes</b>' to %se book with new URL.</p>"%updat+
"<p>Click '<b>No</b>' to skip %sing this book.</p>"%updat,
show_copy_button=False):
if question_dialog(self.gui, 'Download as New Book?',
'<h3>Download as New Book?</h3>'+
'<p><b>%s</b> by <b>%s</b> is already in your library with a different source URL.</p>'%
(mi.title,', '.join(mi.author))+
'<p>You chose not to update the existing book. Do you want to add a new book for this URL?</p>'+
'<p>New URL: <a href="%(newurl)s">%(newurl)s</a></p>'%
{'newurl':book['url']}+
"<p>Click '<b>Yes</b>' to a new book with new URL.</p>"+
"<p>Click '<b>No</b>' to skip URL.</p>",
show_copy_button=False):
book_id = None
mi = None
book['calibre_id'] = None
else:
book['comment'] = "Update declined by user due to differing story URL(%s)"%liburl
book['good']=False
book['icon']='rotate-right.png'
book['status'] = 'Different URL'
return
if book_id != None and collision != ADDNEW:
if collision in (CALIBREONLY):
book['comment'] = 'Metadata collected.'
@@ -889,7 +926,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
db.copy_format_to(book_id,fileform,tmp,index_is_id=True)
print("existing epub tmp:"+tmp.name)
book['epub_for_update'] = tmp.name
if book_id != None and prefs['injectseries']:
mi = db.get_metadata(book_id,index_is_id=True)
if not book['series'] and mi.series != None:
@@ -1159,7 +1196,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
if 'mergebook' in options:
existingbook = options['mergebook']
#print("existingbook:\n%s"%existingbook)
mergebook = self.merge_meta_books(existingbook,good_list)
mergebook = self.merge_meta_books(existingbook,good_list,options['fileform'])
if 'mergebook' in options:
mergebook['calibre_id'] = options['mergebook']['calibre_id']
@@ -1281,16 +1318,10 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
existingepub = db.format(book_id,'EPUB',index_is_id=True, as_file=True)
epubmi = get_metadata(existingepub,'EPUB')
if epubmi.cover_data[1] is not None:
db.set_cover(book_id, epubmi.cover_data[1])
# set author link if found. All current adapters have authorUrl, except anonymous on AO3.
if 'authorUrl' in book['all_metadata']:
authurls = book['all_metadata']['authorUrl'].split(", ")
for i, auth in enumerate(book['author']):
#print("===Update author url for %s to %s"%(auth,authurls[i]))
autid=db.get_author_id(auth)
db.set_link_field_for_author(autid, unicode(authurls[i]),
commit=False, notify=False)
try:
db.set_cover(book_id, epubmi.cover_data[1])
except:
print("Failed to set_cover, skipping")
# implement 'newonly' flags here by setting to the current
# value again.
@@ -1369,7 +1400,11 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
if flag == 'r' or book['added']: # flag 'n' isn't actually needed--*always* set if configured and new book.
if coldef['datatype'] in ('int','float'): # for favs, etc--site specific metadata.
val = unicode(book['all_metadata'][meta]).replace(",","")
if 'anthology_meta_list' in book and meta in book['anthology_meta_list']:
# re-split list, strip commas, convert to floats, sum up.
val = sum([ float(x.replace(",","")) for x in book['all_metadata'][meta].split(", ") ])
else:
val = unicode(book['all_metadata'][meta]).replace(",","")
else:
val = book['all_metadata'][meta]
db.set_custom(book_id, val, label=label, commit=False)
@@ -1390,6 +1425,16 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
db.set_custom(book_id, ", ".join(vallist), label=label, commit=False)
# set author link if found. All current adapters have authorUrl, except anonymous on AO3.
# Moved down so author's already in the DB.
if 'authorUrl' in book['all_metadata']:
authurls = book['all_metadata']['authorUrl'].split(", ")
for i, auth in enumerate(book['author']):
#print("===Update author url for %s to %s"%(auth,authurls[i]))
autid=db.get_author_id(auth)
db.set_link_field_for_author(autid, unicode(authurls[i]),
commit=False, notify=False)
db.commit()
if 'Generate Cover' in self.gui.iactions and (book['added'] or not prefs['gcnewonly']):
@@ -1533,7 +1578,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
def convert_url_to_book(self, url):
book = self.make_book()
# look here for [\d,\d] at end of url, and remove?
mc = re.match(r"^(?P<url>.*?)(?:\[(?P<begin>\d+)?(?P<comma>,)?(?P<end>\d+)?\])?$",url)
mc = re.match(r"^(?P<url>.*?)(?:\[(?P<begin>\d+)?(?P<comma>[,-])?(?P<end>\d+)?\])?$",url)
#print("url:(%s) begin:(%s) end:(%s)"%(mc.group('url'),mc.group('begin'),mc.group('end')))
url = mc.group('url')
book['begin'] = mc.group('begin')
@@ -1623,7 +1668,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
# identifiers have :->| in uri.
# print("uri from ident uri:%s"%identifiers['uri'].replace('|',':'))
return identifiers['uri'].replace('|',':')
elif path.lower().endswith('.epub'):
elif path and path.lower().endswith('.epub'):
existingepub = path
## only epub has URL in it--at least where I can easily find it.
@@ -1642,12 +1687,13 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
def is_good_downloader_url(self,url):
return adapters.getNormalStoryURL(url)
def merge_meta_books(self,existingbook,book_list):
def merge_meta_books(self,existingbook,book_list,fileform):
book = self.make_book()
book['author'] = []
book['tags'] = []
book['url'] = ''
book['all_metadata'] = {}
book['anthology_meta_list'] = {}
book['comment'] = ''
book['added'] = True
book['good'] = True
@@ -1714,12 +1760,16 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
book['all_metadata'][k]=book['all_metadata'][k]+"\n\n"+v
else:
book['all_metadata'][k]=book['all_metadata'][k]+", "+v
# flag psuedo list element. Used so numeric
# cust cols can convert back to numbers and
# add.
book['anthology_meta_list'][k]=True
if existingbook:
book['title'] = deftitle = existingbook['title']
book['comments'] = existingbook['comments']
else:
book['title'] = deftitle = book_list[0]['title']+" Anthology"
book['title'] = deftitle = book_list[0]['title']
book['comments'] = "Anthology containing:\n" + \
"\n".join([ "%s by %s"%(b['title'],', '.join(b['author'])) for b in book_list ])
# book['all_metadata']['description']
@@ -1727,12 +1777,22 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
# if all same series, use series for name. But only if all and not previous named
if len(serieslist) == len(book_list):
series = serieslist[0]
book['title'] = series+" Anthology"
book['title'] = series
for sr in serieslist:
if series != sr:
book['title'] = deftitle;
break
configuration = get_ffdl_config(book['url'],fileform)
print("anthology_title_pattern:%s"%configuration.getConfig('anthology_title_pattern'))
if configuration.getConfig('anthology_title_pattern'):
tmplt = Template(configuration.getConfig('anthology_title_pattern'))
book['title'] = tmplt.safe_substitute({'title':book['title']}).encode('utf8')
else:
# No setting, do fall back default. Shouldn't happen,
# should always have a version in defaults.
book['title'] = book['title']+" Anthology"
book['all_metadata']['title'] = book['title'] # because custom columns are set from all_metadata
book['all_metadata']['author'] = ", ".join(book['author'])
book['author_sort']=book['author']
@@ -1741,6 +1801,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
book['tags'].remove(v)
book['tags'].append('Anthology')
book['all_metadata']['anthology'] = "true"
return book
def split_text_to_urls(urls):
+8 -8
View File
@@ -61,6 +61,13 @@ default_prefs['allow_custcol_from_ini'] = True
default_prefs['std_cols_newonly'] = {}
# This is where all preferences for this plugin *were* stored
# Remember that this name (i.e. plugins/fanfictiondownloader_plugin) is also
# in a global namespace, so make it as unique as possible.
# You should always prefix your config file name with plugins/,
# so as to ensure you dont accidentally clobber a calibre config file
old_prefs = JSONConfig('plugins/fanfictiondownloader_plugin')
def set_library_config(library_config,db):
db.prefs.set_namespaced(PREFS_NAMESPACE,
PREFS_KEY_SETTINGS,
@@ -74,7 +81,7 @@ def get_library_config(db):
if library_id in old_prefs:
#print("get prefs from old_prefs")
library_config = old_prefs[library_id]
set_library_config(library_config)
set_library_config(library_config,db)
old_prefs["migrated to library db %s"%library_id] = old_prefs[library_id]
del old_prefs[library_id]
@@ -84,13 +91,6 @@ def get_library_config(db):
copy.deepcopy(default_prefs))
return library_config
# This is where all preferences for this plugin *were* stored
# Remember that this name (i.e. plugins/fanfictiondownloader_plugin) is also
# in a global namespace, so make it as unique as possible.
# You should always prefix your config file name with plugins/,
# so as to ensure you dont accidentally clobber a calibre config file
old_prefs = JSONConfig('plugins/fanfictiondownloader_plugin')
# fake out so I don't have to change the prefs calls anywhere. The
# Java programmer in me is offended by op-overloading, but it's very
# tidy.
+139 -6
View File
@@ -1,4 +1,4 @@
# Copyright 2012 Fanficdownloader team
# Copyright 2013 Fanficdownloader team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -169,6 +169,8 @@ extratags: FanFiction
## *Five* part lines. Effect only when trailing conditional key=>regexp matches
## metakey[,metakey]=>pattern=>replacement[&&conditionalkey=>regexp]
## Note that if metakey == conditionalkey the conditional is ignored.
## You can use \s in the replacement to add explicit spaces. (The config parser
## tends to discard trailing spaces.)
#replace_metadata:
# genre,category=>Sci-Fi=>SF
# Puella Magi Madoka Magica.* => Madoka
@@ -189,6 +191,10 @@ extratags: FanFiction
## summary.
keep_summary_html:true
## If set true, any style attributes on tags in the story HTML will be
## kept. Useful for keeping extra colors & formatting from original.
#keep_style_attr: false
## Don't like the numbers at the start of chapter titles on some
## sites? You can use strip_chapter_numbers to strip them off. Just
## want to make them all look the same? Strip them off, then add them
@@ -273,7 +279,7 @@ output_css:
[txt]
## Add URLs since there aren't links.
titlepage_entries: series,seriesUrl,category,genre,language,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,numWords,site,storyUrl, authorUrl, description
titlepage_entries: series,seriesUrl,category,genre,language,characters,ships,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,numWords,site,storyUrl, authorUrl, description
## Width to word wrap text output. 0 indicates no wrapping.
wrap_width: 78
@@ -507,6 +513,15 @@ extraships:Severus Snape/Hermione Granger
#username:YourName
#password:yourpassword
[asr3.slashzone.org]
## Site dedicated to these categories/characters/ships
extracategories:The Sentinel
## 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
[bloodties-fans.com]
## Site dedicated to these categories/characters/ships
extracategories:Blood Ties
@@ -571,6 +586,11 @@ extraships:Spike/Buffy
#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
[dramione.org]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
@@ -602,7 +622,7 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
## composite metadata entries. dramione.org, for example, adds
## 'cliches' and then defines as the composite of hermiones,dracos in
## include_in_cliches.
extra_valid_entries:themes,hermiones,dracos,timeline,cliches
extra_valid_entries:themes,hermiones,dracos,timeline,cliches,read,reviews
include_in_cliches:hermiones,dracos
## For another example, you could, by uncommenting this line, include
@@ -647,6 +667,11 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
## 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
@@ -654,11 +679,22 @@ extracategories:Harry Potter
#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.
[fanfic.potterheadsanonymous.com]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
## 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
[fanfiction.portkey.org]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
@@ -702,6 +738,10 @@ extracharacters:Hermione Granger
## personal.ini, not defaults.ini.
#is_adult:true
## 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
@@ -762,6 +802,8 @@ extracategories:West Wing
## Site dedicated to these categories/characters/ships
extracategories:NCIS
[netraptor.org]
[nfacommunity.com]
## Site dedicated to these categories/characters/ships
extracategories:NCIS
@@ -844,6 +886,22 @@ extracategories:Supernatural
extracharacters:Sam,Dean
extraships:Sam/Dean
[scarhead.net]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
## 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
[sg1-heliopolis.com]
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
@@ -918,6 +976,15 @@ extracategories:Harry Potter
## personal.ini, not defaults.ini.
#is_adult:true
[tokra.fandomnet.com]
## Site dedicated to these categories/characters/ships
extracategories:Stargate: SG-1
## 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.adastrafanfic.com]
## Site dedicated to these categories/characters/ships
extracategories:Star Trek
@@ -1148,6 +1215,11 @@ cover_exclusion_regexp:/stories/999/images/.*?_trophy.png
## Site dedicated to these categories/characters/ships
extracategories:NCIS
[www.nickandgreg.net]
## Site dedicated to these categories/characters/ships
extracategories:CSI
extraships:Nick Stokes/Greg Sanders
[www.phoenixsong.net]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
@@ -1160,6 +1232,12 @@ extraships:Harry Potter/Ginny Weasley
#username:YourName
#password:yourpassword
## phoenixsong.net, oddly, can have high rated chapters (login
## required) in the middle of a lower rated story. Use this to force
## FFDL to always login to phoenixsong.net so those stories download
## correctly.
#force_login:true
[www.potionsandsnitches.net]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
@@ -1210,6 +1288,17 @@ extracharacters:Kurt Hummel,Blaine Anderson
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[www.simplyundeniable.com]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
## 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.sinful-desire.org]
## Site dedicated to these categories/characters/ships
extracategories:Supernatural
@@ -1356,3 +1445,47 @@ extracategories:Stargate: Atlantis
## extratags params in all other sections. Only commandline options
## beat overrides.
#extratags:fanficdownloader
[teststory:defaults]
valid_entries:title,author_list,authorId_list,authorUrl_list,storyUrl,
datePublished,dateUpdated,numWords,status,language,series,seriesUrl,
rating,category_list,genre_list,warnings_list,characters_list
# {{storyId}} is a special case--it's the only one that works.
title:Test Story Title {{storyId}}
author_list:Test Author aa
authorId_list:1
authorUrl_list:http://test1.com?authid=1
storyUrl:http://test1.com?sid={{storyId}}
datePublished:1975-03-15
dateUpdated:1975-04-15
numWords:123,456
status:In-Progress
language:English
chaptertitles:Prologue
## Add additional sections with different numbers to get different
## parameters for different story urls.
## test1.com?sid=1000
[teststory:1000]
# note the leading commas when doing add_to_ with valid_entries and *_list
add_to_valid_entries:,favs
title:Testing New Feature {{storyId}}
author_list:Bob Smith
authorId_list:45
authorUrl_list:http://test1.com?authid=45
datePublished:2013-03-15
dateUpdated:2013-04-15
numWords:1456
favs:56
series:The Great Test [4]
seriesUrl:http://test1.com?seriesid=1
rating:Tweenie
category_list:Harry Potter,Furbie,Crossover,Puella Magi Madoka Magica/魔法少女まどか★マギカ,Magical Girl Lyrical Nanoha
genre_list:Fantasy,Comedy,Sci-Fi,Noir
warnings_list:Swearing,Violence
characters_list:Bob Smith,George Johnson,Fred Smythe
chaptertitles:Prologue,Chapter 1\, Xenos on Cinnabar,Chapter 2\, Sinmay on Kintikin,3. Chapter 3
+17 -6
View File
@@ -33,16 +33,24 @@ if sys.version_info >= (2, 7):
rootlogger.addHandler(loghandler)
try:
from fanficdownloader import adapters,writers,exceptions
from fanficdownloader.configurable import Configuration
from fanficdownloader.epubutils import get_dcsource_chaptercount, get_update_data
from fanficdownloader.geturls import get_urls_from_page
from calibre.constants import numeric_version as calibre_version
is_calibre = True
except:
is_calibre = False
# using try/except directly was masking errors during development.
if is_calibre:
# running under calibre
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters,writers,exceptions
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.configurable import Configuration
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.epubutils import get_dcsource_chaptercount, get_update_data
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.geturls import get_urls_from_page
else:
from fanficdownloader import adapters,writers,exceptions
from fanficdownloader.configurable import Configuration
from fanficdownloader.epubutils import get_dcsource_chaptercount, get_update_data
from fanficdownloader.geturls import get_urls_from_page
if sys.version_info < (2, 5):
print "This program requires Python 2.5 or newer."
@@ -94,6 +102,9 @@ def main(argv,
parser.add_option("-l", "--list",
action="store_true", dest="list",
help="Get list of valid story URLs from page given.",)
parser.add_option("-n", "--normalize-list",
action="store_true", dest="normalize",default=False,
help="Get list of valid story URLs from page given, but normalized to standard forms.",)
parser.add_option("-d", "--debug",
action="store_true", dest="debug",
help="Show debug output while downloading.",)
@@ -169,8 +180,8 @@ def main(argv,
(var,val) = opt.split('=')
configuration.set("overrides",var,val)
if options.list:
retlist = get_urls_from_page(args[0], configuration)
if options.list or options.normalize:
retlist = get_urls_from_page(args[0], configuration, normalize=options.normalize)
print "\n".join(retlist)
return
+7 -3
View File
@@ -71,7 +71,6 @@ import adapter_thehexfilesnet
import adapter_dokugacom
import adapter_iketernalnet
import adapter_onedirectionfanfictioncom
import adapter_prisonbreakficnet
import adapter_storiesofardacom
import adapter_samdeanarchivenu
import adapter_destinysgatewaycom
@@ -88,7 +87,6 @@ import adapter_pretendercentrecom
import adapter_darksolaceorg
import adapter_finestoriescom
import adapter_hpfanficarchivecom
import adapter_svufictioncom
import adapter_twilightarchivescom
import adapter_wizardtalesnet
import adapter_nhamagicalworldsus
@@ -103,7 +101,6 @@ import adapter_merlinficdtwinscouk
import adapter_thehookupzonenet
import adapter_bloodtiesfancom
import adapter_indeathnet
import adapter_jlaunlimitedcom
import adapter_qafficcom
import adapter_efpfanficnet
import adapter_potterficscom
@@ -115,6 +112,13 @@ import adapter_imagineeficcom
import adapter_buffynfaithnet
import adapter_psychficcom
import adapter_hennethannunnet
import adapter_tokrafandomnetcom
import adapter_netraptororg
import adapter_asr3slashzoneorg
import adapter_nickandgregnet
import adapter_potterheadsanonymouscom
import adapter_simplyundeniablecom
import adapter_scarheadnet
## This bit of complexity allows adapters to be added by just adding
## importing. It eliminates the long if/else clauses we used to need
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
# Copyright 2011 Fanficdownloader team
#
@@ -292,7 +292,7 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
a = metasoup.find('dd',{'class':"series"})
b = a.find('a', href=re.compile(r"/series/\d+"))
series_name = b.string
series_url = 'http://'+self.host+'/fanfic/'+b['href']
series_url = 'http://'+self.host+b['href']
series_index = int(a.text.split(' ')[1])
self.setSeries(series_name, series_index)
self.story.setMetadata('seriesUrl',series_url)
@@ -0,0 +1,225 @@
# -*- coding: utf-8 -*-
# Copyright 2013 Fanficdownloader team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import time
import logging
logger = logging.getLogger(__name__)
import re
import urllib2
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, makeDate
def getClass():
return Asr3SlashzoneOrgAdapter
class Asr3SlashzoneOrgAdapter(BaseSiteAdapter):
def __init__(self, config, url):
BaseSiteAdapter.__init__(self, config, url)
self.decode = ["Windows-1252",
"utf8"] # 1252 is a superset of iso-8859-1.
# Most sites that claim to be
# iso-8859-1 (and some that claim to be
# utf8) are really windows-1252.
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
self.password = ""
self.is_adult=False
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/archive/viewstory.php?sid='+self.story.getMetadata('storyId'))
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','asr3')
# 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 'asr3.slashzone.org'
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/archive/viewstory.php?sid=1234"
def getSiteURLPattern(self):
return re.escape("http://"+self.getSiteDomain()+"/archive/viewstory.php?sid=")+r"\d+$"
## Getting the chapter list and the meta data, plus 'is adult' checking.
def extractChapterUrlsAndMetadata(self):
if self.is_adult or self.getConfig("is_adult"):
# Weirdly, different sites use different warning numbers.
# If the title search below fails, there's a good chance
# you need a different number. print data at that point
# and see what the 'click here to continue' url says.
addurl = "&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+((?:&amp;ageconsent=ok)?&amp;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 &amp; error in url.
addurl = addurl.replace("&amp;","&")
url = self.url+'&index=1'+addurl
logger.debug("URL 2nd try: "+url)
try:
data = self._fetchUrl(url)
except urllib2.HTTPError, e:
if e.code == 404:
raise exceptions.StoryDoesNotExist(self.url)
else:
raise e
else:
raise exceptions.AdultCheckRequired(self.url)
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
# use BeautifulSoup HTML parser to make everything easier to find.
soup = bs.BeautifulSoup(data)
#print data
# Now go hunting for all the meta data and the chapter list.
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
# Find authorid and URL from... author url.
a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
self.story.setMetadata('authorId',a['href'].split('=')[1])
self.story.setMetadata('authorUrl','http://'+self.host+'/archive/'+a['href'])
self.story.setMetadata('author',a.string)
# 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+'/archive/'+chapter['href']+addurl))
self.story.setMetadata('numChapters',len(self.chapterUrls))
# eFiction sites don't help us out a lot with their meta data
# formating, so it's a little ugly.
metadiv = soup.find('div',{'class':'content'})
smalldiv = metadiv.find('div',{'class':'small'})
categorys = smalldiv.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
for category in categorys:
self.story.addToList('category',category.string)
chars = smalldiv.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
for char in chars:
self.story.addToList('characters',char.string)
ships = smalldiv.parent.findAll('a',href=re.compile(r'browse\.php\?type=class&type_id=2&classid=1'))
for ship in ships:
self.story.addToList('ships',ship.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+'/archive/'+a['href']
# use BeautifulSoup HTML parser to make everything easier to find.
seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url))
# can't use ^viewstory...$ in case of higher rated stories with javascript href.
storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+'))
i=1
for a in storyas:
# skip 'report this' and 'TOC' links
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
self.setSeries(series_name, i)
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 = bs.BeautifulSoup(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)
@@ -211,7 +211,7 @@ class BloodTiesFansComAdapter(BaseSiteAdapter): # XXX
# 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('authorUrl','http://'+self.host+'/fiction/'+a['href'])
self.story.setMetadata('author',a.string)
# Find the chapters:
@@ -184,7 +184,7 @@ class CastleFansOrgAdapter(BaseSiteAdapter): # XXX
# 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('authorUrl','http://'+self.host+'/fanfic/'+a['href'])
self.story.setMetadata('author',a.string)
# Find the chapters:
@@ -49,7 +49,6 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/elysian/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -59,7 +58,7 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
# 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 = "%B %d, %Y"
@staticmethod # must be @staticmethod, don't remove it.
def getSiteDomain():
@@ -79,7 +78,8 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
## Login seems to be reasonably standard across eFiction sites.
def needToLoginCheck(self, data):
if 'This story contains adult content not suitable for children' 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 "Registered Users Only" in data:
return True
else:
return False
@@ -94,17 +94,16 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
params['penname'] = self.getConfig("username")
params['password'] = self.getConfig("password")
params['rememberme'] = '1'
params['sid'] = ''
params['intent'] = ''
params['action'] = 'login'
params['submit'] = 'Submit'
loginUrl = 'http://' + self.getSiteDomain() + '/elysian/user.php'
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
params['penname']))
d = self._fetchUrl(loginUrl, params)
d = self._postUrl(loginUrl, params)
if "User Account Page" not in d : #Member Account
if "Member Account" not in d : #User Account Page
logger.info("Failed to login to URL %s as %s, or have no authorization to access the story" % (loginUrl, params['penname']))
raise exceptions.FailedToLogin(url,params['penname'])
return False
@@ -113,9 +112,19 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
## 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=5"
else:
addurl=""
# index=1 makes sure we see the story chapter index. Some
# sites skip that for one-chapter stories.
url = self.url
url = self.url+'&index=1'+addurl
logger.debug("URL: "+url)
try:
@@ -128,9 +137,30 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
if self.needToLoginCheck(data):
# need to log in for this one.
addurl="&ageconsent=ok"
self.performLogin(url)
data = self._fetchUrl(url+addurl)
data = self._fetchUrl(url)
m = re.search(r"'viewstory.php\?sid=\d+((?:&amp;ageconsent=ok)?&amp;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 &amp; error in url.
addurl = addurl.replace("&amp;","&")
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.")
@@ -142,33 +172,41 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
# Now go hunting for all the meta data and the chapter list.
## Title and author
a = soup.find('div', {'id' : 'pagetitle'})
div = soup.find('div', {'id' : 'pagetitle'})
aut = a.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
aut = div.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
self.story.setMetadata('authorId',aut['href'].split('=')[1])
self.story.setMetadata('authorUrl','http://'+self.host+'/elysian/'+aut['href'])
self.story.setMetadata('author',aut.string)
aut.extract()
self.story.setMetadata('title',a.string[:(len(a.string)-3)])
# first a tag in pagetitle is title
self.story.setMetadata('title',stripHTML(div.find('a')))
# Find the chapters:
chapters=soup.find('select', {'name' : 'chapter'})
if chapters != None:
for chapter in chapters.findAll('option'):
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/elysian/viewstory.php?sid='+self.story.getMetadata('storyId')+'&chapter='+chapter['value']))
else:
self.chapterUrls.append((self.story.getMetadata('title'),url))
# chapters=soup.find('select', {'name' : 'chapter'})
# if chapters != None:
# for chapter in chapters.findAll('option'):
# self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/elysian/viewstory.php?sid='+self.story.getMetadata('storyId')+'&chapter='+chapter['value']))
# else:
# self.chapterUrls.append((self.story.getMetadata('title'),url+"&chapter=1"))
for chapa in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+
self.story.getMetadata('storyId')+'&chapter=\d+')):
self.chapterUrls.append((stripHTML(chapa),'http://'+self.host+'/elysian/'+chapa['href']))
self.story.setMetadata('numChapters',len(self.chapterUrls))
asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')))
for list in asoup.findAll('div', {'class' : re.compile('listbox\s+')}):
a = list.find('a', href=re.compile(r'viewstory.php\?sid='))
if a != None:
if 'viewstory.php?sid='+self.story.getMetadata('storyId') in a['href']:
break
# for metalist in asoup.findAll('div', {'class' : re.compile('listbox\s+')}):
# a = metalist.find('a', href=re.compile(r'viewstory.php\?sid='))
# if a != None:
# if 'viewstory.php?sid='+self.story.getMetadata('storyId') in a['href']:
# break
metalist = asoup.find('a', href=re.compile(r'viewstory.php\?sid='+
self.story.getMetadata('storyId')+'($|[^\d])')).parent.parent
# eFiction sites don't help us out a lot with their meta data
# formating, so it's a little ugly.
@@ -182,7 +220,7 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
# <span class="label">Rated:</span> NC-17<br /> etc
labels = list.findAll('span', {'class' : 'classification'})
labels = metalist.findAll('span', {'class' : 'label'})
for labelspan in labels:
label = labelspan.text
value = labelspan.nextSibling
@@ -190,7 +228,7 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
if 'Summary' in label:
## Everything until the next span class='label'
svalue = ""
while not (defaultGetattr(value,'class') == 'classification' or "Chapters: " in stripHTML(value)):
while not (defaultGetattr(value,'class') == 'label' or "Chapters: " in stripHTML(value)):
svalue += str(value)
value = value.nextSibling
self.setDescription(url,svalue)
@@ -238,7 +276,7 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
try:
# Find Series name from series URL.
a = list.find('a', href=re.compile(r"series.php\?seriesid=\d+"))
a = metalist.find('a', href=re.compile(r"series.php\?seriesid=\d+"))
series_name = a.string
series_url = 'http://'+self.host+'/elysian/'+a['href']
@@ -265,8 +303,7 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
logger.debug('Getting chapter text from: %s' % url)
soup = bs.BeautifulStoneSoup(self._fetchUrl(url),
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
soup = bs.BeautifulSoup(self._fetchUrl(url))
div = soup.find('div', {'id' : 'story'})
@@ -237,6 +237,9 @@ class DramioneOrgAdapter(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:
@@ -282,6 +285,14 @@ class DramioneOrgAdapter(BaseSiteAdapter):
# I find it hard to care if the series parsing fails
pass
try:
self.story.setMetadata('reviews',
stripHTML(soup.find('h2',{'id':'pagetitle'}).
findAll('a', href=re.compile(r'^reviews.php'))[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):
@@ -132,14 +132,28 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
## For 1, use the second link.
## For 2, fetch the crossover page and pull the two categories from there.
categories = soup.findAll('a',{'class':'xcontrast_txt'})
categories = soup.find('div',{'id':'pre_story_links'}).findAll('a',{'class':'xcontrast_txt'})
#print("xcontrast_txt a:%s"%categories)
if len(categories) > 1:
self.story.addToList('category',stripHTML(categories[-1]))
# Strangely, the ones with *two* links are the
# non-crossover categories. Each is in a category itself
# of Book, Movie, etc.
self.story.addToList('category',stripHTML(categories[1]))
elif 'Crossover' in categories[0]['href']:
caturl = "http://%s%s"%(self.getSiteDomain(),categories[0]['href'])
catsoup = bs.BeautifulSoup(self._fetchUrl(caturl))
for a in catsoup.findAll('a',href=re.compile(r"^/crossovers/")):
self.story.addToList('category',stripHTML(a))
else:
# Fall back. I ran across a story with a Crossver
# category link to a broken page once.
# http://www.fanfiction.net/s/2622060/1/
# Naruto + Harry Potter Crossover
logger.info("Fall back category collection")
for c in stripHTML(categories[0]).replace(" Crossover","").split(' + '):
self.story.addToList('category',c)
a = soup.find('a', href='http://www.fictionratings.com/')
rating = a.string
@@ -150,7 +164,7 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
# after Rating, the same bit of text containing id:123456 contains
# Complete--if completed.
gui_table1i = soup.find(id="gui_table1i")
gui_table1i = soup.find('table',{'cellpadding':'5'})
self.story.setMetadata('title', stripHTML(gui_table1i.find('b'))) # title appears to be only(or at least first) bold tag in gui_table1i
@@ -159,15 +173,16 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
self.setDescription(url,stripHTML(summarydiv))
graydiv = gui_table1i.find('div', {'class':'xgray'})
for b in graydiv.findAll('button'):
b.extract()
metatext = stripHTML(graydiv).replace('Hurt/Comfort','Hurt-Comfort')
grayspan = gui_table1i.find('span', {'class':'xgray xcontrast_txt'})
# for b in grayspan.findAll('button'):
# b.extract()
metatext = stripHTML(grayspan).replace('Hurt/Comfort','Hurt-Comfort')
#logger.debug("metatext:(%s)"%metatext)
metalist = metatext.split(" - ")
#logger.debug("metalist:(%s)"%metalist)
# Rated: Fiction K - English - Words: 158,078 - Published: 02-04-11
# Rated: Fiction T - English - Adventure/Sci-Fi - Naruto U. - Chapters: 22 - Words: 114,414 - Reviews: 395 - Favs: 779 - Follows: 835 - Updated: 03-21-13 - Published: 04-28-12 - id: 8067258
# rating is obtained above more robustly.
if metalist[0].startswith('Rated:'):
@@ -257,12 +272,18 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
## slow_down_sleep_time setting is.
data = self._fetchUrl(url)
if "Please email this error message in full to <a href='mailto:support@fanfiction.com'>support@fanfiction.com</a>" in data:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! FanFiction.net Site Error!" % url)
# some ancient stories have body tags inside them that cause
# soup parsing to discard the content. For story text we
# don't care about anything before "<div class='storytextp"
# (there's a space after storytextp, so no close quote(')) and
# this kills any body tags.
data = data[data.index("<div class='storytextp"):]
if "<div class='storytextp" not in data:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
else:
data = data[data.index("<div class='storytextp"):]
data.replace("<body","<notbody").replace("<BODY","<NOTBODY")
soup = bs.BeautifulSoup(data)
@@ -227,6 +227,9 @@ 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:
@@ -278,7 +281,14 @@ class GrangerEnchantedCom(BaseSiteAdapter):
self.story.setMetadata('seriesUrl',series_url)
break
i+=1
except:
# I find it hard to care if the series parsing fails
pass
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
@@ -133,7 +133,7 @@ class LibraryOfMoriaComAdapter(BaseSiteAdapter):
# 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('authorUrl','http://'+self.host+'/a/'+a['href'])
self.story.setMetadata('author',a.string)
# Find the chapters:
@@ -66,14 +66,18 @@ class MuggleNetComAdapter(BaseSiteAdapter): # XXX
@staticmethod # must be @staticmethod, don't remove it.
def getSiteDomain():
# The site domain. Does have www here, if it uses it.
return 'fanfiction.mugglenet.com' # XXX
# The site domain.
return 'fanfiction.mugglenet.com'
@classmethod
def getAcceptDomains(cls):
return ['fanfiction.mugglenet.com','fanfic.mugglenet.com']
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
def getSiteURLPattern(self):
return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$"
return re.escape("http://")+r"fanfic(tion)?\.mugglenet\.com"+re.escape("/viewstory.php?sid=")+r"\d+$"
## Login seems to be reasonably standard across eFiction sites.
def needToLoginCheck(self, data):
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2012 Fanficdownloader team
# Copyright 2013 Fanficdownloader team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -28,11 +28,9 @@ from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, makeDate
def getClass():
return PrisonBreakFicNetAdapter
return NetRaptorOrgAdapter
# Class name has to be unique. Our convention is camel case the
# sitename with Adapter at the end. www is skipped.
class PrisonBreakFicNetAdapter(BaseSiteAdapter):
class NetRaptorOrgAdapter(BaseSiteAdapter):
def __init__(self, config, url):
BaseSiteAdapter.__init__(self, config, url)
@@ -51,31 +49,29 @@ class PrisonBreakFicNetAdapter(BaseSiteAdapter):
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
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','pbf')
self.story.setMetadata('siteabbrev','netrap')
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%B %d, %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 'www.prisonbreakfic.net'
return 'netraptor.org'
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
return "http://"+self.getSiteDomain()+"/fanfiction/viewstory.php?sid=1234"
def getSiteURLPattern(self):
return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$"
return re.escape("http://"+self.getSiteDomain()+"/fanfiction/viewstory.php?sid=")+r"\d+$"
## Getting the chapter list and the meta data, plus 'is adult' checking.
def extractChapterUrlsAndMetadata(self):
# index=1 makes sure we see the story chapter index. Some
# sites skip that for one-chapter stories.
url = self.url+'&index=1'
logger.debug("URL: "+url)
@@ -86,6 +82,10 @@ class PrisonBreakFicNetAdapter(BaseSiteAdapter):
raise exceptions.StoryDoesNotExist(self.url)
else:
raise e
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
# use BeautifulSoup HTML parser to make everything easier to find.
soup = bs.BeautifulSoup(data)
@@ -94,19 +94,20 @@ class PrisonBreakFicNetAdapter(BaseSiteAdapter):
# 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')+"$"))
pagetitle = soup.find('div',{'id':'pagetitle'})
a = pagetitle.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
# Find authorid and URL from... author url.
a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
a = pagetitle.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
self.story.setMetadata('authorId',a['href'].split('=')[1])
self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href'])
self.story.setMetadata('authorUrl','http://'+self.host+'/fanfiction/'+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']))
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/fanfiction/'+chapter['href']))
self.story.setMetadata('numChapters',len(self.chapterUrls))
@@ -119,10 +120,7 @@ class PrisonBreakFicNetAdapter(BaseSiteAdapter):
return d[k]
except:
return ""
# summary, rated, word count, categories, characters, genre, warnings, completed, published, updated, seires
# <span class="label">Rated:</span> NC-17<br /> etc
labels = soup.findAll('span',{'class':'label'})
for labelspan in labels:
value = labelspan.nextSibling
@@ -145,21 +143,18 @@ class PrisonBreakFicNetAdapter(BaseSiteAdapter):
if 'Categories' in label:
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
for cat in cats:
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'))
for char in chars:
charstext = [char.string for char in chars]
for char in charstext:
self.story.addToList('characters',char.string)
if 'Pairing' in label:
ships = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=3')) # XXX
for ship in ships:
self.story.addToList('ships',ship.string.replace(" and ","/"))
if 'Genre' in label:
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) # XXX
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)
@@ -184,7 +179,7 @@ class PrisonBreakFicNetAdapter(BaseSiteAdapter):
# Find Series name from series URL.
a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+"))
series_name = a.string
series_url = 'http://'+self.host+'/'+a['href']
series_url = 'http://'+self.host+'/fanfiction/'+a['href']
# use BeautifulSoup HTML parser to make everything easier to find.
seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url))
@@ -206,8 +201,7 @@ class PrisonBreakFicNetAdapter(BaseSiteAdapter):
logger.debug('Getting chapter text from: %s' % url)
soup = bs.BeautifulStoneSoup(self._fetchUrl(url),
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
soup = bs.BeautifulSoup(self._fetchUrl(url))
div = soup.find('div', {'id' : 'story'})
@@ -112,7 +112,8 @@ class NHAMagicalWorldsUsAdapter(BaseSiteAdapter):
for info in asoup.findAll('table', {'width' : '100%', 'bordercolor' : re.compile(r'#')}):
a = info.find('a')
if ('viewstory.php?sid='+self.story.getMetadata('storyId')) in a['href']:
if 'viewstory.php?sid='+self.story.getMetadata('storyId') == a['href'] or \
('viewstory.php?sid='+self.story.getMetadata('storyId')+'&') in a['href']:
self.story.setMetadata('title',a.string)
break
@@ -142,14 +143,14 @@ class NHAMagicalWorldsUsAdapter(BaseSiteAdapter):
for cat in cats:
self.story.addToList('category',cat.string)
a = info.find('a', href=re.compile(r'reviews.php\?sid='+self.story.getMetadata('storyId')))
a = info.find('a', href=re.compile(r'viewuser.php'))
val = a.nextSibling
svalue = ""
while not defaultGetattr(val) == 'br':
val = val.nextSibling
val = val.nextSibling
while not defaultGetattr(val) == 'br':
svalue += str(val)
svalue += unicode(val)
val = val.nextSibling
self.setDescription(url,svalue)
@@ -0,0 +1,174 @@
# -*- coding: utf-8 -*-
# Copyright 2013 Fanficdownloader team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import time
import logging
logger = logging.getLogger(__name__)
import re
import urllib2
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, makeDate
def getClass():
return 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])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
# XXX Most sites don't have the /fanfic part. Replace all to remove it usually.
self._setURL('http://' + self.getSiteDomain() + '/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.nickandgreg.net'
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/desert_archive/viewstory.php?sid=1234"
def getSiteURLPattern(self):
return re.escape("http://"+self.getSiteDomain()+"/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 = bs.BeautifulSoup(data)
# print data
# Now go hunting for all the meta data and the chapter list.
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
# Find authorid and URL from... author url.
a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
self.story.setMetadata('authorId',a['href'].split('=')[1])
self.story.setMetadata('authorUrl','http://'+self.host+'/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 = bs.BeautifulSoup(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 = bs.BeautifulStoneSoup(self._fetchUrl(url),
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
div = soup.find('table', {'class' : 'tblborder6'})
if None == div:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
return self.utf8FromSoup(url,div)
@@ -112,6 +112,8 @@ class PhoenixSongNetAdapter(BaseSiteAdapter):
logger.debug("URL: "+url)
try:
if self.getConfig('force_login'):
self.performLogin(url)
data = self._fetchUrl(url)
except urllib2.HTTPError, e:
if e.code == 404:
@@ -123,7 +125,6 @@ class PhoenixSongNetAdapter(BaseSiteAdapter):
# need to log in for this one.
self.performLogin(url)
data = self._fetchUrl(url)
# use BeautifulSoup HTML parser to make everything easier to find.
soup = bs.BeautifulSoup(data)
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2011 Fanficdownloader team
# Copyright 2013 Fanficdownloader team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -27,12 +27,12 @@ from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, makeDate
def getClass():
return JLAUnlimitedComAdapter
return PotterHeadsAnonymousComAdapter
class JLAUnlimitedComAdapter(BaseSiteAdapter):
# Class name has to be unique. Our convention is camel case the
# sitename with Adapter at the end. www is skipped.
class PotterHeadsAnonymousComAdapter(BaseSiteAdapter):
def __init__(self, config, url):
BaseSiteAdapter.__init__(self, config, url)
@@ -42,36 +42,69 @@ class JLAUnlimitedComAdapter(BaseSiteAdapter):
# Most sites that claim to be
# iso-8859-1 (and some that claim to be
# utf8) are really windows-1252.
self.username = "" # if left empty, site doesn't return any message at all.
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
self.password = ""
self.is_adult=False
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
self._setURL('http://' + self.getSiteDomain() + '/eFiction1.1/viewstory.php?sid='+self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','jla')
self.story.setMetadata('siteabbrev','pha')
# 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 = "%d %b %Y"
@staticmethod # must be @staticmethod, don't remove it.
def getSiteDomain():
# The site domain. Does have www here, if it uses it.
return 'www.jlaunlimited.com'
return 'fanfic.potterheadsanonymous.com'
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/eFiction1.1/viewstory.php?sid=1234"
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
def getSiteURLPattern(self):
return re.escape("http://"+self.getSiteDomain()+"/eFiction1.1/viewstory.php?sid=")+r"\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):
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):
@@ -81,7 +114,7 @@ class JLAUnlimitedComAdapter(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=4" # XXX
addurl = "&ageconsent=ok&warning=4"
else:
addurl=""
@@ -98,8 +131,21 @@ class JLAUnlimitedComAdapter(BaseSiteAdapter):
else:
raise e
# Assume that if there is a url with 'warning=#' in the page then it is a 'check' page
m = re.search(r"'viewstory.php\?sid=\d+((?:&amp;ageconsent=ok)?&amp;warning=\d+)",data)
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
# &amp;warning= -- actually, so do other sites. Must be an
# eFiction book.
# viewstory.php?sid=1882&amp;warning=4
# viewstory.php?sid=1654&amp;ageconsent=ok&amp;warning=5
#print data
#m = re.search(r"'viewstory.php\?sid=1882(&amp;warning=4)'",data)
m = re.search(r"'viewstory.php\?sid=\d+((?:&amp;ageconsent=ok)?&amp;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
@@ -120,27 +166,32 @@ class JLAUnlimitedComAdapter(BaseSiteAdapter):
raise e
else:
raise exceptions.AdultCheckRequired(self.url)
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
# use BeautifulSoup HTML parser to make everything easier to find.
soup = bs.BeautifulSoup(data)
# print data
# Now go hunting for all the meta data and the chapter list.
pagetitle = soup.find('div',{'id':'pagetitle'})
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')))
a = pagetitle.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
# Find authorid and URL from... author url.
a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
a = pagetitle.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
self.story.setMetadata('authorId',a['href'].split('=')[1])
self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href'])
self.story.setMetadata('author',a.string)
# Find the chapters:
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+")):
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")):
# just in case there's tags, like <i> in chapter titles.
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/eFiction1.1/'+chapter['href']+addurl))
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href']+addurl))
self.story.setMetadata('numChapters',len(self.chapterUrls))
@@ -177,34 +228,22 @@ class JLAUnlimitedComAdapter(BaseSiteAdapter):
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:
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'))
charstext = [char.string for char in chars]
for char in charstext:
for char in chars:
self.story.addToList('characters',char.string)
## Not all sites use Genre, but there's no harm to
## leaving it in. Check to make sure the type_id number
## is correct, though--it's site specific.
if 'Genre' in label:
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) # XXX
genrestext = [genre.string for genre in genres]
self.genre = ', '.join(genrestext)
for genre in genrestext:
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)
## 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:
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:
@@ -225,7 +264,7 @@ class JLAUnlimitedComAdapter(BaseSiteAdapter):
# Find Series name from series URL.
a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+"))
series_name = a.string
series_url = 'http://'+self.host+'/fanfic/'+a['href']
series_url = 'http://'+self.host+'/'+a['href']
# use BeautifulSoup HTML parser to make everything easier to find.
seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url))
@@ -241,20 +280,18 @@ class JLAUnlimitedComAdapter(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):
logger.debug('Getting chapter text from: %s' % url)
soup = bs.BeautifulSoup(self._fetchUrl(url),
soup = bs.BeautifulStoneSoup(self._fetchUrl(url),
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
div = soup.find('div', {'id' : 'story'})
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)
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2012 Fanficdownloader team
# Copyright 2013 Fanficdownloader team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -28,11 +28,11 @@ from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, makeDate
def getClass():
return SVUFictionComAdapter
return ScarHeadNetAdapter
# Class name has to be unique. Our convention is camel case the
# sitename with Adapter at the end. www is skipped.
class SVUFictionComAdapter(BaseSiteAdapter):
class ScarHeadNetAdapter(BaseSiteAdapter):
def __init__(self, config, url):
BaseSiteAdapter.__init__(self, config, url)
@@ -54,16 +54,16 @@ class SVUFictionComAdapter(BaseSiteAdapter):
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','svuf')
self.story.setMetadata('siteabbrev','shn')
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%b %d, %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 'svufiction.com'
return 'scarhead.net'
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -108,13 +108,13 @@ class SVUFictionComAdapter(BaseSiteAdapter):
## 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=6"
addurl = "&ageconsent=ok&warning=5"
else:
addurl=""
@@ -130,12 +130,21 @@ class SVUFictionComAdapter(BaseSiteAdapter):
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
# &amp;warning= -- actually, so do other sites. Must be an
# eFiction book.
# viewstory.php?sid=1882&amp;warning=4
# viewstory.php?sid=1654&amp;ageconsent=ok&amp;warning=5
#print data
#m = re.search(r"'viewstory.php\?sid=1882(&amp;warning=4)'",data)
m = re.search(r"'viewstory.php\?sid=\d+((?:&amp;ageconsent=ok)?&amp;warning=\d+)'",data)
if m != None:
if self.is_adult or self.getConfig("is_adult"):
@@ -166,24 +175,19 @@ class SVUFictionComAdapter(BaseSiteAdapter):
# print data
# Now go hunting for all the meta data and the chapter list.
pagetitle = soup.find('tr',{'valign':'top'})
## Title
pt = soup.find('div', {'class' : 'title'})
a = pt.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
if a.text != "":
self.story.setMetadata('title',a.text)
else:
self.story.setMetadata('title','Banner')
a = pagetitle.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
# Find authorid and URL from... author url.
a = pt.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)
rating=pt.text.split('[')[1].split(']')[0]
self.story.setMetadata('rating', rating)
# Find the chapters:
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")):
# just in case there's tags, like <i> in chapter titles.
@@ -193,46 +197,70 @@ class SVUFictionComAdapter(BaseSiteAdapter):
# 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 ""
bottom=soup.find('div', {'class' : 'bottom'}).text
self.story.setMetadata('numWords', bottom.split('Word count: ')[1].split(' Read:')[0])
self.story.setMetadata('datePublished', makeDate(bottom.split('Published: ')[1].split(' Updated:')[0], self.dateformat))
self.story.setMetadata('dateUpdated', makeDate(bottom.split('Updated: ')[1], self.dateformat))
content=soup.find('div', {'class' : 'content'})
value=content.find('h1').nextSibling
svalue = ""
while not defaultGetattr(value,'class') == 'smaller':
svalue += str(value)
value = value.nextSibling
self.setDescription(url,svalue)
status = content.text.split('Completed: ')[1]
if 'Yes' in status:
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)
cats = content.findAll('a',href=re.compile(r'browse.php\?type=categories'))
for cat in cats:
self.story.addToList('category',cat.string)
chars = content.findAll('a',href=re.compile(r'browse.php\?type=characters'))
for char in chars:
self.story.addToList('characters',char.string)
genres = content.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1'))
for genre in genres:
self.story.addToList('genre',genre.string)
a=soup.find(text=re.compile("Summary: "))
i=0
svalue = ""
while i == 0:
try:
b = str(a)
svalue += b.split('Summary: ')[1]
except:
svalue += str(a)
if a.nextSibling != None:
a = a.nextSibling
else:
a = a.parent.nextSibling
if 'Disclaimer: ' in stripHTML(a):
i=1
self.setDescription(url,svalue)
try:
# Find Series name from series URL.
a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+"))
@@ -241,17 +269,14 @@ class SVUFictionComAdapter(BaseSiteAdapter):
# use BeautifulSoup HTML parser to make everything easier to find.
seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url))
# can't use ^viewstory...$ in case of higher rated stories with javascript href.
storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+'))
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
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
@@ -262,7 +287,7 @@ class SVUFictionComAdapter(BaseSiteAdapter):
logger.debug('Getting chapter text from: %s' % url)
soup = bs.BeautifulStoneSoup(self._fetchUrl(url),
soup = bs.BeautifulSoup(self._fetchUrl(url),
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
div = soup.find('div', {'id' : 'story'})
@@ -141,7 +141,7 @@ class ScarvesAndCoffeeNetAdapter(BaseSiteAdapter):
self.story.setMetadata('title',a.string)
# Find authorid and URL from... author url.
a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
a = soup.find('div',{"id":"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)
@@ -0,0 +1,217 @@
# -*- coding: utf-8 -*-
# Copyright 2013 Fanficdownloader team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import time
import logging
logger = logging.getLogger(__name__)
import re
import urllib2
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, makeDate
def getClass():
return SimplyUndeniableComAdapter
# Class name has to be unique. Our convention is camel case the
# sitename with Adapter at the end. www is skipped.
class SimplyUndeniableComAdapter(BaseSiteAdapter):
def __init__(self, config, url):
BaseSiteAdapter.__init__(self, config, url)
self.decode = ["Windows-1252",
"utf8"] # 1252 is a superset of iso-8859-1.
# Most sites that claim to be
# iso-8859-1 (and some that claim to be
# utf8) are really windows-1252.
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
self.password = ""
self.is_adult=False
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','sud')
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%m/%d/%Y"
@staticmethod # must be @staticmethod, don't remove it.
def getSiteDomain():
# The site domain. Does have www here, if it uses it.
return 'www.simplyundeniable.com'
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
def getSiteURLPattern(self):
return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$"
## Login seems to be reasonably standard across eFiction sites.
def needToLoginCheck(self, data):
if 'Please log in now' 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 "My 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):
# 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.")
if "Please log in now" in data:
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: You need to have access to the restricted section.")
# use BeautifulSoup HTML parser to make everything easier to find.
soup = bs.BeautifulSoup(data)
# print data
# Now go hunting for all the meta data and the chapter list.
## Title
a = soup.find('h1')
self.story.setMetadata('title',a.text)
# 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)
asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')))
for info in asoup.findAll('table', {'cellpadding' : '5'}):
a = info.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
if a != None:
self.story.setMetadata('title',a.string)
break
# Find the chapters:
if "Disclaimer" in data:
self.chapterUrls.append((self.story.getMetadata('title'),url))
else:
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+&i=1$')):
# just in case there's tags, like <i> in chapter titles.
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href']))
self.story.setMetadata('numChapters',len(self.chapterUrls))
# eFiction sites don't help us out a lot with their meta data
# formating, so it's a little ugly.
a = info.find('td', {'valign' : 'top'}).find('p')
self.setDescription(url,a)
a = info.find('td', {'class' : 'greysm'}).findAll('b')
self.story.setMetadata('datePublished', makeDate(stripHTML(a[0].nextSibling), self.dateformat))
self.story.setMetadata('dateUpdated', makeDate(stripHTML(a[1].nextSibling), self.dateformat))
if 'Yes' in a[2].nextSibling:
self.story.setMetadata('status', 'Completed')
else:
self.story.setMetadata('status', 'In-Progress')
self.story.setMetadata('rating', a[3].nextSibling)
self.story.setMetadata('numWords', a[4].nextSibling)
warnings = info.find('td', {'width' : '45'}).nextSibling.nextSibling.text.split(', ')
for warning in warnings:
if 'none' not in warning:
self.story.addToList('warnings',warning)
chars = info.find('td', {'width' : '51'}).nextSibling.nextSibling.text.split(', ')
for char in chars:
if '&' in char:
self.story.addToList('ships',char)
else:
self.story.addToList('characters',char)
genres = info.find('td', {'width' : '36'}).nextSibling.nextSibling.text.split(', ')
for genre in genres:
self.story.addToList('genre',genre)
cat = info.find('a', href=re.compile(r'categories.php'))
self.story.addToList('category',cat.string)
# grab the text for an individual chapter.
def getChapterText(self, url):
logger.debug('Getting chapter text from: %s' % url)
soup = bs.BeautifulStoneSoup(self._fetchUrl(url),
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
div = soup.find('span', {'class' : 'style'})
if None == div:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
return self.utf8FromSoup(url,div)
@@ -110,7 +110,7 @@ class SiyeCoUkAdapter(BaseSiteAdapter): # XXX
## Title
titlea = authsoup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',titlea.string)
self.story.setMetadata('title',stripHTML(titlea))
# Find the chapters (from soup, not authsoup):
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")):
+50 -19
View File
@@ -47,31 +47,63 @@ class TestSiteAdapter(BaseSiteAdapter):
return BaseSiteAdapter.getSiteURLPattern(self)+r'/?\?sid=\d+$'
def extractChapterUrlsAndMetadata(self):
idstr = self.story.getMetadata('storyId')
idnum = int(idstr)
if self.story.getMetadata('storyId') == '665' and not (self.is_adult or self.getConfig("is_adult")):
if idnum >= 1000:
logger.warn("storyId:%s - Custom INI data will be used."%idstr)
sections = ['teststory:%s'%idstr,'teststory:defaults']
#print("self.get_config_list(sections,'valid_entries'):%s"%self.get_config_list(sections,'valid_entries'))
for key in self.get_config_list(sections,'valid_entries'):
if key.endswith("_list"):
nkey = key[:-len("_list")]
#print("addList:%s"%(nkey))
for val in self.get_config_list(sections,key):
#print("addList:%s->%s"%(nkey,val))
self.story.addToList(nkey,val.decode('utf-8').replace('{{storyId}}',idstr))
else:
# Special cases:
if key in ['datePublished','dateUpdated']:
self.story.setMetadata(key,makeDate(self.get_config(sections,key),"%Y-%m-%d"))
else:
self.story.setMetadata(key,self.get_config(sections,key).decode('utf-8').replace('{{storyId}}',idstr))
#print("set:%s->%s"%(key,self.story.getMetadata(key)))
self.chapterUrls = []
for (j,chap) in enumerate(self.get_config_list(sections,'chaptertitles'),start=1):
self.chapterUrls.append( (chap,self.url+"&chapter=%d"%j) )
# self.chapterUrls = [(u'Prologue '+self.crazystring,self.url+"&chapter=1"),
# ('Chapter 1, Xenos on Cinnabar',self.url+"&chapter=2"),
# ]
self.story.setMetadata('numChapters',len(self.chapterUrls))
return
if idstr == '665' and not (self.is_adult or self.getConfig("is_adult")):
logger.warn("self.is_adult:%s"%self.is_adult)
raise exceptions.AdultCheckRequired(self.url)
if self.story.getMetadata('storyId') == '666':
if idstr == '666':
raise exceptions.StoryDoesNotExist(self.url)
if self.story.getMetadata('storyId').startswith('670'):
if idstr.startswith('670'):
time.sleep(1.0)
if self.story.getMetadata('storyId').startswith('671'):
if idstr.startswith('671'):
time.sleep(1.0)
if self.getConfig("username"):
self.username = self.getConfig("username")
if self.story.getMetadata('storyId') == '668' and self.username != "Me" :
if idstr == '668' and self.username != "Me" :
raise exceptions.FailedToLogin(self.url,self.username)
if self.story.getMetadata('storyId') == '664':
self.story.setMetadata(u'title',"Test Story Title "+self.story.getMetadata('storyId')+self.crazystring)
if idstr == '664':
self.story.setMetadata(u'title',"Test Story Title "+idstr+self.crazystring)
self.story.setMetadata('author','Test Author aa bare amp(&) quote(&#39;) amp(&amp;)')
else:
self.story.setMetadata(u'title',"Test Story Title "+self.story.getMetadata('storyId'))
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
@@ -79,13 +111,12 @@ class TestSiteAdapter(BaseSiteAdapter):
Some more longer description. "I suck at summaries!" "Better than it sounds!" "My first fic"
''')
self.story.setMetadata('datePublished',makeDate("1975-03-15","%Y-%m-%d"))
if self.story.getMetadata('storyId') == '669':
if idstr == '669':
self.story.setMetadata('dateUpdated',datetime.datetime.now())
else:
self.story.setMetadata('dateUpdated',makeDate("1975-04-15","%Y-%m-%d"))
self.story.setMetadata('numWords','123456')
idnum = int(self.story.getMetadata('storyId'))
if idnum % 2 == 1:
self.story.setMetadata('status','In-Progress')
else:
@@ -108,7 +139,7 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
self.story.setMetadata('rating','Tweenie')
if self.story.getMetadata('storyId') == '673':
if idstr == '673':
self.story.addToList('author','Author From List')
self.story.addToList('author','Author From List 2')
@@ -128,18 +159,19 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
self.story.addToList('warnings','Swearing')
self.story.addToList('warnings','Violence')
if self.story.getMetadata('storyId') == '80':
if idstr == '80':
self.story.addToList('category',u'Rizzoli & Isles')
self.story.addToList('characters','J. Rizzoli')
elif self.story.getMetadata('storyId') == '81':
elif idstr == '81':
self.story.addToList('category',u'Pitch Perfect')
self.story.addToList('characters','Chloe B.')
elif self.story.getMetadata('storyId') == '83':
elif idstr == '83':
self.story.addToList('category',u'Rizzoli & Isles')
self.story.addToList('characters','J. Rizzoli')
self.story.addToList('category',u'Pitch Perfect')
self.story.addToList('characters','Chloe B.')
elif self.story.getMetadata('storyId') == '82':
self.story.addToList('ships','Chloe B. &amp; J. Rizzoli')
elif idstr == '82':
self.story.addToList('characters','Henry (Once Upon a Time)')
self.story.addToList('category',u'Once Upon a Time (TV)')
else:
@@ -217,6 +249,9 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
<div>
<h3>Prologue</h3>
<p>This is a fake adapter for testing purposes. Different sid's will give different errors:</p>
<h4>Config(personal.ini)</h4>
<p>sid&gt;=1000 will use custom test story data from your configuration(personal.ini)</p>
<p>Hard coded ids:</p>
<p>http://test1.com?sid=664 - Crazy string title</p>
<p>http://test1.com?sid=665 - raises AdultCheckRequired</p>
<p>http://test1.com?sid=666 - raises StoryDoesNotExist</p>
@@ -224,10 +259,6 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
<p>http://test1.com?sid=668 - raises FailedToLogin unless username='Me'</p>
<p>http://test1.com?sid=669 - Succeeds with Updated Date=now</p>
<p>http://test1.com?sid=670 - Succeeds, but sleeps 2sec on each chapter</p>
<p>http://test1.com?sid=671 - Succeeds, but sleeps 2sec metadata only</p>
<p>http://test1.com?sid=672 - Succeeds, quick meta, sleeps 2sec chapters only</p>
<p>http://test1.com?sid=673 - Succeeds, multiple authors, extra categories, genres</p>
@@ -183,7 +183,7 @@ class TheHookupZoneNetAdapter(BaseSiteAdapter): # XXX
# 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('authorUrl','http://'+self.host+'/CriminalMinds/'+a['href'])
self.story.setMetadata('author',a.string)
# Find the chapters:
@@ -244,16 +244,23 @@ class TheWritersCoffeeShopComSiteAdapter(BaseSiteAdapter):
# issues with different SGML parsers in python. This is a
# nasty hack, but it works.
data = data[data.index("<body"):]
soup = bs.BeautifulStoneSoup(data,
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
span = soup.find('div', {'id' : 'story'})
if None == span:
chapter=bs.BeautifulSoup('<div class="story"></div>')
soup = bs.BeautifulSoup(data)
found=False
for div in soup.findAll('div'):
if div.has_key('class') and div['class'] == 'notes':
chapter.append(div)
if div.has_key('id') and div['id'] == 'story':
chapter.append(div)
found=True
if not found:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
return self.utf8FromSoup(url,span)
return self.utf8FromSoup(url,chapter)
def getClass():
return TheWritersCoffeeShopComSiteAdapter
@@ -0,0 +1,236 @@
# -*- coding: utf-8 -*-
# Copyright 2013 Fanficdownloader team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import time
import logging
logger = logging.getLogger(__name__)
import re
import urllib2
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, makeDate
def getClass():
return 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])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','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'
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
def getSiteURLPattern(self):
return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$"
## 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+((?:&amp;ageconsent=ok)?&amp;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 &amp; error in url.
addurl = addurl.replace("&amp;","&")
url = self.url+'&index=1'+addurl
logger.debug("URL 2nd try: "+url)
try:
data = self._fetchUrl(url)
except urllib2.HTTPError, e:
if e.code == 404:
raise exceptions.StoryDoesNotExist(self.url)
else:
raise e
else:
raise exceptions.AdultCheckRequired(self.url)
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
# use BeautifulSoup HTML parser to make everything easier to find.
soup = bs.BeautifulSoup(data)
#print data
# Now go hunting for all the meta data and the chapter list.
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
# Find authorid and URL from... author url.
a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
self.story.setMetadata('authorId',a['href'].split('=')[1])
self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href'])
self.story.setMetadata('author',a.string)
# 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 = bs.BeautifulSoup(self._fetchUrl(series_url))
# can't use ^viewstory...$ in case of higher rated stories with javascript href.
storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+'))
i=1
for a in storyas:
# skip 'report this' and 'TOC' links
if 'contact.php' not in a['href'] and 'index' not in a['href']:
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
self.setSeries(series_name, i)
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 = bs.BeautifulSoup(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)
@@ -154,26 +154,6 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
self.story.setMetadata('author',stripHTML(a))
authorurl = 'http://'+self.host+a['href']
ainfo = soup.find('a', href='/StoryInfo-%s-1'%self.story.getMetadata('storyId'))
if ainfo != None: # indicates multiple authors/contributors.
try:
# going to pull part of the meta data from author list page.
infourl = 'http://'+self.host+ainfo['href']
logger.debug("**StoryInfo** URL: "+infourl)
infodata = self._fetchUrl(infourl)
infosoup = bs.BeautifulSoup(infodata)
for a in infosoup.findAll('a',href=re.compile(r"^/Author-\d+")):
self.story.addToList('authorId',a['href'].split('/')[1].split('-')[1])
self.story.addToList('authorUrl','http://'+self.host+a['href'].replace("/Author-","/AuthorStories-"))
self.story.addToList('author',stripHTML(a))
except urllib2.HTTPError, e:
if e.code == 404:
raise exceptions.StoryDoesNotExist(url)
else:
raise e
try:
# going to pull part of the meta data from *primary* author list page.
logger.debug("**AUTHOR** URL: "+authorurl)
@@ -198,6 +178,53 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
#self.story.setMetadata('description',stripHTML(storydiv.find('div',{'class':'storydesc'})))
self.story.setMetadata('title',stripHTML(storydiv.find('a',{'class':'storylink'})))
ainfo = soup.find('a', href='/StoryInfo-%s-1'%self.story.getMetadata('storyId'))
if ainfo != None: # indicates multiple authors/contributors.
try:
# going to pull part of the meta data from author list page.
infourl = 'http://'+self.host+ainfo['href']
logger.debug("**StoryInfo** URL: "+infourl)
infodata = self._fetchUrl(infourl)
infosoup = bs.BeautifulSoup(infodata)
# for a in infosoup.findAll('a',href=re.compile(r"^/Author-\d+")):
# self.story.addToList('authorId',a['href'].split('/')[1].split('-')[1])
# self.story.addToList('authorUrl','http://'+self.host+a['href'].replace("/Author-","/AuthorStories-"))
# self.story.addToList('author',stripHTML(a))
# second verticaltable is the chapter list.
table = infosoup.findAll('table',{'class':'verticaltable'})[1]
for a in table.findAll('a',href=re.compile(r"^/Story-"+self.story.getMetadata('storyId'))):
autha = a.findNext('a',href=re.compile(r"^/Author-\d+"))
self.story.addToList('authorId',autha['href'].split('/')[1].split('-')[1])
self.story.addToList('authorUrl','http://'+self.host+autha['href'].replace("/Author-","/AuthorStories-"))
self.story.addToList('author',stripHTML(autha))
# include leading number to match 1. ... 2. ...
self.chapterUrls.append(("%d. %s by %s"%(len(self.chapterUrls)+1,
stripHTML(a),
stripHTML(autha)),'http://'+self.host+a['href']))
except urllib2.HTTPError, e:
if e.code == 404:
raise exceptions.StoryDoesNotExist(url)
else:
raise e
else: # single author:
# Find the chapter selector
select = soup.find('select', { 'name' : 'chapnav' } )
if select is None:
# no selector found, so it's a one-chapter story.
self.chapterUrls.append((self.story.getMetadata('title'),url))
else:
allOptions = select.findAll('option')
for o in allOptions:
url = "http://"+self.host+o['value']
# just in case there's tags, like <i> in chapter titles.
self.chapterUrls.append((stripHTML(o),url))
self.story.setMetadata('numChapters',len(self.chapterUrls))
verticaltable = soup.find('table', {'class':'verticaltable'})
BtVS = True
@@ -235,21 +262,6 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
if BtVS:
self.story.addToList('category','Buffy: The Vampire Slayer')
# Find the chapter selector
select = soup.find('select', { 'name' : 'chapnav' } )
if select is None:
# no selector found, so it's a one-chapter story.
self.chapterUrls.append((self.story.getMetadata('title'),url))
else:
allOptions = select.findAll('option')
for o in allOptions:
url = "http://"+self.host+o['value']
# just in case there's tags, like <i> in chapter titles.
self.chapterUrls.append((stripHTML(o),url))
self.story.setMetadata('numChapters',len(self.chapterUrls))
pseries = soup.find('p', {'style':'margin-top:0px'})
m = re.match('This story is No\. (?P<num>\d+) in the series &quot;(?P<series>.+)&quot;\.',
pseries.text)
@@ -119,7 +119,7 @@ class WalkingThePlankOrgAdapter(BaseSiteAdapter):
# 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('authorUrl','http://'+self.host+'/archive/'+a['href'])
self.story.setMetadata('author',a.string)
# Find the chapters:
@@ -126,8 +126,13 @@ class WraithBaitComAdapter(BaseSiteAdapter):
# Find the chapters:
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")):
# include author on chapters if multiple authors.
if len(alist) > 1:
add = " by %s"%stripHTML(chapter.findNext('a', href=re.compile(r"viewuser.php\?uid=\d+")))
else:
add = ""
# 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)+add,'http://'+self.host+'/'+chapter['href']+addurl))
self.story.setMetadata('numChapters',len(self.chapterUrls))
+6 -2
View File
@@ -197,7 +197,8 @@ class BaseSiteAdapter(Configurable):
for index, (title,url) in enumerate(self.chapterUrls):
if (self.chapterFirst!=None and index < self.chapterFirst) or \
(self.chapterLast!=None and index > self.chapterLast):
self.story.addChapter(removeEntities(title),
self.story.addChapter(url,
removeEntities(title),
None)
else:
if self.oldchapters and index < len(self.oldchapters):
@@ -206,7 +207,8 @@ class BaseSiteAdapter(Configurable):
partial(cachedfetch,self._fetchUrlRaw,self.oldimgs))
else:
data = self.getChapterText(url)
self.story.addChapter(removeEntities(title),
self.story.addChapter(url,
removeEntities(title),
removeEntities(data))
self.storyDone = True
@@ -313,6 +315,8 @@ class BaseSiteAdapter(Configurable):
fetch=self._fetchUrlRaw
acceptable_attributes = ['href','name','class','id']
if self.getConfig("keep_style_attr"):
acceptable_attributes.append('style')
#print("include_images:"+self.getConfig('include_images'))
if self.getConfig('include_images'):
acceptable_attributes.extend(('src','alt','longdesc'))
+42 -12
View File
@@ -15,7 +15,7 @@
# limitations under the License.
#
import ConfigParser
import ConfigParser, re
# All of the writers(epub,html,txt) and adapters(ffnet,twlt,etc)
# inherit from Configurable. The config file(s) uses ini format:
@@ -37,10 +37,20 @@ class Configuration(ConfigParser.SafeConfigParser):
def __init__(self, site, fileform):
ConfigParser.SafeConfigParser.__init__(self)
self.sectionslist = ['defaults']
self.addConfigSection(site)
if site.startswith("www."):
sitewith = site
sitewithout = site.replace("www.","")
else:
sitewith = "www."+site
sitewithout = site
self.addConfigSection(sitewith)
self.addConfigSection(sitewithout)
if fileform:
self.addConfigSection(fileform)
self.addConfigSection(site+":"+fileform)
self.addConfigSection(sitewith+":"+fileform)
self.addConfigSection(sitewithout+":"+fileform)
self.addConfigSection("overrides")
self.validEntries = [
@@ -89,8 +99,12 @@ class Configuration(ConfigParser.SafeConfigParser):
def getValidMetaList(self):
return self.validEntries + self.getConfigList("extra_valid_entries")
# used by adapters & writers, non-convention naming style
def hasConfig(self, key):
for section in self.sectionslist:
return self.has_config(self.sectionslist, key)
def has_config(self, sections, key):
for section in sections:
try:
self.get(section,key)
#print("found %s in section [%s]"%(key,section))
@@ -104,10 +118,14 @@ class Configuration(ConfigParser.SafeConfigParser):
pass
return False
# used by adapters & writers, non-convention naming style
def getConfig(self, key, default=""):
return self.get_config(self.sectionslist,key,default)
def get_config(self, sections, key, default=""):
val = default
for section in self.sectionslist:
for section in sections:
try:
val = self.get(section,key)
if val and val.lower() == "false":
@@ -117,7 +135,7 @@ class Configuration(ConfigParser.SafeConfigParser):
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError), e:
pass
for section in self.sectionslist[::-1]:
for section in sections[::-1]:
# 'martian smiley' [::-1] reverses list by slicing whole list with -1 step.
try:
val = val + self.get(section,"add_to_"+key)
@@ -128,11 +146,15 @@ class Configuration(ConfigParser.SafeConfigParser):
return val
# split and strip each.
def getConfigList(self, key):
vlist = self.getConfig(key).split(',')
vlist = filter( lambda x : x !='', [ v.strip() for v in vlist ])
def get_config_list(self, sections, key):
vlist = re.split(r'(?<!\\),',self.get_config(sections,key)) # don't split on \,
vlist = filter( lambda x : x !='', [ v.strip().replace('\,',',') for v in vlist ])
#print "vlist("+key+"):"+str(vlist)
return vlist
# used by adapters & writers, non-convention naming style
def getConfigList(self, key):
return self.get_config_list(self.sectionslist, key)
# extended by adapter, writer and story for ease of calling configuration.
class Configurable(object):
@@ -147,11 +169,19 @@ class Configurable(object):
return self.configuration.getValidMetaList()
def hasConfig(self, key):
return self.configuration.hasConfig(key)
return self.configuration.hasConfig(key)
def has_config(self, sections, key):
return self.configuration.has_config(sections, key)
def getConfig(self, key, default=""):
return self.configuration.getConfig(key,default)
def get_config(self, sections, key, default=""):
return self.configuration.get_config(sections,key,default)
def getConfigList(self, key):
return self.configuration.getConfigList(key)
def get_config_list(self, sections, key):
return self.configuration.get_config_list(sections,key)
+3
View File
@@ -130,6 +130,9 @@ def get_update_data(inputio,
h2 = soup.find('h2')
if h2:
h2.extract()
for skip in soup.findAll(attrs={'class':'skip_on_ffdl_update'}):
skip.extract()
soups.append(soup)
+7
View File
@@ -68,3 +68,10 @@ class UnknownSite(Exception):
def __str__(self):
return "Unknown Site(%s). Supported sites: (%s)" % (self.url, ", ".join(self.supported_sites_list))
class FailedToWriteOutput(Exception):
def __init__(self,error):
self.error=error
def __str__(self):
return self.error
+26 -12
View File
@@ -25,7 +25,7 @@ from gziphttp import GZipProcessor
import adapters
from configurable import Configuration
def get_urls_from_page(url,configuration=None):
def get_urls_from_page(url,configuration=None,normalize=False):
if not configuration:
configuration = Configuration("test1.com","EPUB")
@@ -54,17 +54,25 @@ def get_urls_from_page(url,configuration=None):
opener = u2.build_opener(u2.HTTPCookieProcessor(),GZipProcessor())
data = opener.open(url).read()
return get_urls_from_html(data,url)
# kludge because I don't see it on enough sites to be worth generalizing yet.
restrictsearch=None
if 'scarvesandcoffee.net' in url:
restrictsearch=('div',{'id':'mainpage'})
def get_urls_from_html(data,url=None,configuration=None):
return get_urls_from_html(data,url,configuration,normalize,restrictsearch)
normalized = set() # normalized url
def get_urls_from_html(data,url=None,configuration=None,normalize=False,restrictsearch=None):
normalized = [] # normalized url
retlist = [] # orig urls.
if not configuration:
configuration = Configuration("test1.com","EPUB")
soup = BeautifulSoup(data)
if restrictsearch:
soup = soup.find(*restrictsearch)
print("restrict search:%s"%soup)
for a in soup.findAll('a'):
if a.has_key('href'):
@@ -82,16 +90,19 @@ def get_urls_from_html(data,url=None,configuration=None):
href = href.replace('&index=1','')
adapter = adapters.getAdapter(configuration,href)
if adapter.story.getMetadata('storyUrl') not in normalized:
normalized.add(adapter.story.getMetadata('storyUrl'))
normalized.append(adapter.story.getMetadata('storyUrl'))
retlist.append(href)
except:
pass
return retlist
if normalize:
return normalized
else:
return retlist
def get_urls_from_text(data,configuration=None):
def get_urls_from_text(data,configuration=None,normalize=False):
normalized = set() # normalized url
normalized = [] # normalized url
retlist = [] # orig urls.
if not configuration:
@@ -102,19 +113,22 @@ def get_urls_from_text(data,configuration=None):
# 'are you old enough' links, and 'Report This' links.
# The 'normalized' set prevents duplicates.
if 'story.php' in href:
m = re.search(r"(?P<sid>(view)?story\.php\?(sid|psid|no|story|stid)=\d+)",a['href'])
m = re.search(r"(?P<sid>(view)?story\.php\?(sid|psid|no|story|stid)=\d+)",href)
if m != None:
href = form_url(None,m.group('sid'))
href = form_url(href,m.group('sid'))
try:
href = href.replace('&index=1','')
adapter = adapters.getAdapter(configuration,href)
if adapter.story.getMetadata('storyUrl') not in normalized:
normalized.add(adapter.story.getMetadata('storyUrl'))
normalized.append(adapter.story.getMetadata('storyUrl'))
retlist.append(href)
except:
pass
return retlist
if normalize:
return normalized
else:
return retlist
def form_url(parenturl,url):
url = url.strip() # ran across an image with a space in the
+1 -1
View File
@@ -17,7 +17,7 @@ class HtmlProcessor:
self.unfill = unfill
html = self._ProcessRawHtml(html)
self._soup = BeautifulSoup(html)
if self._soup.title:
if self._soup.title.contents:
self.title = self._soup.title.contents[0]
else:
self.title = None
+4 -2
View File
@@ -8,6 +8,8 @@ import time
import random
import logging
logger = logging.getLogger(__name__)
from html import HtmlProcessor
# http://wiki.mobileread.com/wiki/MOBI
@@ -124,8 +126,8 @@ class Converter:
tmp = self.MakeOneHTML(html_strs)
self._ConvertStringToFile(tmp, out_file)
except Exception, e:
logging.error('Error %s', e)
logging.debug('Details: %s' % html_strs)
logger.error('Error %s', e)
#logger.debug('Details: %s' % html_strs)
def _ConvertStringToFile(self, html_data, out):
html = HtmlProcessor(html_data)
+18 -12
View File
@@ -280,10 +280,10 @@ class Story(Configurable):
for line in replace.splitlines():
(metakeys,regexp,replacement,condkey,condregexp)=(None,None,None,None,None)
if "&&" in line:
(line,conditional) = map( lambda x: x.strip(), line.split("&&") )
(condkey,condregexp) = map( lambda x: x.strip(), conditional.split("=>") )
(line,conditional) = line.split("&&")
(condkey,condregexp) = conditional.split("=>")
if "=>" in line:
parts = map( lambda x: x.strip(), line.split("=>") )
parts = line.split("=>")
if len(parts) > 2:
metakeys = map( lambda x: x.strip(), parts[0].split(",") )
(regexp,replacement)=parts[1:]
@@ -294,9 +294,13 @@ class Story(Configurable):
regexp = re.compile(regexp)
if condregexp:
condregexp = re.compile(condregexp)
# A way to explicitly include spaces in the
# replacement string. The .ini parser eats any
# trailing spaces.
replacement=replacement.replace('\s',' ')
self.replacements.append([metakeys,regexp,replacement,condkey,condregexp])
def doReplacments(self,value,key):
def doReplacements(self,value,key):
for (metakeys,regexp,replacement,condkey,condregexp) in self.replacements:
if (metakeys == None or key in metakeys) \
and isinstance(value,basestring) \
@@ -336,7 +340,7 @@ class Story(Configurable):
value = value.strftime(self.getConfig(key+"_format","%Y-%m-%d"))
if doreplacements:
value=self.doReplacments(value,key)
value=self.doReplacements(value,key)
if removeallentities and value != None:
return removeAllEntities(value)
else:
@@ -360,8 +364,8 @@ class Story(Configurable):
auth = v
# make sure doreplacements & removeallentities are honored.
if doreplacements:
aurl=self.doReplacments(aurl,'authorUrl')
auth=self.doReplacments(auth,'author')
aurl=self.doReplacements(aurl,'authorUrl')
auth=self.doReplacements(auth,'author')
if removeallentities:
aurl=removeAllEntities(aurl)
auth=removeAllEntities(auth)
@@ -434,7 +438,7 @@ class Story(Configurable):
if retlist:
if doreplacements:
retlist = filter( lambda x : x!=None and x!='' ,
map(partial(self.doReplacments,key=listname),retlist) )
map(partial(self.doReplacements,key=listname),retlist) )
if removeallentities:
retlist = filter( lambda x : x!=None and x!='' ,
map(removeAllEntities,retlist) )
@@ -470,11 +474,11 @@ class Story(Configurable):
return list(subjectset | set(self.getConfigList("extratags")))
def addChapter(self, title, html):
def addChapter(self, url, title, html):
if self.getConfig('strip_chapter_numbers') and \
self.getConfig('chapter_title_strip_pattern'):
title = re.sub(self.getConfig('chapter_title_strip_pattern'),"",title)
self.chapters.append( (title,html) )
self.chapters.append( (url,title,html) )
def getChapters(self,fortoc=False):
"Chapters will be tuples of (title,html)"
@@ -484,8 +488,10 @@ class Story(Configurable):
(self.getConfig('add_chapter_numbers') == "true" \
or (self.getConfig('add_chapter_numbers') == "toconly" and fortoc)) \
and self.getConfig('chapter_title_add_pattern'):
for index, (title,html) in enumerate(self.chapters):
retval.append( (string.Template(self.getConfig('chapter_title_add_pattern')).substitute({'index':index+1,'title':title}),html) )
for index, (url,title,html) in enumerate(self.chapters):
retval.append( (url,
string.Template(self.getConfig('chapter_title_add_pattern')).substitute({'index':index+1,'title':title}),
html) )
else:
retval = self.chapters
+3 -2
View File
@@ -177,11 +177,12 @@ class BaseStoryWriter(Configurable):
self._write(out,START.substitute(self.story.getAllMetadata()))
for index, (title,html) in enumerate(self.story.getChapters(fortoc=True)):
for index, (url,title,html) in enumerate(self.story.getChapters(fortoc=True)):
if html:
self._write(out,ENTRY.substitute({'chapter':title,
'number':index+1,
'index':"%04d"%(index+1)}))
'index':"%04d"%(index+1),
'url':url}))
self._write(out,END.substitute(self.story.getAllMetadata()))
+3 -3
View File
@@ -501,7 +501,7 @@ div { margin: 0pt; padding: 0pt; }
items.append(("log_page","OEBPS/log_page.xhtml","application/xhtml+xml","Update Log"))
itemrefs.append("log_page")
for index, (title,html) in enumerate(self.story.getChapters(fortoc=True)):
for index, (url,title,html) in enumerate(self.story.getChapters(fortoc=True)):
if html:
i=index+1
items.append(("file%04d"%i,
@@ -649,10 +649,10 @@ div { margin: 0pt; padding: 0pt; }
else:
CHAPTER_END = self.EPUB_CHAPTER_END
for index, (title,html) in enumerate(self.story.getChapters()):
for index, (url,title,html) in enumerate(self.story.getChapters()):
if html:
logger.debug('Writing chapter text for: %s' % title)
vals={'chapter':title, 'index':"%04d"%(index+1), 'number':index+1}
vals={'url':url, 'chapter':title, 'index':"%04d"%(index+1), 'number':index+1}
fullhtml = CHAPTER_START.substitute(vals) + html + CHAPTER_END.substitute(vals)
# ffnet(& maybe others) gives the whole chapter text
# as one line. This causes problems for nook(at
+2 -2
View File
@@ -128,10 +128,10 @@ ${output_css}
else:
CHAPTER_END = self.HTML_CHAPTER_END
for index, (title,html) in enumerate(self.story.getChapters()):
for index, (url,title,html) in enumerate(self.story.getChapters()):
if html:
logging.debug('Writing chapter text for: %s' % title)
vals={'chapter':title, 'index':"%04d"%(index+1), 'number':index+1}
vals={'url':url, 'chapter':title, 'index':"%04d"%(index+1), 'number':index+1}
self._write(out,CHAPTER_START.substitute(vals))
self._write(out,html)
self._write(out,CHAPTER_END.substitute(vals))
+8 -3
View File
@@ -22,6 +22,9 @@ import StringIO
from base_writer import *
from ..htmlcleanup import stripHTML
from ..mobi import Converter
from ..exceptions import FailedToWriteOutput
logger = logging.getLogger(__name__)
class MobiWriter(BaseStoryWriter):
@@ -158,10 +161,10 @@ ${value}<br />
else:
CHAPTER_END = self.MOBI_CHAPTER_END
for index, (title,html) in enumerate(self.story.getChapters()):
for index, (url,title,html) in enumerate(self.story.getChapters()):
if html:
logging.debug('Writing chapter text for: %s' % title)
vals={'chapter':title, 'index':"%04d"%(index+1), 'number':index+1}
logger.debug('Writing chapter text for: %s' % title)
vals={'url':url, 'chapter':title, 'index':"%04d"%(index+1), 'number':index+1}
fullhtml = CHAPTER_START.substitute(vals) + html + CHAPTER_END.substitute(vals)
# ffnet(& maybe others) gives the whole chapter text
# as one line. This causes problems for nook(at
@@ -175,6 +178,8 @@ ${value}<br />
author=self.getMetadata('author'),
publisher=self.getMetadata('site'))
mobidata = c.ConvertStrings(files)
if len(mobidata) < 1:
raise FailedToWriteOutput("Zero length mobi output")
out.write(mobidata)
del files
+2 -2
View File
@@ -154,10 +154,10 @@ End file.
else:
CHAPTER_END = self.TEXT_CHAPTER_END
for index, (title,html) in enumerate(self.story.getChapters()):
for index, (url, title,html) in enumerate(self.story.getChapters()):
if html:
logging.debug('Writing chapter text for: %s' % title)
vals={'chapter':title, 'index':"%04d"%(index+1), 'number':index+1}
vals={'url':url, 'chapter':title, 'index':"%04d"%(index+1), 'number':index+1}
self._write(out,self.lineends(self.wraplines(removeAllEntities(CHAPTER_START.substitute(vals)))))
self._write(out,self.lineends(html2text(html,wrap_width=self.wrap_width)))
self._write(out,self.lineends(self.wraplines(removeAllEntities(CHAPTER_END.substitute(vals)))))
+41 -20
View File
@@ -57,11 +57,12 @@
<h3>Changes:</h3>
<p>
<ul>
<li>Fix for changes to fanfiction.net.</li>
<li>Add seriesUrl (and generated seriesHTML) as valid metadata entries. The default series on title_page is now a link.</li>
<li>Add user/pass for dokuga.com.</li>
<li>New Site: scarhead.net -- Thanks, Ida!</li>
<li>Improved error reporting of fanfiction.net site errors</li>
<li>New force_login parameter for phoenixsong.net site.</li>
</ul>
</p>
<p>
Questions? Check out our
<a href="http://code.google.com/p/fanficdownloader/wiki/FanFictionDownloaderFAQs">FAQs</a>.
@@ -70,7 +71,7 @@
If you have any problems with this application, please
report them in
the <a href="http://groups.google.com/group/fanfic-downloader">FanFictionDownLoader Google Group</a>. The
<a href="http://4-4-47.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
<a href="http://4-4-59.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
</p>
<div id='error'>
{{ error_message }}
@@ -380,11 +381,6 @@
Use the URL of the story's first chapter, such as
<br /><a href="http://onedirectionfanfiction.com/viewstory.php?sid=1234">http://onedirectionfanfiction.com/viewstory.php?sid=1234</a>
</dd>
<dt>www.prisonbreakfic.net</dt>
<dd>
Use the URL of the story's first chapter, such as
<br /><a href="http://www.prisonbreakfic.net/viewstory.php?sid=1234">http://www.prisonbreakfic.net/viewstory.php?sid=1234</a>
</dd>
<dt>www.storiesofarda.com</dt>
<dd>
Use the URL of the story's chapter list, such as
@@ -473,11 +469,6 @@
Use the URL of the story's first chapter, such as
<br /><a href="http://www.hpfanficarchive.com/stories/viewstory.php?sid=1234">http://www.hpfanficarchive.com/stories/viewstory.php?sid=1234</a>
</dd>
<dt>svufiction.com</dt>
<dd>
Use the URL of the story's first chapter, such as
<br /><a href="http://svufiction.com/viewstory.php?sid=1234">http://svufiction.com/viewstory.php?sid=1234</a>
</dd>
<dt>www.twilightarchives.com</dt>
<dd>
Use the URL of the story's first chapter, such as
@@ -554,11 +545,6 @@
Use the URL of the story's chapter list, such as
<br /><a href="http://www.indeath.net/blog/25-nightmare-in-death/">http://www.indeath.net/blog/25-nightmare-in-death/</a>
</dd>
<dt>www.jlaunlimited.com</dt>
<dd>
Use the URL of the story's first chapter, such as
<br /><a href="http://www.jlaunlimited.com/eFiction1.1/viewstory.php?sid=1234">http://www.jlaunlimited.com/eFiction1.1/viewstory.php?sid=1234</a>
</dd>
<dt>www.efpfanfic.net</dt>
<dd>
Use the URL of any story chapter, such as
@@ -609,11 +595,46 @@
Use the URL of the story's chapter list, such as
<br /><a href="http://www.henneth-annun.net/stories/chapter.cfm?stid=1234">http://www.henneth-annun.net/stories/chapter.cfm?stid=1234</a>
</dd>
<dt>http://www.psychfic.com</dt>
<dt>www.psychfic.com</dt>
<dd>
Use the URL of the story's chapter list, such as
<br /><a href="http://www.psychfic.com/viewstory.php?sid=1234">http://www.psychfic.com/viewstory.php?sid=1234</a>
</dd>
<dt>tokra.fandomnet.com</dt>
<dd>
Use the URL of the story's chapter list, such as
<br /><a href="http://tokra.fandomnet.com/viewstory.php?sid=1234">http://tokra.fandomnet.com/viewstory.php?sid=1234</a>
</dd>
<dt>asr3.slashzone.org</dt>
<dd>
Use the URL of the story's chapter list, such as
<br /><a href="http://asr3.slashzone.org/archive/viewstory.php?sid=1234">http://asr3.slashzone.org/archive/viewstory.php?sid=1234</a>
</dd>
<dt>netraptor.org</dt>
<dd>
Use the URL of the story's chapter list, such as
<br /><a href="http://netraptor.org/archive/viewstory.php?sid=1234">http://netraptor.org/archive/viewstory.php?sid=1234</a>
</dd>
<dt>nickandgreg.net</dt>
<dd>
Use the URL of the story's chapter list, such as
<br /><a href="http://www.nickandgreg.net/desert_archive/viewstory.php?sid=1234">http://www.nickandgreg.net/desert_archive/viewstory.php?sid=1234</a>
</dd>
<dt>fanfic.potterheadsanonymous.com</dt>
<dd>
Use the URL of the story's chapter list, such as
<br /><a href="http://fanfic.potterheadsanonymous.com/viewstory.php?sid=1234">http://fanfic.potterheadsanonymous.com/viewstory.php?sid=1234</a>
</dd>
<dt>www.simplyundeniable.com</dt>
<dd>
Use the URL of the story's chapter list, such as
<br /><a href="http://www.simplyundeniable.com/viewstory.php?sid=1234">http://www.simplyundeniable.com/viewstory.php?sid=1234</a>
</dd>
<dt>scarhead.net</dt>
<dd>
Use the URL of the story's chapter list, such as
<br /><a href="http://scarhead.net/viewstory.php?sid=1234">http://scarhead.net/viewstory.php?sid=1234</a>
</dd>
</dl>
<p>
+1475 -1337
View File
File diff suppressed because it is too large Load Diff