mirror of
https://github.com/wassname/FanFicFare.git
synced 2026-09-15 11:36:30 +08:00
Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37bc1306c9 | ||
|
|
7f8c39227f | ||
|
|
d4988545b5 | ||
|
|
5ceb96c913 | ||
|
|
3d735e430d | ||
|
|
3ccb895c7b | ||
|
|
f0a1e1d471 | ||
|
|
3719e9860c | ||
|
|
ea43dffe0c | ||
|
|
0a2cff3469 | ||
|
|
3cb92f48cc | ||
|
|
e5cc4ebbd5 | ||
|
|
c6c67f99ca | ||
|
|
c75e62d07e | ||
|
|
e9b203eb57 | ||
|
|
a2eaa9fb22 | ||
|
|
0cf5ec9880 | ||
|
|
465e25d3e6 | ||
|
|
0c85cf1d10 | ||
|
|
7f7a1a983c | ||
|
|
6f4be261ef | ||
|
|
04eb551d48 | ||
|
|
6299fc7582 |
@@ -0,0 +1,78 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
|
||||
<html>
|
||||
<head>
|
||||
<link href="/css/index.css" rel="stylesheet" type="text/css">
|
||||
<title>FanFictionDownLoader (fanfiction.net, fanficauthors, fictionalley, ficwad to epub and HTML)</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<script type="text/javascript">
|
||||
|
||||
var _gaq = _gaq || [];
|
||||
_gaq.push(['_setAccount', 'UA-12136939-1']);
|
||||
_gaq.push(['_trackPageview']);
|
||||
|
||||
(function() {
|
||||
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
|
||||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='main'>
|
||||
<h1>
|
||||
<a href="/" style="text-decoration: none; color: black;">FanFictionDownLoader</a>
|
||||
</h1>
|
||||
|
||||
<script type="text/javascript"><!--
|
||||
google_ad_client = "ca-pub-0320924304307555";
|
||||
/* Standard */
|
||||
google_ad_slot = "8974025478";
|
||||
google_ad_width = 468;
|
||||
google_ad_height = 60;
|
||||
//-->
|
||||
</script>
|
||||
<script type="text/javascript"
|
||||
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
|
||||
</script>
|
||||
<!-- <div id='yourfile'> -->
|
||||
{{yourfile}}
|
||||
<!-- </div> -->
|
||||
|
||||
<div id='helpbox'>
|
||||
{% for fic in fics %}
|
||||
<p>
|
||||
<a href="{{ fic.url }}" title="Link to original story"><span class="recent"><i>{{ fic.title }}</i></span></a>
|
||||
by <a href="{{ fic.authorUrl }}">{{ fic.author }}</a> <b>Download Count:</b> {{ fic.count }} <br />
|
||||
<b>Word Count:</b> {{ fic.numWords }} <b>Chapter Count:</b> {{ fic.numChapters }}<br />
|
||||
{% if fic.category %} <b>Categories:</b> {{ fic.category }} <br /> {% endif %}
|
||||
{% if fic.genre %} <b>Genres:</b> {{ fic.genre }} <br /> {% endif %}
|
||||
{% if fic.language %} <b>Language:</b> {{ fic.language }} <br /> {% endif %}
|
||||
{% if fic.series %} <b>Series:</b> {{ fic.series }} <br /> {% endif %}
|
||||
{% if fic.characters %} <b>Characters:</b> {{ fic.characters }} <br /> {% endif %}
|
||||
{% if fic.status %} <b>Status:</b> {{ fic.status }} <br /> {% endif %}
|
||||
{% if fic.datePublished %} <b>Published:</b> {{ fic.datePublished }} <br /> {% endif %}
|
||||
{% if fic.dateUpdated %} <b>Last Updated:</b> {{ fic.dateUpdated }} <br /> {% endif %}
|
||||
{% if fic.dateCreated %} <b>Last Downloaded:</b> {{ fic.dateCreated }} <br /> {% endif %}
|
||||
{% if fic.rating %} <b>Rating:</b> {{ fic.rating }} <br /> {% endif %}
|
||||
{% if fic.warnings %} <b>Warnings:</b> {{ fic.warnings }} <br /> {% endif %}
|
||||
{% if fic.description %} <b>Summary:</b> {{ fic.description }} <br /> {% endif %}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<script type="text/javascript"><!--
|
||||
google_ad_client = "ca-pub-0320924304307555";
|
||||
/* Standard */
|
||||
google_ad_slot = "8974025478";
|
||||
google_ad_width = 468;
|
||||
google_ad_height = 60;
|
||||
//-->
|
||||
</script>
|
||||
<script type="text/javascript"
|
||||
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
|
||||
</script>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +1,6 @@
|
||||
# ffd-retief-hrd fanfictiondownloader
|
||||
application: fanfictiondownloader
|
||||
version: 4-4-5
|
||||
version: 4-4-9
|
||||
runtime: python27
|
||||
api_version: 1
|
||||
threadsafe: true
|
||||
@@ -35,8 +35,8 @@ handlers:
|
||||
- url: /.*
|
||||
script: main.app
|
||||
|
||||
builtins:
|
||||
- datastore_admin: on
|
||||
#builtins:
|
||||
#- datastore_admin: on
|
||||
|
||||
libraries:
|
||||
- name: django
|
||||
|
||||
@@ -27,7 +27,7 @@ class FanFictionDownLoaderBase(InterfaceActionBase):
|
||||
description = 'UI plugin to download FanFiction stories from various sites.'
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
author = 'Jim Miller'
|
||||
version = (1, 5, 14)
|
||||
version = (1, 5, 20)
|
||||
minimum_calibre_version = (0, 8, 30)
|
||||
|
||||
#: This field defines the GUI plugin class that contains all the code
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<hr />
|
||||
|
||||
<p>Created by Jim Miller, borrowing heavily from Grant Drake's
|
||||
<p>Plugin created by Jim Miller, borrowing heavily from Grant Drake's
|
||||
'<a href="http://www.mobileread.com/forums/showthread.php?t=134856">Reading List</a>',
|
||||
'<a href="http://www.mobileread.com/forums/showthread.php?t=126727">Extract ISBN</a>' and
|
||||
'<a href="http://www.mobileread.com/forums/showthread.php?t=134000">Count Pages</a>'
|
||||
|
||||
+104
-18
@@ -9,16 +9,17 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
import traceback, copy
|
||||
|
||||
from PyQt4.Qt import (QDialog, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QFont,
|
||||
QTextEdit, QComboBox, QCheckBox, QPushButton, QTabWidget, QVariant)
|
||||
from PyQt4.Qt import (QDialog, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QFont, QWidget,
|
||||
QTextEdit, QComboBox, QCheckBox, QPushButton, QTabWidget, QVariant, QScrollArea)
|
||||
|
||||
from calibre.gui2 import dynamic, info_dialog
|
||||
from calibre.utils.config import JSONConfig
|
||||
from calibre.gui2.ui import get_gui
|
||||
|
||||
from calibre_plugins.fanfictiondownloader_plugin.dialogs \
|
||||
import (SKIP, ADDNEW, UPDATE, UPDATEALWAYS, OVERWRITE, OVERWRITEALWAYS,
|
||||
CALIBREONLY,collision_order)
|
||||
import (UPDATE, UPDATEALWAYS, OVERWRITE, collision_order)
|
||||
|
||||
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.adapters import getConfigSections
|
||||
|
||||
from calibre_plugins.fanfictiondownloader_plugin.common_utils \
|
||||
import ( get_library_uuid, KeyboardConfigDialog )
|
||||
@@ -53,6 +54,9 @@ all_prefs.defaults['addtolists'] = False
|
||||
all_prefs.defaults['addtoreadlists'] = False
|
||||
all_prefs.defaults['addtolistsonread'] = False
|
||||
|
||||
all_prefs.defaults['gc_site_settings'] = {}
|
||||
all_prefs.defaults['allow_gc_from_ini'] = True
|
||||
|
||||
all_prefs.defaults['custom_cols'] = {}
|
||||
|
||||
# The list of settings to copy from all_prefs or the previous library
|
||||
@@ -67,7 +71,9 @@ copylist = ['personal.ini',
|
||||
'collision',
|
||||
'deleteotherforms',
|
||||
'adddialogstaysontop',
|
||||
'includeimages']
|
||||
'includeimages',
|
||||
'gc_site_settings',
|
||||
'allow_gc_from_ini']
|
||||
|
||||
# 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
|
||||
@@ -87,7 +93,8 @@ class PrefsFacade():
|
||||
self.all_prefs[libraryid] = dict(self._get_copylist_prefs(self.all_prefs))
|
||||
else:
|
||||
self.all_prefs[libraryid] = dict(self._get_copylist_prefs(self.all_prefs[self.lastlibid]))
|
||||
self.lastlibid = libraryid
|
||||
|
||||
self.lastlibid = libraryid
|
||||
|
||||
return self.all_prefs[libraryid]
|
||||
|
||||
@@ -139,10 +146,15 @@ class ConfigWidget(QWidget):
|
||||
self.personalini_tab = PersonalIniTab(self, plugin_action)
|
||||
tab_widget.addTab(self.personalini_tab, 'personal.ini')
|
||||
|
||||
self.list_tab = ListTab(self, plugin_action)
|
||||
tab_widget.addTab(self.list_tab, 'Reading Lists')
|
||||
self.readinglist_tab = ReadingListTab(self, plugin_action)
|
||||
tab_widget.addTab(self.readinglist_tab, 'Reading Lists')
|
||||
if 'Reading List' not in plugin_action.gui.iactions:
|
||||
self.list_tab.setEnabled(False)
|
||||
self.readinglist_tab.setEnabled(False)
|
||||
|
||||
self.generatecover_tab = GenerateCoverTab(self, plugin_action)
|
||||
tab_widget.addTab(self.generatecover_tab, 'Generate Cover')
|
||||
if 'Generate Cover' not in plugin_action.gui.iactions:
|
||||
self.generatecover_tab.setEnabled(False)
|
||||
|
||||
self.columns_tab = ColumnsTab(self, plugin_action)
|
||||
tab_widget.addTab(self.columns_tab, 'Custom Columns')
|
||||
@@ -165,15 +177,15 @@ class ConfigWidget(QWidget):
|
||||
prefs['adddialogstaysontop'] = self.basic_tab.adddialogstaysontop.isChecked()
|
||||
prefs['includeimages'] = self.basic_tab.includeimages.isChecked()
|
||||
|
||||
if self.list_tab:
|
||||
if self.readinglist_tab:
|
||||
# lists
|
||||
prefs['send_lists'] = ', '.join(map( lambda x : x.strip(), filter( lambda x : x.strip() != '', unicode(self.list_tab.send_lists_box.text()).split(','))))
|
||||
prefs['read_lists'] = ', '.join(map( lambda x : x.strip(), filter( lambda x : x.strip() != '', unicode(self.list_tab.read_lists_box.text()).split(','))))
|
||||
prefs['send_lists'] = ', '.join(map( lambda x : x.strip(), filter( lambda x : x.strip() != '', unicode(self.readinglist_tab.send_lists_box.text()).split(','))))
|
||||
prefs['read_lists'] = ', '.join(map( lambda x : x.strip(), filter( lambda x : x.strip() != '', unicode(self.readinglist_tab.read_lists_box.text()).split(','))))
|
||||
# print("send_lists: %s"%prefs['send_lists'])
|
||||
# print("read_lists: %s"%prefs['read_lists'])
|
||||
prefs['addtolists'] = self.list_tab.addtolists.isChecked()
|
||||
prefs['addtoreadlists'] = self.list_tab.addtoreadlists.isChecked()
|
||||
prefs['addtolistsonread'] = self.list_tab.addtolistsonread.isChecked()
|
||||
prefs['addtolists'] = self.readinglist_tab.addtolists.isChecked()
|
||||
prefs['addtoreadlists'] = self.readinglist_tab.addtoreadlists.isChecked()
|
||||
prefs['addtolistsonread'] = self.readinglist_tab.addtolistsonread.isChecked()
|
||||
|
||||
# personal.ini
|
||||
ini = unicode(self.personalini_tab.ini.toPlainText())
|
||||
@@ -183,6 +195,16 @@ class ConfigWidget(QWidget):
|
||||
# if they've removed everything, reset to default.
|
||||
prefs['personal.ini'] = get_resources('plugin-example.ini')
|
||||
|
||||
# Generate Covers tab
|
||||
gc_site_settings = {}
|
||||
for (site,combo) in self.generatecover_tab.gc_dropdowns.iteritems():
|
||||
val = unicode(combo.itemData(combo.currentIndex()).toString())
|
||||
if val != 'none':
|
||||
gc_site_settings[site] = val
|
||||
#print("gc_site_settings[%s]:%s"%(site,gc_site_settings[site]))
|
||||
prefs['gc_site_settings'] = gc_site_settings
|
||||
prefs['allow_gc_from_ini'] = self.generatecover_tab.allow_gc_from_ini.isChecked()
|
||||
|
||||
# Custom Columns tab
|
||||
colsmap = {}
|
||||
for (col,combo) in self.columns_tab.custcol_dropdowns.iteritems():
|
||||
@@ -191,7 +213,7 @@ class ConfigWidget(QWidget):
|
||||
colsmap[col] = val
|
||||
#print("colsmap[%s]:%s"%(col,colsmap[col]))
|
||||
prefs['custom_cols'] = colsmap
|
||||
|
||||
|
||||
def edit_shortcuts(self):
|
||||
self.save_settings()
|
||||
# Force the menus to be rebuilt immediately, so we have all our actions registered
|
||||
@@ -286,7 +308,7 @@ class BasicTab(QWidget):
|
||||
self.includeimages.setToolTip("Download and include images in EPUB stories. This is equivalent to adding:\n\n[epub]\ninclude_images:true\nkeep_summary_html:true\nmake_firstimage_cover:true\n\n ...to the top of personal.ini. Your settings in personal.ini will override this.")
|
||||
self.includeimages.setChecked(prefs['includeimages'])
|
||||
self.l.addWidget(self.includeimages)
|
||||
|
||||
|
||||
self.l.insertStretch(-1)
|
||||
|
||||
def set_collisions(self):
|
||||
@@ -372,7 +394,7 @@ class ShowDefaultsIniDialog(QDialog):
|
||||
self.ok_button.clicked.connect(self.hide)
|
||||
self.l.addWidget(self.ok_button)
|
||||
|
||||
class ListTab(QWidget):
|
||||
class ReadingListTab(QWidget):
|
||||
|
||||
def __init__(self, parent_dialog, plugin_action):
|
||||
self.parent_dialog = parent_dialog
|
||||
@@ -432,6 +454,70 @@ class ListTab(QWidget):
|
||||
|
||||
self.l.insertStretch(-1)
|
||||
|
||||
class GenerateCoverTab(QWidget):
|
||||
|
||||
def __init__(self, parent_dialog, plugin_action):
|
||||
self.parent_dialog = parent_dialog
|
||||
self.plugin_action = plugin_action
|
||||
QWidget.__init__(self)
|
||||
|
||||
self.l = QVBoxLayout()
|
||||
self.setLayout(self.l)
|
||||
|
||||
try:
|
||||
gc_plugin = plugin_action.gui.iactions['Generate Cover']
|
||||
gc_settings = gc_plugin.get_saved_setting_names()
|
||||
except KeyError:
|
||||
gc_settings= []
|
||||
|
||||
label = QLabel('The Generate Cover plugin can create cover images for books using various metadata and configurations. If you have GC installed, FFDL can run GC on new downloads and metadata updates. Pick a GC setting by site or Default.')
|
||||
label.setWordWrap(True)
|
||||
self.l.addWidget(label)
|
||||
self.l.addSpacing(5)
|
||||
|
||||
scrollable = QScrollArea()
|
||||
scrollcontent = QWidget()
|
||||
scrollable.setWidget(scrollcontent)
|
||||
scrollable.setWidgetResizable(True)
|
||||
self.l.addWidget(scrollable)
|
||||
|
||||
self.sl = QVBoxLayout()
|
||||
scrollcontent.setLayout(self.sl)
|
||||
|
||||
self.gc_dropdowns = {}
|
||||
|
||||
sitelist = getConfigSections()
|
||||
sitelist.sort()
|
||||
sitelist.insert(0,u"Default")
|
||||
for site in sitelist:
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel(site)
|
||||
if site == u"Default":
|
||||
s = "On Metadata update, run Generate Cover with this setting, if not selected for specific site."
|
||||
else:
|
||||
s = "On Metadata update, run Generate Cover with this setting for site (%s)."%site
|
||||
|
||||
label.setToolTip(s)
|
||||
horz.addWidget(label)
|
||||
dropdown = QComboBox(self)
|
||||
dropdown.setToolTip(s)
|
||||
dropdown.addItem('',QVariant('none'))
|
||||
for setting in gc_settings:
|
||||
dropdown.addItem(setting,QVariant(setting))
|
||||
self.gc_dropdowns[site] = dropdown
|
||||
if site in prefs['gc_site_settings']:
|
||||
dropdown.setCurrentIndex(dropdown.findData(QVariant(prefs['gc_site_settings'][site])))
|
||||
|
||||
horz.addWidget(dropdown)
|
||||
self.sl.addLayout(horz)
|
||||
|
||||
self.allow_gc_from_ini = QCheckBox('Allow generate_cover_settings from personal.ini to override.',self)
|
||||
self.allow_gc_from_ini.setToolTip("The INI parameter generate_cover_settings allows you to choose a GC setting based on metadata rather than site,\nbut it's much more complex. generate_cover_settings is ignored when this is off.")
|
||||
self.allow_gc_from_ini.setChecked(prefs['allow_gc_from_ini'])
|
||||
self.l.addWidget(self.allow_gc_from_ini)
|
||||
|
||||
self.l.insertStretch(-1)
|
||||
|
||||
class OtherTab(QWidget):
|
||||
|
||||
def __init__(self, parent_dialog, plugin_action):
|
||||
|
||||
@@ -7,11 +7,12 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2012, Jim Miller'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import time, os, copy, threading
|
||||
import time, os, copy, threading, re
|
||||
from ConfigParser import SafeConfigParser
|
||||
from StringIO import StringIO
|
||||
from functools import partial
|
||||
from datetime import datetime
|
||||
from string import Template
|
||||
|
||||
from PyQt4.Qt import (QApplication, QMenu, QToolButton)
|
||||
|
||||
@@ -772,7 +773,6 @@ make_firstimage_cover:true
|
||||
epubmi = get_metadata(existingepub,'EPUB')
|
||||
if epubmi.cover_data[1] is not None:
|
||||
db.set_cover(book_id, epubmi.cover_data[1])
|
||||
#mi.cover = epubmi.cover_data[1]
|
||||
|
||||
# set author link if found. All current adapters have authorUrl.
|
||||
if 'authorUrl' in book['all_metadata']:
|
||||
@@ -793,7 +793,8 @@ make_firstimage_cover:true
|
||||
print("%s not an existing column, skipping."%col)
|
||||
continue
|
||||
coldef = custom_columns[col]
|
||||
if not meta.startswith('status-') and meta not in book['all_metadata']:
|
||||
if not meta.startswith('status-') and meta not in book['all_metadata'] or \
|
||||
meta.startswith('status-') and 'status' not in book['all_metadata']:
|
||||
print("No value for %s, skipping."%meta)
|
||||
continue
|
||||
if meta not in permitted_values[coldef['datatype']]:
|
||||
@@ -811,8 +812,48 @@ make_firstimage_cover:true
|
||||
if meta == 'status-I':
|
||||
val = book['all_metadata']['status'] == 'In-Progress'
|
||||
db.set_custom(book_id, val, label=label, commit=False)
|
||||
|
||||
|
||||
db.commit()
|
||||
|
||||
if 'Generate Cover' in self.gui.iactions:
|
||||
|
||||
gc_plugin = self.gui.iactions['Generate Cover']
|
||||
setting_name = None
|
||||
if prefs['allow_gc_from_ini']:
|
||||
ffdlconfig = SafeConfigParser()
|
||||
ffdlconfig.readfp(StringIO(get_resources("plugin-defaults.ini")))
|
||||
ffdlconfig.readfp(StringIO(prefs['personal.ini']))
|
||||
adapter = adapters.getAdapter(ffdlconfig,book['url'],options['fileform'])
|
||||
|
||||
# template => regexp to match => GC Setting to use.
|
||||
# generate_cover_settings:
|
||||
# ${category} => Buffy:? the Vampire Slayer => Buffy
|
||||
for line in adapter.getConfig('generate_cover_settings').splitlines():
|
||||
if "=>" in line:
|
||||
(template,regexp,setting) = map( lambda x: x.strip(), line.split("=>") )
|
||||
value = Template(template).substitute(book['all_metadata']).encode('utf8')
|
||||
print("%s(%s) => %s => %s"%(template,value,regexp,setting))
|
||||
if re.search(regexp,value):
|
||||
setting_name = setting
|
||||
break
|
||||
|
||||
if setting_name:
|
||||
print("Generate Cover Setting from generate_cover_settings(%s)"%line)
|
||||
if setting_name not in gc_plugin.get_saved_setting_names():
|
||||
print("GC Name %s not found, discarding! (check personal.ini for typos)"%setting_name)
|
||||
setting_name = None
|
||||
|
||||
if not setting_name and book['all_metadata']['site'] in prefs['gc_site_settings']:
|
||||
setting_name = prefs['gc_site_settings'][book['all_metadata']['site']]
|
||||
|
||||
if not setting_name and 'Default' in prefs['gc_site_settings']:
|
||||
setting_name = prefs['gc_site_settings']['Default']
|
||||
|
||||
if setting_name:
|
||||
print("Running Generate Cover with settings %s."%setting_name)
|
||||
realmi = db.get_metadata(book_id, index_is_id=True)
|
||||
gc_plugin.generate_cover_for_book(realmi,saved_setting_name=setting_name)
|
||||
|
||||
|
||||
def _get_clean_reading_lists(self,lists):
|
||||
if lists == None or lists.strip() == "" :
|
||||
|
||||
@@ -430,6 +430,12 @@ extratags:
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[www.libraryofmoria.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[www.mediaminer.org]
|
||||
|
||||
[www.midnightwhispers.ca]
|
||||
@@ -446,6 +452,17 @@ cover_exclusion_regexp:/stories/999/images/.*?_trophy.png
|
||||
|
||||
[www.siye.co.uk]
|
||||
|
||||
[www.squidge.org/peja]
|
||||
# www.squidge.org/peja calls it Fandom <shrug>
|
||||
category_label:Fandom
|
||||
# Remove numWords -- www.squidge.org/peja word counts are inaccurate
|
||||
titlepage_entries: series,category,genre,language,characters,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,site,description
|
||||
|
||||
[www.squidge.org/peja:txt]
|
||||
## Add URLs since there aren't links.
|
||||
# Remove numWords -- www.squidge.org/peja word counts are inaccurate
|
||||
titlepage_entries: series,category,genre,language,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,site,storyUrl, authorUrl, description
|
||||
|
||||
[www.thewriterscoffeeshop.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -499,6 +516,12 @@ collect_series: false
|
||||
|
||||
[www.whofic.com]
|
||||
|
||||
[www.wraithbait.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[overrides]
|
||||
## It may sometimes be useful to override all of the specific format,
|
||||
## site and site:format sections in your private configuration. For
|
||||
|
||||
@@ -52,6 +52,9 @@ import adapter_nfacommunitycom
|
||||
import adapter_midnightwhispersca
|
||||
import adapter_ksarchivecom
|
||||
import adapter_archiveskyehawkecom
|
||||
import adapter_squidgeorgpeja
|
||||
import adapter_libraryofmoriacom
|
||||
import adapter_wraithbaitcom
|
||||
|
||||
## This bit of complexity allows adapters to be added by just adding
|
||||
## importing. It eliminates the long if/else clauses we used to need
|
||||
@@ -95,11 +98,14 @@ def getAdapter(config,url,fileform=None):
|
||||
fixedurl = fixedurl.replace("http://","http://www.")
|
||||
if cls:
|
||||
adapter = cls(config,fixedurl) # raises InvalidStoryURL
|
||||
adapter.setSectionOrder(adapter.getSiteDomain(),fileform)
|
||||
adapter.setSectionOrder(adapter.getConfigSection(),fileform)
|
||||
return adapter
|
||||
# No adapter found.
|
||||
raise exceptions.UnknownSite( url, [cls.getSiteDomain() for cls in __class_list] )
|
||||
|
||||
def getConfigSections():
|
||||
return [cls.getConfigSection() for cls in __class_list]
|
||||
|
||||
def getClassFor(domain):
|
||||
for cls in __class_list:
|
||||
if cls.matchesSite(domain):
|
||||
|
||||
@@ -101,7 +101,7 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
## Title
|
||||
a = soup.find('a', href=re.compile(r"^/works/\w+"))
|
||||
a = soup.find('a', href=re.compile(r"^/works/\d+$"))
|
||||
self.story.setMetadata('title',a.string)
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
|
||||
@@ -20,7 +20,8 @@ import logging
|
||||
import re
|
||||
import urllib2
|
||||
import cookielib as cl
|
||||
import datetime
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
from .. import BeautifulSoup as bs
|
||||
from ..htmlcleanup import stripHTML
|
||||
@@ -36,7 +37,7 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
self.story.setMetadata('siteabbrev','fimficnet')
|
||||
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
|
||||
self.story.setMetadata('storyId', self.parsedUrl.path.split('/',)[2])
|
||||
self._setURL("http://"+self.getSiteDomain()+"/story/"+self.story.getMetadata('storyId')+"/")
|
||||
self.is_adult = False
|
||||
|
||||
@@ -74,6 +75,10 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
self.opener = urllib2.build_opener(cookieproc)
|
||||
|
||||
try:
|
||||
apiResponse = urllib2.urlopen("http://www.fimfiction.net/api/story.php?story=%s" % (self.story.getMetadata("storyId"))).read()
|
||||
apiData = json.loads(apiResponse)
|
||||
|
||||
# Unfortunately, we still need to load the story index page to parse the characters
|
||||
data = self._fetchUrl(self.url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
@@ -87,9 +92,12 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
if "/images/missing_story.png" in data:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
|
||||
if "Invalid story id" in apiData.values():
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
|
||||
if "This story has been marked as having adult content." in data:
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
|
||||
if self.password:
|
||||
params = {}
|
||||
params['password'] = self.password
|
||||
@@ -100,98 +108,56 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
raise exceptions.FailedToDownload("%s requires story password and fail_on_password is true."%self.url)
|
||||
else:
|
||||
raise exceptions.FailedToLogin(self.url,"Story requires individual password",passwdonly=True)
|
||||
|
||||
storyMetadata = apiData["story"]
|
||||
|
||||
self.story.setMetadata("title", storyMetadata["title"])
|
||||
self.story.setMetadata("author", storyMetadata["author"]["name"])
|
||||
self.story.setMetadata("authorId", storyMetadata["author"]["id"])
|
||||
self.story.setMetadata("authorUrl", "http://%s/user/%s" % (self.getSiteDomain(), storyMetadata["author"]["name"]))
|
||||
|
||||
soup = bs.BeautifulSoup(data).find("div", {"class":"content_box post_content_box"})
|
||||
|
||||
titleheader = soup.find("h2")
|
||||
title = titleheader.find("a", href=re.compile(r'^/story/')).text
|
||||
author = titleheader.find("a", href=re.compile(r'^/user/')).text
|
||||
self.story.setMetadata("title", title)
|
||||
self.story.setMetadata("author", author)
|
||||
self.story.setMetadata("authorId", author) # The author's name will be unique
|
||||
self.story.setMetadata("authorUrl", "http://%s/user/%s" % (self.getSiteDomain(),author))
|
||||
chapters = [{"chapterTitle": chapter["title"], "chapterURL": chapter["link"]} for chapter in storyMetadata["chapters"]]
|
||||
for chapter in chapters:
|
||||
self.chapterUrls.append((chapter["chapterTitle"], chapter["chapterURL"]))
|
||||
self.story.setMetadata("numChapters", len(self.chapterUrls))
|
||||
|
||||
chapterDates = []
|
||||
|
||||
for chapter in soup.findAll("a", {"class":"chapter_link"}):
|
||||
chapterDates.append(chapter.span.extract().text.strip("()"))
|
||||
self.chapterUrls.append((chapter.text.strip(), "http://"+self.getSiteDomain() + chapter['href']))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
for character in [character_icon['title'] for character_icon in soup.findAll("a", {"class":"character_icon"})]:
|
||||
self.story.addToList("characters", character)
|
||||
for category in [category.text for category in soup.find("div", {"class":"categories"}).findAll("a")]:
|
||||
self.story.addToList("genre", category)
|
||||
self.story.addToList("category", "My Little Pony")
|
||||
|
||||
|
||||
# The very last list element in the list of chapters contains the status, rating and word count e.g.:
|
||||
#
|
||||
# <li>
|
||||
# Incomplete | Rating:
|
||||
# <span style="color:#c78238;">Teen</span>
|
||||
# <div class="word_count"><b>5,203</b>words total</div>
|
||||
# </li>
|
||||
#
|
||||
|
||||
status_bar = soup.findAll('li')[-1]
|
||||
# In the case of fimfiction.net, possible statuses are 'Completed', 'Incomplete', 'On Hiatus' and 'Cancelled'
|
||||
# For the sake of bringing it in line with the other adapters, 'Incomplete' and 'On Hiatus' become 'In-Progress'
|
||||
# and 'Complete' beomes 'Completed'. 'Cancelled' seems an important enough (not to mention more strictly true)
|
||||
# status to leave unchanged.
|
||||
status = status_bar.text.split("|")[0].strip().replace("Incomplete", "In-Progress").replace("On Hiatus", "In-Progress").replace("Complete", "Completed")
|
||||
self.story.setMetadata('status', status)
|
||||
self.story.setMetadata('rating', status_bar.span.text)
|
||||
# This way is less elegant, perhaps, but more robust in face of format changes.
|
||||
numWords = status_bar.find("div",{"class":"word_count"}).b.text
|
||||
self.story.setMetadata('numWords', numWords)
|
||||
status = storyMetadata["status"].replace("Incomplete", "In-Progress").replace("On Hiatus", "In-Progress").replace("Complete", "Completed")
|
||||
self.story.setMetadata("status", status)
|
||||
self.story.setMetadata("rating", storyMetadata["content_rating_text"])
|
||||
|
||||
for category in storyMetadata["categories"]:
|
||||
if storyMetadata["categories"][category]:
|
||||
self.story.addToList("genre", category)
|
||||
|
||||
self.story.addToList("category", "My Little Pony")
|
||||
|
||||
self.story.setMetadata("numWords", str(storyMetadata["words"]))
|
||||
|
||||
description_soup = soup.find("div", {"class":"description"})
|
||||
# Sometimes the description has an expanding element
|
||||
# This removes the ellipsis and the expand button
|
||||
try:
|
||||
description_soup.find('span', {"id":re.compile(r"description_more_elipses_\d+")}).extract() # Web designer can't spell 'ellipsis'
|
||||
description_soup.find('a', {"class":"more"}).extract()
|
||||
except:
|
||||
pass
|
||||
|
||||
# fimfic is the first site with an explicit cover image.
|
||||
story_img = soup.find('img',{'class':'story_image'})
|
||||
if self.getConfig('include_images') and story_img:
|
||||
coverurl = story_img['src']
|
||||
if self.getConfig('include_images') and "image" in storyMetadata.keys():
|
||||
coverurl = storyMetadata["image"]
|
||||
if coverurl.startswith('//static.fimfiction.net'): # fix for img urls missing 'http:'
|
||||
coverurl = "http:"+coverurl
|
||||
self.story.addImgUrl(self,self.url,coverurl,self._fetchUrlRaw,cover=True)
|
||||
self.setDescription(self.url,description_soup.text)
|
||||
#self.story.setMetadata('description', description_soup.text)
|
||||
|
||||
self.setDescription(self.url, storyMetadata["description"])
|
||||
|
||||
# Unfortunately, nowhere on the page is the year mentioned.
|
||||
# Best effort to deal with this:
|
||||
# Use this year, if that's a date in the future, subtract one year.
|
||||
# Their earliest story is Jun, so they'll probably change the date
|
||||
# around then.
|
||||
|
||||
now = datetime.datetime.now()
|
||||
# Dates are in Unix time
|
||||
# Take the publish date from the first chapter posted
|
||||
rawDatePublished = storyMetadata["chapters"][0]["date_modified"]
|
||||
self.story.setMetadata("datePublished", datetime.fromtimestamp(rawDatePublished))
|
||||
rawDateUpdated = storyMetadata["date_modified"]
|
||||
self.story.setMetadata("dateUpdated", datetime.fromtimestamp(rawDateUpdated))
|
||||
|
||||
dateUpdated_soup = bs.BeautifulSoup(data).find("div", {"class":"calendar"})
|
||||
dateUpdated_soup.find('span').extract()
|
||||
dateUpdated = makeDate("%s%s"%(now.year,dateUpdated_soup.text), "%Y%b%d")
|
||||
if dateUpdated > now :
|
||||
dateUpdated = dateUpdated.replace(year=now.year-1)
|
||||
self.story.setMetadata("dateUpdated", dateUpdated)
|
||||
|
||||
# Get the date of creation from the first chapter
|
||||
if len(chapterDates) > 0:
|
||||
datePublished_text = chapterDates[0]
|
||||
day, month = datePublished_text.split()
|
||||
day = re.sub(r"[^\d.]+", '', day)
|
||||
datePublished = makeDate("%s%s%s"%(now.year,month,day), "%Y%b%d")
|
||||
if datePublished > now :
|
||||
datePublished = datePublished.replace(year=now.year-1)
|
||||
self.story.setMetadata("datePublished", datePublished)
|
||||
else:
|
||||
self.story.setMetadata("datePublished", dateUpdated)
|
||||
soup = bs.BeautifulSoup(data).find("div", {"class":"story"})
|
||||
for character in [character_icon["title"] for character_icon in soup.findAll("a", {"class":"character_icon"})]:
|
||||
self.story.addToList("characters", character)
|
||||
|
||||
|
||||
def getChapterText(self, url):
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2011 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
|
||||
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 LibraryOfMoriaComAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class LibraryOfMoriaComAdapter(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])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/a/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','lom')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Lord of the Rings")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%B %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.libraryofmoria.com'
|
||||
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://"+self.getSiteDomain()+"/a/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain()+"/a/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"):
|
||||
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+addurl
|
||||
logging.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data)
|
||||
if m != None:
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
# We tried the default and still got a warning, so
|
||||
# let's pull the warning number from the 'continue'
|
||||
# link and reload data.
|
||||
addurl = m.group(1)
|
||||
# correct stupid & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
url = self.url+'&index=1'+addurl
|
||||
logging.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)
|
||||
|
||||
# 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+'/a/'+chapter['href']+addurl))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
# eFiction sites don't help us out a lot with their meta data
|
||||
# formating, so it's a little ugly.
|
||||
|
||||
# utility method
|
||||
def defaultGetattr(d,k):
|
||||
try:
|
||||
return d[k]
|
||||
except:
|
||||
return ""
|
||||
|
||||
# <span class="label">Rated:</span> NC-17<br /> etc
|
||||
labels = soup.findAll('span',{'class':'label'})
|
||||
for labelspan in labels:
|
||||
value = labelspan.nextSibling
|
||||
label = labelspan.string
|
||||
|
||||
if 'Summary' in label:
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'label':
|
||||
svalue += str(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
if 'Rated' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
if 'Categories' in label:
|
||||
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
catstext = [cat.string for cat in cats]
|
||||
for cat in catstext:
|
||||
self.story.addToList('category',cat.string)
|
||||
|
||||
if 'Characters' in label:
|
||||
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
|
||||
charstext = [char.string for char in chars]
|
||||
for char in charstext:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
if 'Type' in label:
|
||||
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1'))
|
||||
for genre in genres:
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
if 'Warning' in label:
|
||||
warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=5'))
|
||||
for warning in warnings:
|
||||
self.story.addToList('warnings',warning.string)
|
||||
|
||||
if 'Completed' in label:
|
||||
if 'Yes' in value:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
if 'Published' in label:
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
if 'Updated' in label:
|
||||
# there's a stray [ at the end.
|
||||
#value = value[0:-1]
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
try:
|
||||
# Find Series name from series URL.
|
||||
a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+"))
|
||||
series_name = a.string
|
||||
series_url = 'http://'+self.host+'/a/'+a['href']
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url))
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$'))
|
||||
i=1
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
break
|
||||
i+=1
|
||||
|
||||
except:
|
||||
# I find it hard to care if the series parsing fails
|
||||
pass
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.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('div', {'id' : 'story'})
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
@@ -0,0 +1,234 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2011 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
|
||||
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 SquidgeOrgPejaAdapter
|
||||
|
||||
## XXX IMPORTANT NOTE!! This adapter is for squidge.org/peja ONLY!
|
||||
## There are lots of other sites and stuff under squidge.org that
|
||||
## we're not supporting. If/When we ever want to support more
|
||||
## sections of squidge.org, FFDL will need to be changed more
|
||||
## fundamentally to find different adapters under the same domain.
|
||||
##
|
||||
## For now, I've only implemented the part for ini section names so
|
||||
## if/when more adapters under squidge.org come along, existing ini
|
||||
## files will still work correctly.
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class SquidgeOrgPejaAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.decode = ["utf8",
|
||||
"Windows-1252"] # 1252 is a superset of iso-8859-1.
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
self.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])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/peja/cgi-bin/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','wwomb')
|
||||
|
||||
# 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.squidge.org'
|
||||
|
||||
@classmethod # must be @staticmethod, don't remove it.
|
||||
def getConfigSection(cls):
|
||||
# The config section name. Only override if != site domain.
|
||||
return cls.getSiteDomain()+'/peja'
|
||||
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://"+self.getSiteDomain()+"/peja/cgi-bin/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://"+self.getSiteDomain()+"/peja/cgi-bin/viewstory.php?sid=")+r"\d+$"
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
url = self.url
|
||||
logging.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
# 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.
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
author = soup.find('div', {'id':"pagetitle"}).find('a')
|
||||
self.story.setMetadata('authorId',author['href'].split('=')[1])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/peja/cgi-bin/'+author['href'])
|
||||
self.story.setMetadata('author',author.string)
|
||||
|
||||
authorSoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')))
|
||||
|
||||
# eFiction sites don't help us out a lot with their meta data
|
||||
# formating, so it's a little ugly.
|
||||
|
||||
title = authorSoup.find('a',{'href':'viewstory.php?sid='+self.story.getMetadata('storyId')})
|
||||
self.story.setMetadata('title',title.string)
|
||||
titleblock=title.parent.parent
|
||||
|
||||
chapterselect=soup.find('select',{'name':'chapter'})
|
||||
if chapterselect:
|
||||
for ch in chapterselect.findAll('option'):
|
||||
self.chapterUrls.append((stripHTML(ch),'http://'+self.host+'/peja/cgi-bin/viewstory.php?sid='+self.story.getMetadata('storyId')+'&chapter='+ch['value']))
|
||||
else:
|
||||
self.chapterUrls.append((title,url))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
|
||||
# utility method
|
||||
def defaultGetattr(d,k):
|
||||
try:
|
||||
return d[k]
|
||||
except:
|
||||
return ""
|
||||
|
||||
# <span class="classification">Rated:</span> NC-17<br /> etc
|
||||
labels = titleblock.findAll('span',{'class':'classification'})
|
||||
for labelspan in labels:
|
||||
value = labelspan.nextSibling
|
||||
label = labelspan.string
|
||||
|
||||
if 'Summary' in label:
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while not defaultGetattr(value,'class') == 'classification':
|
||||
svalue += str(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
if 'Rated' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
if 'Characters' in label:
|
||||
for char in value.split(','):
|
||||
self.story.addToList('characters',char.strip())
|
||||
|
||||
if 'Genre' in label:
|
||||
for genre in value.split(','):
|
||||
if genre.strip() != "None":
|
||||
self.story.addToList('genre',genre.strip())
|
||||
|
||||
if 'Warnings' in label:
|
||||
for warning in value.split(','):
|
||||
if warning.strip() != 'None':
|
||||
self.story.addToList('warnings',warning.strip())
|
||||
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
if 'Fandoms' in label:
|
||||
cats = labelspan.parent.findAll('a',href=re.compile(r'categories.php'))
|
||||
catstext = [cat.string for cat in cats]
|
||||
for cat in catstext:
|
||||
self.story.addToList('category',cat.string)
|
||||
|
||||
if 'Completed' in label:
|
||||
if 'Yes' in value:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
if 'Published' in label:
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
if 'Updated' in label:
|
||||
# there's a stray [ at the end.
|
||||
#value = value[0:-1]
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
try:
|
||||
# Find Series name from series URL.
|
||||
# http://www.squidge.org/peja/cgi-bin/series.php?seriesid=254
|
||||
a = titleblock.find('a', href=re.compile(r"series.php\?seriesid=\d+"))
|
||||
series_name = a.string
|
||||
series_url = 'http://'+self.host+'/peja/cgi-bin/'+a['href']
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url))
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$'))
|
||||
i=1
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
break
|
||||
i+=1
|
||||
|
||||
except:
|
||||
# I find it hard to care if the series parsing fails
|
||||
pass
|
||||
|
||||
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
soup = bs.BeautifulStoneSoup(self._fetchUrl(url),
|
||||
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
|
||||
|
||||
chaptext = soup.find('div',{'id':"story"}).find('span')
|
||||
|
||||
if None == chaptext:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
return self.utf8FromSoup(url,chaptext)
|
||||
@@ -179,11 +179,12 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
|
||||
<div>
|
||||
<h3>Chapter title from site</h3>
|
||||
<p><center>Centered text</center></p>
|
||||
<p>Lorem '''+self.crazystring+''' <i>italics</i>, <b>bold</b>, <u>underline</u> consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
<p>Lorem '''+self.crazystring+u''' <i>italics</i>, <b>bold</b>, <u>underline</u> consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
br breaks<br><br>
|
||||
|
||||
Puella Magi Madoka Magica/魔法少女まどか★マギカ
|
||||
<a href="http://code.google.com/p/fanficdownloader/wiki/FanFictionDownLoaderPluginWithReadingList" title="Tilt-a-Whirl by Jim & Sarah, on Flickr"><img src="http://i.imgur.com/bo8eD.png"></a><br/>
|
||||
br breaks<br><br>
|
||||
Don't—e;ver—d;o—that—a;gain, 法 é
|
||||
<hr>
|
||||
horizontal rules
|
||||
<hr size=1 noshade>
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2011 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
|
||||
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 WraithBaitComAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class WraithBaitComAdapter(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])
|
||||
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
|
||||
|
||||
|
||||
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','wb')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do.
|
||||
self.story.addToList("category","Stargate")
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
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.wraithbait.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=12"
|
||||
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
|
||||
logging.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 "for adults only" in data:
|
||||
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
|
||||
pt = soup.find('div', {'id' : 'pagetitle'})
|
||||
a = pt.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+"))
|
||||
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.
|
||||
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.
|
||||
|
||||
# utility method
|
||||
def defaultGetattr(d,k):
|
||||
try:
|
||||
return d[k]
|
||||
except:
|
||||
return ""
|
||||
|
||||
info = soup.find('div', {'class' : 'small'})
|
||||
|
||||
word=info.find(text=re.compile("Word count:")).split(':')
|
||||
self.story.setMetadata('numWords', word[1])
|
||||
|
||||
cats = info.findAll('a',href=re.compile(r'browse.php\?type=categories&id=\d'))
|
||||
for cat in cats:
|
||||
if "General" != cat.string:
|
||||
self.story.addToList('category',cat.string)
|
||||
|
||||
chars = info.findAll('a',href=re.compile(r'browse.php\?type=characters&charid=\d'))
|
||||
for char in chars:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
completed=info.find(text=re.compile("Completed: Yes"))
|
||||
if completed != None:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
date=soup.find('div',{'class' : 'bottom'})
|
||||
pd=date.find(text=re.compile("Published:")).string.split(': ')
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(pd[1].split(' U')[0]), self.dateformat))
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(pd[2]), self.dateformat))
|
||||
|
||||
# <span class="label">Rated:</span> NC-17<br /> etc
|
||||
labels = soup.findAll('span',{'class':'label'})
|
||||
pub=0
|
||||
for labelspan in labels:
|
||||
value = labelspan.nextSibling
|
||||
label = labelspan.string
|
||||
|
||||
if 'Genres' in label:
|
||||
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1'))
|
||||
for genre in genres:
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
if 'Warnings' in label:
|
||||
warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2'))
|
||||
for warning in warnings:
|
||||
self.story.addToList('warnings',warning.string)
|
||||
|
||||
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))
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$'))
|
||||
i=1
|
||||
for a in storyas:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
break
|
||||
i+=1
|
||||
|
||||
except:
|
||||
# I find it hard to care if the series parsing fails
|
||||
pass
|
||||
|
||||
info.extract()
|
||||
summary = soup.find('div', {'class' : 'content'})
|
||||
self.setDescription(url,summary)
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logging.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('div', {'id' : 'story'})
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
|
||||
@@ -70,9 +70,7 @@ class BaseSiteAdapter(Configurable):
|
||||
def __init__(self, config, url):
|
||||
self.config = config
|
||||
Configurable.__init__(self, config)
|
||||
self.setSectionOrder(self.getSiteDomain())
|
||||
# self.addConfigSection(self.getSiteDomain())
|
||||
# self.addConfigSection("overrides")
|
||||
self.setSectionOrder(self.getConfigSection())
|
||||
|
||||
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
@@ -82,7 +80,7 @@ class BaseSiteAdapter(Configurable):
|
||||
self.storyDone = False
|
||||
self.metadataDone = False
|
||||
self.story = Story()
|
||||
self.story.setMetadata('site',self.getSiteDomain())
|
||||
self.story.setMetadata('site',self.getConfigSection())
|
||||
self.story.setMetadata('dateCreated',datetime.datetime.now())
|
||||
self.chapterUrls = [] # tuples of (chapter title,chapter url)
|
||||
self.chapterFirst = None
|
||||
@@ -235,6 +233,11 @@ class BaseSiteAdapter(Configurable):
|
||||
"Needs to be overriden in each adapter class."
|
||||
return 'no such domain'
|
||||
|
||||
@classmethod
|
||||
def getConfigSection(cls):
|
||||
"Only needs to be overriden if != site domain."
|
||||
return cls.getSiteDomain()
|
||||
|
||||
## URL pattern validation is done *after* picking an adaptor based
|
||||
## on domain instead of *as* the adaptor selector so we can offer
|
||||
## the user example(s) for that particular site.
|
||||
|
||||
@@ -19,12 +19,21 @@ import re
|
||||
|
||||
def _unirepl(match):
|
||||
"Return the unicode string for a decimal number"
|
||||
if match.group(1)=='x':
|
||||
if match.group(1).startswith('x'):
|
||||
radix=16
|
||||
s = match.group(1)[1:]
|
||||
else:
|
||||
radix=10
|
||||
value = int(match.group(2), radix)
|
||||
return "%s%s"%(unichr(value),match.group(3))
|
||||
s = match.group(1)
|
||||
try:
|
||||
value = int(s, radix)
|
||||
retval = "%s%s"%(unichr(value),match.group(2))
|
||||
except:
|
||||
# This way, at least if there's more of entities out there
|
||||
# that fail, it doesn't blow the entire download.
|
||||
print "Numeric entity translation failed, skipping: &#x%s%s"%(match.group(1),match.group(2))
|
||||
retval = ""
|
||||
return retval
|
||||
|
||||
def _replaceNumberEntities(data):
|
||||
# The same brokenish entity parsing in SGMLParser that inserts ';'
|
||||
@@ -32,7 +41,9 @@ def _replaceNumberEntities(data):
|
||||
# entities, including part of the next word if it's a-z.
|
||||
# "Don't—ever—do—that—again," becomes
|
||||
# "Don't—e;ver—d;o—that—a;gain,"
|
||||
p = re.compile(r'&#(x?)([0-9a-fA-F]{,4})([0-9a-fA-F]*);')
|
||||
# Also need to allow for 5 digit decimal entities 法
|
||||
# Last expression didn't allow for 2 digit hex correctly: é
|
||||
p = re.compile(r'&#(x[0-9a-fA-F]{,4}|[0-9]{,5})([0-9a-fA-F]*?);')
|
||||
return p.sub(_unirepl, data)
|
||||
|
||||
def _replaceNotEntities(data):
|
||||
|
||||
@@ -218,7 +218,7 @@ class Story:
|
||||
value = re.sub(p,v,value)
|
||||
return value
|
||||
|
||||
def getMetadata(self, key, removeallentities=False):
|
||||
def getMetadata(self, key, removeallentities=False, doreplacements=True):
|
||||
value = None
|
||||
if self.getLists().has_key(key):
|
||||
value = ', '.join(self.getList(key))
|
||||
@@ -232,21 +232,22 @@ class Story:
|
||||
if key == "datePublished" or key == "dateUpdated":
|
||||
value = value.strftime("%Y-%m-%d")
|
||||
|
||||
value=self.doReplacments(value)
|
||||
if doreplacements:
|
||||
value=self.doReplacments(value)
|
||||
if removeallentities and value != None:
|
||||
return removeAllEntities(value)
|
||||
else:
|
||||
return value
|
||||
|
||||
def getAllMetadata(self, removeallentities=False):
|
||||
def getAllMetadata(self, removeallentities=False, doreplacements=True):
|
||||
'''
|
||||
All single value *and* list value metadata as strings.
|
||||
'''
|
||||
allmetadata = {}
|
||||
for k in self.metadata.keys():
|
||||
allmetadata[k] = self.getMetadata(k, removeallentities)
|
||||
allmetadata[k] = self.getMetadata(k, removeallentities, doreplacements)
|
||||
for l in self.listables.keys():
|
||||
allmetadata[l] = self.getMetadata(l, removeallentities)
|
||||
allmetadata[l] = self.getMetadata(l, removeallentities, doreplacements)
|
||||
|
||||
return allmetadata
|
||||
|
||||
|
||||
@@ -38,11 +38,7 @@ class BaseStoryWriter(Configurable):
|
||||
|
||||
def __init__(self, config, adapter):
|
||||
Configurable.__init__(self, config)
|
||||
self.setSectionOrder(adapter.getSiteDomain(),self.getFormatName())
|
||||
# self.addConfigSection(adapter.getSiteDomain())
|
||||
# self.addConfigSection(self.getFormatName())
|
||||
# self.addConfigSection(adapter.getSiteDomain()+":"+self.getFormatName())
|
||||
# self.addConfigSection("overrides")
|
||||
self.setSectionOrder(adapter.getConfigSection(),self.getFormatName())
|
||||
|
||||
self.adapter = adapter
|
||||
self.story = adapter.getStoryMetadataOnly() # only cache the metadata initially.
|
||||
|
||||
@@ -13,8 +13,23 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import pickle, copy
|
||||
from google.appengine.ext import db
|
||||
|
||||
class ObjectProperty(db.Property):
|
||||
data_type = db.Blob
|
||||
|
||||
def get_value_for_datastore(self, model_instance):
|
||||
value = self.__get__(model_instance, model_instance.__class__)
|
||||
pickled_val = pickle.dumps(value,protocol=pickle.HIGHEST_PROTOCOL)
|
||||
if value is not None: return db.Blob(pickled_val)
|
||||
|
||||
def make_value_from_datastore(self, value):
|
||||
if value is not None: return pickle.loads(value)
|
||||
|
||||
def default_value(self):
|
||||
return copy.copy(self.default)
|
||||
|
||||
class DownloadMeta(db.Model):
|
||||
user = db.UserProperty()
|
||||
url = db.StringProperty()
|
||||
@@ -37,3 +52,12 @@ class DownloadData(db.Model):
|
||||
class UserConfig(db.Model):
|
||||
user = db.UserProperty()
|
||||
config = db.BlobProperty()
|
||||
|
||||
class SavedMeta(db.Model):
|
||||
url = db.StringProperty()
|
||||
title = db.StringProperty()
|
||||
author = db.StringProperty()
|
||||
date = db.DateTimeProperty(auto_now_add=True)
|
||||
count = db.IntegerProperty()
|
||||
meta = ObjectProperty()
|
||||
|
||||
|
||||
+31
-3
@@ -54,9 +54,18 @@
|
||||
much easier. </p>
|
||||
</div>
|
||||
<!-- put announcements here, h3 is a good title size. -->
|
||||
<h3>New Site Added</h3>
|
||||
<h3>New Sites</h3>
|
||||
<p>
|
||||
Support for archive.skyehawke.com has been added. Thanks to Ida Leter for implementing this.
|
||||
We now support www.wraithbait.com and
|
||||
www.libraryofmoria.com, thanks to Ida for adding these!
|
||||
</p>
|
||||
<p>
|
||||
Support for the Wonderful World of MakeBelieve(WWOMB)
|
||||
archive at
|
||||
<a href="http://www.squidge.org/peja/cgi-bin/index.php">http://www.squidge.org/peja/cgi-bin/index.php</a>
|
||||
has also been added. This does not support other sections of
|
||||
www.squidge.org, or the other files under www.squidge.org/peja that
|
||||
aren't in the eFiction instance.
|
||||
</p>
|
||||
<p>
|
||||
Questions? Check out our
|
||||
@@ -66,7 +75,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-4.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
|
||||
<a href="http://4-4-8.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
|
||||
</p>
|
||||
<div id='error'>
|
||||
{{ error_message }}
|
||||
@@ -96,6 +105,9 @@
|
||||
<p>
|
||||
Or see your personal list of <a href="/recent">previously downloaded fanfics</a>.
|
||||
</p>
|
||||
<p>
|
||||
See a list of downloaded fanfics by all users by <a href="/allrecent">most popular</a> or <a href="/allrecent?bydate=1">most recent</a>.
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
@@ -288,6 +300,22 @@
|
||||
Use the URL of the story's summary, such as
|
||||
<br /><a href="http://archive.skyehawke.com/story.php?no=17466">http://archive.skyehawke.com/story.php?no=17466</a>.
|
||||
</dd>
|
||||
<dt>www.libraryofmoria.com</dt>
|
||||
<dd>
|
||||
Use the URL of the story's summary, such as
|
||||
<br /><a href="http://www.libraryofmoria.com/a/viewstory.php?sid=434">http://www.libraryofmoria.com/a/viewstory.php?sid=434</a>.
|
||||
</dd>
|
||||
<dt>www.wraithbait.com</dt>
|
||||
<dd>
|
||||
Use the URL of the story's summary, such as
|
||||
<br /><a href="http://www.wraithbait.com/viewstory.php?sid=14305">http://www.wraithbait.com/viewstory.php?sid=14305</a>.
|
||||
</dd>
|
||||
<dt>www.squidge.org/peja (WWOMB)</dt>
|
||||
<dd>
|
||||
Use the URL of the story's summary, such as
|
||||
<br /><a href="http://www.squidge.org/peja/cgi-bin/viewstory.php?sid=1234">http://www.squidge.org/peja/cgi-bin/viewstory.php?sid=1234</a>.<br />
|
||||
This is <i>only</i> for squidge.org/peja, not other parts of squidge.org.
|
||||
</dd>
|
||||
</dl>
|
||||
<p>
|
||||
A few additional things to know, which will make your life substantially easier:
|
||||
|
||||
+3
-8
@@ -1,6 +1,6 @@
|
||||
indexes:
|
||||
|
||||
# AUTOGENERATED
|
||||
# notAUTOGENERATED
|
||||
|
||||
# This index.yaml is automatically updated whenever the dev_appserver
|
||||
# detects that a new type of query is run. If you want to manage the
|
||||
@@ -21,13 +21,8 @@ indexes:
|
||||
- name: date
|
||||
direction: desc
|
||||
|
||||
- kind: DownloadedFanfic
|
||||
- kind: SavedMeta
|
||||
properties:
|
||||
- name: cleared
|
||||
- name: date
|
||||
|
||||
- kind: DownloadedFanfic
|
||||
properties:
|
||||
- name: user
|
||||
- name: count
|
||||
- name: date
|
||||
direction: desc
|
||||
|
||||
@@ -303,6 +303,39 @@ class RecentFilesServer(webapp2.RequestHandler):
|
||||
path = os.path.join(os.path.dirname(__file__), 'recent.html')
|
||||
self.response.out.write(template.render(path, template_values))
|
||||
|
||||
class AllRecentFilesServer(webapp2.RequestHandler):
|
||||
def get(self):
|
||||
user = users.get_current_user()
|
||||
if not user:
|
||||
self.redirect(users.create_login_url(self.request.uri))
|
||||
return
|
||||
|
||||
q = SavedMeta.all()
|
||||
if self.request.get('bydate'):
|
||||
q.order('-date')
|
||||
else:
|
||||
q.order('-count')
|
||||
|
||||
fics = q.fetch(200)
|
||||
logging.info("Recent fetched %d downloads for user %s."%(len(fics),user.nickname()))
|
||||
|
||||
sendslugs = []
|
||||
|
||||
for fic in fics:
|
||||
ficslug = FicSlug(fic)
|
||||
sendslugs.append(ficslug)
|
||||
|
||||
template_values = dict(fics = sendslugs, nickname = user.nickname())
|
||||
path = os.path.join(os.path.dirname(__file__), 'allrecent.html')
|
||||
self.response.out.write(template.render(path, template_values))
|
||||
|
||||
class FicSlug():
|
||||
def __init__(self,savedmeta):
|
||||
self.url = savedmeta.url
|
||||
self.count = savedmeta.count
|
||||
for k, v in savedmeta.meta.iteritems():
|
||||
setattr(self,k,v)
|
||||
|
||||
class FanfictionDownloader(UserConfigServer):
|
||||
def get(self):
|
||||
self.post()
|
||||
@@ -385,7 +418,7 @@ class FanfictionDownloader(UserConfigServer):
|
||||
template_values = dict(nickname = user.nickname(),
|
||||
url = url,
|
||||
format = format,
|
||||
site = adapter.getSiteDomain(),
|
||||
site = adapter.getConfigSection(),
|
||||
fic = download,
|
||||
is_login=is_login,
|
||||
)
|
||||
@@ -464,6 +497,8 @@ class FanfictionDownloaderTask(UserConfigServer):
|
||||
download.url = adapter.getStory().getMetadata('storyUrl')
|
||||
download.put()
|
||||
|
||||
allmeta = adapter.getStory().getAllMetadata(removeallentities=True,doreplacements=False)
|
||||
|
||||
outbuffer = StringIO()
|
||||
writer.writeStory(outbuffer)
|
||||
data = outbuffer.getvalue()
|
||||
@@ -495,6 +530,21 @@ class FanfictionDownloaderTask(UserConfigServer):
|
||||
download.completed=True
|
||||
download.put()
|
||||
|
||||
smetal = SavedMeta.all().filter('url =', allmeta['storyUrl'] ).fetch(1)
|
||||
if smetal and smetal[0]:
|
||||
smeta = smetal[0]
|
||||
smeta.count += 1
|
||||
else:
|
||||
smeta=SavedMeta()
|
||||
smeta.count = 1
|
||||
|
||||
smeta.url = allmeta['storyUrl']
|
||||
smeta.title = allmeta['title']
|
||||
smeta.author = allmeta['author']
|
||||
smeta.meta = allmeta
|
||||
smeta.date = datetime.datetime.now()
|
||||
smeta.put()
|
||||
|
||||
logging.info("Download finished OK")
|
||||
del data
|
||||
|
||||
@@ -563,6 +613,7 @@ app = webapp2.WSGIApplication([('/', MainHandler),
|
||||
('/fdown', FanfictionDownloader),
|
||||
(r'/file.*', FileServer),
|
||||
('/status', FileStatusServer),
|
||||
('/allrecent', AllRecentFilesServer),
|
||||
('/recent', RecentFilesServer),
|
||||
('/editconfig', EditConfigServer),
|
||||
('/clearrecent', ClearRecentServer),
|
||||
|
||||
@@ -127,6 +127,20 @@ extratags: FanFiction
|
||||
## doesn't work on some devices either.)
|
||||
#replace_hr: false
|
||||
|
||||
## If you have the Generate Cover plugin installed, you can use the
|
||||
## generate_cover_settings parameter to intelligently decide which GC
|
||||
## setting to run. There are three parts 1) a template of which
|
||||
## metadata part(s) to look at, 2) a regular expression to match the
|
||||
## template, and 3) the name of the GC setting to use, which must
|
||||
## match exactly. Use this parameter in [defaults], or by site eg,
|
||||
## [www.ficwad.com]
|
||||
## Make sure to keep at least one space at the start of each line and
|
||||
## to escape % to %%, if used.
|
||||
## template => regexp to match => GC Setting to use.
|
||||
#generate_cover_settings:
|
||||
# ${category} => Buffy:? [tT]he Vampire Slayer => BuffyCover
|
||||
# ${category} => Star Trek => StarTrekCover
|
||||
|
||||
## Each output format has a section that overrides [defaults]
|
||||
[html]
|
||||
|
||||
@@ -399,6 +413,12 @@ extratags:
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[www.libraryofmoria.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[www.mediaminer.org]
|
||||
|
||||
[www.midnightwhispers.ca]
|
||||
@@ -415,6 +435,17 @@ cover_exclusion_regexp:/stories/999/images/.*?_trophy.png
|
||||
|
||||
[www.siye.co.uk]
|
||||
|
||||
[www.squidge.org/peja]
|
||||
# www.squidge.org/peja calls it Fandom <shrug>
|
||||
category_label:Fandom
|
||||
# Remove numWords -- www.squidge.org/peja word counts are inaccurate
|
||||
titlepage_entries: series,category,genre,language,characters,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,site,description
|
||||
|
||||
[www.squidge.org/peja:txt]
|
||||
## Add URLs since there aren't links.
|
||||
# Remove numWords -- www.squidge.org/peja word counts are inaccurate
|
||||
titlepage_entries: series,category,genre,language,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,site,storyUrl, authorUrl, description
|
||||
|
||||
[www.thewriterscoffeeshop.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -468,6 +499,12 @@ collect_series: false
|
||||
|
||||
[www.whofic.com]
|
||||
|
||||
[www.wraithbait.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[overrides]
|
||||
## It may sometimes be useful to override all of the specific format,
|
||||
## site and site:format sections in your private configuration. For
|
||||
|
||||
Reference in New Issue
Block a user