mirror of
https://github.com/wassname/FanFicFare.git
synced 2026-09-13 12:11:20 +08:00
Compare commits
67
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e61bebf21 | ||
|
|
7f47100db4 | ||
|
|
83b48d15d8 | ||
|
|
5aec8188a2 | ||
|
|
5a34f4c86a | ||
|
|
8e5ba7b634 | ||
|
|
7a47d1fad2 | ||
|
|
7f4749a022 | ||
|
|
b44f059e57 | ||
|
|
6b9058a9eb | ||
|
|
5acf9a8d0b | ||
|
|
454c7ffb2f | ||
|
|
f6dcb447b0 | ||
|
|
5ce064bf92 | ||
|
|
c9a1537190 | ||
|
|
36e192e82c | ||
|
|
42058d02b3 | ||
|
|
e0832b9deb | ||
|
|
4ccb94fca0 | ||
|
|
0e5b64bee0 | ||
|
|
53a325ffa3 | ||
|
|
29f900199e | ||
|
|
f56fb0efed | ||
|
|
a703011aef | ||
|
|
a635272bcf | ||
|
|
1079ed565c | ||
|
|
17ab4a3d25 | ||
|
|
2566e434ee | ||
|
|
91f5269453 | ||
|
|
235f00a5e6 | ||
|
|
aad4a26131 | ||
|
|
f20a02a2bc | ||
|
|
1ba1da4d65 | ||
|
|
a951fc3d6c | ||
|
|
606785f6e7 | ||
|
|
842ab4feeb | ||
|
|
029af794cf | ||
|
|
87033bd4a3 | ||
|
|
c8577893c4 | ||
|
|
a822064da0 | ||
|
|
af7c717c20 | ||
|
|
56c75350a6 | ||
|
|
34b5076753 | ||
|
|
f0847809ad | ||
|
|
976ddf827e | ||
|
|
da188234ac | ||
|
|
89a676d0b6 | ||
|
|
1d48b7c72c | ||
|
|
ce82d96163 | ||
|
|
049486a059 | ||
|
|
d098bdbdc8 | ||
|
|
2dd85dd044 | ||
|
|
fcc5c7ffcc | ||
|
|
9c25271a59 | ||
|
|
6dd5522b7a | ||
|
|
aaccb45df5 | ||
|
|
7b052d353a | ||
|
|
d969fbd251 | ||
|
|
bcc4ee5efd | ||
|
|
6b2a03dc8f | ||
|
|
a5a3d284b5 | ||
|
|
b3bf2fc50a | ||
|
|
22f4cc76fc | ||
|
|
c02ad3f6d5 | ||
|
|
e65e209ab3 | ||
|
|
d940936713 | ||
|
|
423e1d2840 |
@@ -1,6 +1,6 @@
|
||||
# ffd-retief-hrd fanfictiondownloader
|
||||
application: fanfictiondownloader
|
||||
version: 4-4-53
|
||||
version: 4-4-61
|
||||
runtime: python27
|
||||
api_version: 1
|
||||
threadsafe: true
|
||||
|
||||
@@ -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, 20)
|
||||
version = (1, 7, 28)
|
||||
minimum_calibre_version = (0, 8, 57)
|
||||
|
||||
#: This field defines the GUI plugin class that contains all the code
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -15,8 +15,8 @@ from PyQt4.Qt import (QDialog, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QCheckBox, QPushButton, QTabWidget, QVariant, QScrollArea,
|
||||
QDialogButtonBox )
|
||||
|
||||
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 \
|
||||
|
||||
@@ -53,13 +53,14 @@ 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)
|
||||
@@ -258,8 +259,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,
|
||||
@@ -828,7 +829,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 +953,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):
|
||||
|
||||
@@ -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']):
|
||||
@@ -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):
|
||||
|
||||
+87
-3
@@ -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
|
||||
@@ -702,6 +722,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 +786,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
|
||||
@@ -918,6 +944,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 +1183,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
|
||||
@@ -1356,3 +1396,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
|
||||
|
||||
+12
-4
@@ -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."
|
||||
|
||||
@@ -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,10 @@ import adapter_imagineeficcom
|
||||
import adapter_buffynfaithnet
|
||||
import adapter_psychficcom
|
||||
import adapter_hennethannunnet
|
||||
import adapter_tokrafandomnetcom
|
||||
import adapter_netraptororg
|
||||
import adapter_asr3slashzoneorg
|
||||
import adapter_nickandgregnet
|
||||
|
||||
## 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)
|
||||
|
||||
+76
-111
@@ -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,10 @@ from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
|
||||
def getClass():
|
||||
return JLAUnlimitedComAdapter
|
||||
return Asr3SlashzoneOrgAdapter
|
||||
|
||||
|
||||
class JLAUnlimitedComAdapter(BaseSiteAdapter):
|
||||
class Asr3SlashzoneOrgAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
@@ -42,36 +40,34 @@ 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() + '/archive/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','asr3')
|
||||
|
||||
# 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/%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.jlaunlimited.com'
|
||||
return 'asr3.slashzone.org'
|
||||
|
||||
def getSiteExampleURLs(self):
|
||||
return "http://"+self.getSiteDomain()+"/eFiction1.1/viewstory.php?sid=1234"
|
||||
return "http://"+self.getSiteDomain()+"/archive/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()+"/archive/viewstory.php?sid=")+r"\d+$"
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
@@ -81,7 +77,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=3"
|
||||
else:
|
||||
addurl=""
|
||||
|
||||
@@ -98,8 +94,7 @@ 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+((?:&ageconsent=ok)?&warning=\d+)",data)
|
||||
m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data)
|
||||
if m != None:
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
# We tried the default and still got a warning, so
|
||||
@@ -120,141 +115,111 @@ 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
|
||||
#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')))
|
||||
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('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+")):
|
||||
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+'/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.
|
||||
|
||||
# utility method
|
||||
def defaultGetattr(d,k):
|
||||
try:
|
||||
return d[k]
|
||||
except:
|
||||
return ""
|
||||
metadiv = soup.find('div',{'class':'content'})
|
||||
smalldiv = metadiv.find('div',{'class':'small'})
|
||||
|
||||
# <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
|
||||
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)
|
||||
|
||||
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))
|
||||
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)
|
||||
|
||||
if 'Rated' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
metatext = stripHTML(smalldiv)
|
||||
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value)
|
||||
if 'Completed: Yes' in metatext:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
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)
|
||||
wordstart=metatext.rindex('Word count:')+12
|
||||
words = metatext[wordstart:metatext.index(' ',wordstart)]
|
||||
self.story.setMetadata('numWords', words)
|
||||
|
||||
if 'Characters' in label:
|
||||
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
|
||||
charstext = [char.string for char in chars]
|
||||
for char in charstext:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
## Not all sites use Genre, but there's no harm to
|
||||
## leaving it in. Check to make sure the type_id number
|
||||
## is correct, though--it's site specific.
|
||||
if 'Genre' in label:
|
||||
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) # XXX
|
||||
genrestext = [genre.string for genre in genres]
|
||||
self.genre = ', '.join(genrestext)
|
||||
for genre in genrestext:
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
## Not all sites use Warnings, but there's no harm to
|
||||
## leaving it in. Check to make sure the type_id number
|
||||
## is correct, though--it's site specific.
|
||||
if 'Warnings' in label:
|
||||
warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX
|
||||
warningstext = [warning.string for warning in warnings]
|
||||
self.warning = ', '.join(warningstext)
|
||||
for warning in warningstext:
|
||||
self.story.addToList('warnings',warning.string)
|
||||
|
||||
if 'Completed' in label:
|
||||
if 'Yes' in value:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
if 'Published' in label:
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
if 'Updated' in label:
|
||||
# there's a stray [ at the end.
|
||||
#value = value[0:-1]
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
|
||||
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+'/fanfic/'+a['href']
|
||||
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))
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$'))
|
||||
# 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:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
# 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),
|
||||
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
|
||||
|
||||
div = soup.find('div', {'id' : 'story'})
|
||||
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+((?:&ageconsent=ok)?&warning=\d+)'",data)
|
||||
if m != None:
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
# We tried the default and still got a warning, so
|
||||
# let's pull the warning number from the 'continue'
|
||||
# link and reload data.
|
||||
addurl = m.group(1)
|
||||
# correct stupid & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
url = self.url+'&index=1'+addurl
|
||||
logger.debug("URL 2nd try: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
else:
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -135,12 +135,25 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
categories = soup.find('div',{'id':'pre_story_links'}).findAll('a',{'class':'xcontrast_txt'})
|
||||
#print("xcontrast_txt a:%s"%categories)
|
||||
if len(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
|
||||
|
||||
@@ -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:
|
||||
|
||||
+26
-32
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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+$")):
|
||||
|
||||
@@ -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(') 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. & 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>=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
|
||||
|
||||
+60
-97
@@ -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 TokraFandomnetComAdapter
|
||||
|
||||
# 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 TokraFandomnetComAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
@@ -54,16 +54,17 @@ 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','tokra')
|
||||
|
||||
# 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 = "%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 'svufiction.com'
|
||||
# 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"
|
||||
@@ -71,50 +72,15 @@ class SVUFictionComAdapter(BaseSiteAdapter):
|
||||
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 'Registered Users Only' in data \
|
||||
or 'There is no such account on our website' in data \
|
||||
or "That password doesn't match the one in our database" in data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def performLogin(self, url):
|
||||
params = {}
|
||||
|
||||
if self.password:
|
||||
params['penname'] = self.username
|
||||
params['password'] = self.password
|
||||
else:
|
||||
params['penname'] = self.getConfig("username")
|
||||
params['password'] = self.getConfig("password")
|
||||
params['cookiecheck'] = '1'
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://' + self.getSiteDomain() + '/user.php?action=login'
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._fetchUrl(loginUrl, params)
|
||||
|
||||
if "Member Account" not in d : #Member Account
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
# Weirdly, different sites use different warning numbers.
|
||||
# If the title search below fails, there's a good chance
|
||||
# you need a different number. print data at that point
|
||||
# and see what the 'click here to continue' url says.
|
||||
addurl = "&ageconsent=ok&warning=6"
|
||||
addurl = "&ageconsent=ok&warning=3"
|
||||
else:
|
||||
addurl=""
|
||||
|
||||
@@ -130,12 +96,7 @@ 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)
|
||||
|
||||
|
||||
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"):
|
||||
@@ -163,26 +124,24 @@ class SVUFictionComAdapter(BaseSiteAdapter):
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = bs.BeautifulSoup(data)
|
||||
# print data
|
||||
#print data
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
## 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 = 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 = pt.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
|
||||
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=pt.text.split('[')[1].split(']')[0]
|
||||
self.story.setMetadata('rating', rating)
|
||||
# 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+$")):
|
||||
@@ -194,45 +153,36 @@ 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 ""
|
||||
metadiv = soup.find('div',{'class':'content'})
|
||||
smalldiv = metadiv.find('div',{'class':'small'})
|
||||
|
||||
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))
|
||||
# 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)
|
||||
|
||||
|
||||
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:
|
||||
metatext = stripHTML(smalldiv)
|
||||
|
||||
if 'Completed: Yes' in metatext:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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+"))
|
||||
@@ -257,17 +207,30 @@ class SVUFictionComAdapter(BaseSiteAdapter):
|
||||
# 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.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'})
|
||||
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,6 +154,30 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('author',stripHTML(a))
|
||||
authorurl = 'http://'+self.host+a['href']
|
||||
|
||||
try:
|
||||
# going to pull part of the meta data from *primary* author list page.
|
||||
logger.debug("**AUTHOR** URL: "+authorurl)
|
||||
authordata = self._fetchUrl(authorurl)
|
||||
descurl=authorurl
|
||||
authorsoup = bs.BeautifulSoup(authordata)
|
||||
# author can have several pages, scan until we find it.
|
||||
while( not authorsoup.find('a', href=re.compile(r"^/Story-"+self.story.getMetadata('storyId'))) ):
|
||||
nextpage = 'http://'+self.host+authorsoup.find('a', {'class':'arrowf'})['href']
|
||||
logger.debug("**AUTHOR** nextpage URL: "+nextpage)
|
||||
authordata = self._fetchUrl(nextpage)
|
||||
descurl=nextpage
|
||||
authorsoup = bs.BeautifulSoup(authordata)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
storydiv = authorsoup.find('div', {'id':'st'+self.story.getMetadata('storyId'), 'class':re.compile(r"storylistitem")})
|
||||
self.setDescription(descurl,storydiv.find('div',{'class':'storydesc'}))
|
||||
#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:
|
||||
@@ -201,30 +225,6 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
try:
|
||||
# going to pull part of the meta data from *primary* author list page.
|
||||
logger.debug("**AUTHOR** URL: "+authorurl)
|
||||
authordata = self._fetchUrl(authorurl)
|
||||
descurl=authorurl
|
||||
authorsoup = bs.BeautifulSoup(authordata)
|
||||
# author can have several pages, scan until we find it.
|
||||
while( not authorsoup.find('a', href=re.compile(r"^/Story-"+self.story.getMetadata('storyId'))) ):
|
||||
nextpage = 'http://'+self.host+authorsoup.find('a', {'class':'arrowf'})['href']
|
||||
logger.debug("**AUTHOR** nextpage URL: "+nextpage)
|
||||
authordata = self._fetchUrl(nextpage)
|
||||
descurl=nextpage
|
||||
authorsoup = bs.BeautifulSoup(authordata)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
storydiv = authorsoup.find('div', {'id':'st'+self.story.getMetadata('storyId'), 'class':re.compile(r"storylistitem")})
|
||||
self.setDescription(descurl,storydiv.find('div',{'class':'storydesc'}))
|
||||
#self.story.setMetadata('description',stripHTML(storydiv.find('div',{'class':'storydesc'})))
|
||||
self.story.setMetadata('title',stripHTML(storydiv.find('a',{'class':'storylink'})))
|
||||
|
||||
verticaltable = soup.find('table', {'class':'verticaltable'})
|
||||
|
||||
BtVS = True
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -54,9 +54,14 @@ def get_urls_from_page(url,configuration=None,normalize=False):
|
||||
opener = u2.build_opener(u2.HTTPCookieProcessor(),GZipProcessor())
|
||||
data = opener.open(url).read()
|
||||
|
||||
return get_urls_from_html(data,url,configuration,normalize)
|
||||
# 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,normalize=False):
|
||||
return get_urls_from_html(data,url,configuration,normalize,restrictsearch)
|
||||
|
||||
def get_urls_from_html(data,url=None,configuration=None,normalize=False,restrictsearch=None):
|
||||
|
||||
normalized = [] # normalized url
|
||||
retlist = [] # orig urls.
|
||||
@@ -65,6 +70,9 @@ def get_urls_from_html(data,url=None,configuration=None,normalize=False):
|
||||
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'):
|
||||
@@ -105,9 +113,9 @@ def get_urls_from_text(data,configuration=None,normalize=False):
|
||||
# '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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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()))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)))))
|
||||
|
||||
+25
-19
@@ -53,13 +53,14 @@
|
||||
<p>Hi, {{ nickname }}! This is FanFictionDownLoader, which makes reading stories from various websites
|
||||
much easier. </p>
|
||||
</div>
|
||||
<!-- put announcements here, h3 is a good title size. -->
|
||||
<!-- put announcements here, h3 is a good title size.
|
||||
<h3>Changes:</h3>
|
||||
<p>
|
||||
<ul>
|
||||
<li>Yet another fix for fanfiction.net changes.</li>
|
||||
<li></li>
|
||||
</ul>
|
||||
</p>
|
||||
-->
|
||||
<p>
|
||||
Questions? Check out our
|
||||
<a href="http://code.google.com/p/fanficdownloader/wiki/FanFictionDownloaderFAQs">FAQs</a>.
|
||||
@@ -68,7 +69,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-52.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 }}
|
||||
@@ -378,11 +379,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
|
||||
@@ -471,11 +467,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
|
||||
@@ -552,11 +543,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
|
||||
@@ -607,11 +593,31 @@
|
||||
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>
|
||||
</dl>
|
||||
|
||||
<p>
|
||||
|
||||
+1426
-1337
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user