Compare commits

...
Author SHA1 Message Date
Jim Miller 0a2cff3469 Yet more fixes for poor parsing of numeric entities--this time, my poor parsing. 2012-04-25 21:52:56 -05:00
Jim Miller 3cb92f48cc Added tag FanFictionDownLoader-4.4.8 for changeset 8fc7a6cc2d87 2012-04-22 10:10:08 -05:00
Jim Miller e5cc4ebbd5 Bump Web/CLI version. 2012-04-22 10:09:44 -05:00
Jim Miller c6c67f99ca Moved tag calibre-plugin-1.5.15 to changeset a1a840bcd4c9 (from changeset bac4028b803d) 2012-04-22 09:55:58 -05:00
Jim Miller c75e62d07e Merge fimfiction changes to trunk. 2012-04-22 09:55:39 -05:00
Jim Miller e9b203eb57 Added tag calibre-plugin-1.5.15 for changeset bac4028b803d 2012-04-21 21:37:27 -05:00
Jim Miller a2eaa9fb22 Fix for ArchiveOfOurOwn site change. 2012-04-21 21:37:16 -05:00
althaine 0cf5ec9880 Updated fimfic.net adapter to use provided API 2012-04-20 20:36:20 +10:00
Jim Miller 465e25d3e6 Added tag FanFictionDownLoader-4.4.7 for changeset c8ab1f6bbd57 2012-04-19 12:19:06 -05:00
Jim Miller 0c85cf1d10 Web- Add list of all downloads feature to web service. 2012-04-19 12:18:40 -05:00
Jim Miller 7f7a1a983c Plugin-Fix setting status column with no status available(fictionalley). 2012-04-19 12:14:51 -05:00
Jim Miller 6f4be261ef Fix Japanese in CLI & web version. Broke it in 443 and didn't notice. 2012-04-18 17:54:42 -05:00
Jim Miller 04eb551d48 Added tag CLI-4.4.6 for changeset b69d6004ae25 2012-04-17 14:07:50 -05:00
Jim Miller 6299fc7582 Added tag calibre-plugin-1.5.14 for changeset b69d6004ae25 2012-04-17 14:07:29 -05:00
Jim Miller c2d7b4ecce Change plugin 'images' checkbox to add to [epubs], kludge for fimfiction images 2012-04-17 14:07:19 -05:00
Jim Miller 01b14701a0 Moved tag calibre-plugin-1.5.13 to changeset b55fabb0b8c9 (from changeset b0720fc31c10) 2012-04-11 11:11:43 -05:00
Jim Miller 08aa677855 Fix for Python GC'ing menu objects with no explicit Python references. 2012-04-11 11:11:25 -05:00
Jim Miller bd477d2696 Added tag FanFictionDownLoader-4.4.5 for changeset b0720fc31c10 2012-04-10 09:43:20 -05:00
Jim Miller 1f3220a221 Added tag calibre-plugin-1.5.13 for changeset b0720fc31c10 2012-04-10 09:42:32 -05:00
Jim Miller b4a8361df9 Package Ida's new archive.skyehawke.com adapter for release. 2012-04-10 09:42:01 -05:00
Ida e647b0e69b First version adapter for archive.skyehawke.com 2012-04-10 01:59:34 -04:00
Jim Miller a5fd8646b7 Added tag calibre-plugin-1.5.12 for changeset 6145294332cd 2012-04-09 11:35:25 -05:00
16 changed files with 457 additions and 121 deletions
+78
View File
@@ -0,0 +1,78 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html>
<head>
<link href="/css/index.css" rel="stylesheet" type="text/css">
<title>FanFictionDownLoader (fanfiction.net, fanficauthors, fictionalley, ficwad to epub and HTML)</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-12136939-1']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
</head>
<body>
<div id='main'>
<h1>
<a href="/" style="text-decoration: none; color: black;">FanFictionDownLoader</a>
</h1>
<script type="text/javascript"><!--
google_ad_client = "ca-pub-0320924304307555";
/* Standard */
google_ad_slot = "8974025478";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script>
<!-- <div id='yourfile'> -->
{{yourfile}}
<!-- </div> -->
<div id='helpbox'>
{% for fic in fics %}
<p>
<a href="{{ fic.url }}" title="Link to original story"><span class="recent"><i>{{ fic.title }}</i></span></a>
by <a href="{{ fic.authorUrl }}">{{ fic.author }}</a> <b>Download Count:</b> {{ fic.count }} <br />
<b>Word Count:</b> {{ fic.numWords }} <b>Chapter Count:</b> {{ fic.numChapters }}<br />
{% if fic.category %} <b>Categories:</b> {{ fic.category }} <br /> {% endif %}
{% if fic.genre %} <b>Genres:</b> {{ fic.genre }} <br /> {% endif %}
{% if fic.language %} <b>Language:</b> {{ fic.language }} <br /> {% endif %}
{% if fic.series %} <b>Series:</b> {{ fic.series }} <br /> {% endif %}
{% if fic.characters %} <b>Characters:</b> {{ fic.characters }} <br /> {% endif %}
{% if fic.status %} <b>Status:</b> {{ fic.status }} <br /> {% endif %}
{% if fic.datePublished %} <b>Published:</b> {{ fic.datePublished }} <br /> {% endif %}
{% if fic.dateUpdated %} <b>Last Updated:</b> {{ fic.dateUpdated }} <br /> {% endif %}
{% if fic.dateCreated %} <b>Last Downloaded:</b> {{ fic.dateCreated }} <br /> {% endif %}
{% if fic.rating %} <b>Rating:</b> {{ fic.rating }} <br /> {% endif %}
{% if fic.warnings %} <b>Warnings:</b> {{ fic.warnings }} <br /> {% endif %}
{% if fic.description %} <b>Summary:</b> {{ fic.description }} <br /> {% endif %}
</p>
{% endfor %}
</div>
<script type="text/javascript"><!--
google_ad_client = "ca-pub-0320924304307555";
/* Standard */
google_ad_slot = "8974025478";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script>
</div>
</body>
</html>
+3 -3
View File
@@ -1,6 +1,6 @@
# ffd-retief-hrd fanfictiondownloader
application: fanfictiondownloader
version: 4-4-4
version: 4-4-8
runtime: python27
api_version: 1
threadsafe: true
@@ -35,8 +35,8 @@ handlers:
- url: /.*
script: main.app
builtins:
- datastore_admin: on
#builtins:
#- datastore_admin: on
libraries:
- name: django
+1 -1
View File
@@ -27,7 +27,7 @@ class FanFictionDownLoaderBase(InterfaceActionBase):
description = 'UI plugin to download FanFiction stories from various sites.'
supported_platforms = ['windows', 'osx', 'linux']
author = 'Jim Miller'
version = (1, 5, 12)
version = (1, 5, 16)
minimum_calibre_version = (0, 8, 30)
#: This field defines the GUI plugin class that contains all the code
+1 -1
View File
@@ -283,7 +283,7 @@ class BasicTab(QWidget):
# this is a cheat to make it easier for users to realize there's a new include_images features.
self.includeimages = QCheckBox("Include images in EPUBs?",self)
self.includeimages.setToolTip("Download and include images in EPUB stories. This is equivalent to adding:\n\n[defaults]\ninclude_images:true\nkeep_summary_html:true\nmake_firstimage_cover:true\n\n ...to the top of personal.ini. Your settings in personal.ini will override this.")
self.includeimages.setToolTip("Download and include images in EPUB stories. This is equivalent to adding:\n\n[epub]\ninclude_images:true\nkeep_summary_html:true\nmake_firstimage_cover:true\n\n ...to the top of personal.ini. Your settings in personal.ini will override this.")
self.includeimages.setChecked(prefs['includeimages'])
self.l.addWidget(self.includeimages)
+8 -2
View File
@@ -109,6 +109,9 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
# Assign our menu to this action
self.menu = QMenu(self.gui)
self.old_actions_unique_map = {}
# menu_actions is just to keep a live reference to the menu
# items to prevent GC removing it.
self.menu_actions = []
self.qaction.setMenu(self.menu)
self.menu.aboutToShow.connect(self.about_to_show_menu)
@@ -135,6 +138,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
do_user_config = self.interface_action_base_plugin.do_user_config
self.menu.clear()
self.actions_unique_map = {}
self.menu_actions = []
self.add_action = self.create_menu_item_ex(self.menu, '&Add New from URL(s)', image='plus.png',
unique_name='Add New FanFiction Book(s) from URL(s)',
shortcut_name='Add New FanFiction Book(s) from URL(s)',
@@ -227,6 +231,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
ac = create_menu_action_unique(self, parent_menu, menu_text, image, tooltip,
shortcut, triggered, is_checked, shortcut_name, unique_name)
self.actions_unique_map[ac.calibre_shortcut_unique_name] = ac.calibre_shortcut_unique_name
self.menu_actions.append(ac)
return ac
def plugin_button(self):
@@ -415,7 +420,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
options['personal.ini'] = prefs['personal.ini']
if prefs['includeimages']:
# this is a cheat to make it easier for users.
options['personal.ini'] = '''[defaults]
options['personal.ini'] = '''[epub]
include_images:true
keep_summary_html:true
make_firstimage_cover:true
@@ -788,7 +793,8 @@ make_firstimage_cover:true
print("%s not an existing column, skipping."%col)
continue
coldef = custom_columns[col]
if not meta.startswith('status-') and meta not in book['all_metadata']:
if not meta.startswith('status-') and meta not in book['all_metadata'] or \
meta.startswith('status-') and 'status' not in book['all_metadata']:
print("No value for %s, skipping."%meta)
continue
if meta not in permitted_values[coldef['datatype']]:
+1
View File
@@ -51,6 +51,7 @@ import adapter_thequidditchpitchorg
import adapter_nfacommunitycom
import adapter_midnightwhispersca
import adapter_ksarchivecom
import adapter_archiveskyehawkecom
## This bit of complexity allows adapters to be added by just adding
## importing. It eliminates the long if/else clauses we used to need
@@ -101,7 +101,7 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
# Now go hunting for all the meta data and the chapter list.
## Title
a = soup.find('a', href=re.compile(r"^/works/\w+"))
a = soup.find('a', href=re.compile(r"^/works/\d+$"))
self.story.setMetadata('title',a.string)
# Find authorid and URL from... author url.
@@ -0,0 +1,190 @@
# -*- coding: utf-8 -*-
# Copyright 2011 Fanficdownloader team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import time
import logging
import re
import urllib2
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, makeDate
def getClass():
return ArchiveSkyeHawkeComAdapter
# Class name has to be unique. Our convention is camel case the
# sitename with Adapter at the end. www is skipped.
class ArchiveSkyeHawkeComAdapter(BaseSiteAdapter):
def __init__(self, config, url):
BaseSiteAdapter.__init__(self, config, url)
self.decode = ["Windows-1252",
"utf8"] # 1252 is a superset of iso-8859-1.
# Most sites that claim to be
# iso-8859-1 (and some that claim to be
# utf8) are really windows-1252.
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
self.password = ""
self.is_adult=False
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/story.php?no='+self.story.getMetadata('storyId'))
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','ash')
# 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 'archive.skyehawke.com'
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/story.php?no=1234"
def getSiteURLPattern(self):
return re.escape("http://"+self.getSiteDomain()+"/story.php?no=")+r"\d+$"
## Getting the chapter list and the meta data, plus 'is adult' checking.
def extractChapterUrlsAndMetadata(self):
url = self.url
logging.debug("URL: "+url)
try:
data = self._fetchUrl(url)
except urllib2.HTTPError, e:
if e.code == 404:
raise exceptions.StoryDoesNotExist(self.url)
else:
raise e
data = self._fetchUrl(url)
# use BeautifulSoup HTML parser to make everything easier to find.
soup = bs.BeautifulSoup(data)
# print data
# Now go hunting for all the meta data and the chapter list.
## Title
a = soup.find('div', {'class':"story border"}).find('span',{'class':'left'})
title=a.text.split('"')[1]
self.story.setMetadata('title',title)
# Find authorid and URL from... author url.
author = a.find('a')
self.story.setMetadata('authorId',author['href'].split('=')[1])
self.story.setMetadata('authorUrl','http://'+self.host+'/'+author['href'])
self.story.setMetadata('author',author.string)
authorSoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')))
chapter=soup.find('select',{'name':'chapter'}).findAll('option')
for i in range(1,len(chapter)):
ch=chapter[i]
self.chapterUrls.append((stripHTML(ch),ch['value']))
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.
box=soup.find('div', {'class': "container borderridge"})
sum=box.find('span').text
self.setDescription(url,sum)
boxes=soup.findAll('div', {'class': "container bordersolid"})
for box in boxes:
if box.find('b') != None and box.find('b').text == "History and Story Information":
for b in box.findAll('b'):
if "words" in b.nextSibling:
self.story.setMetadata('numWords', b.text)
if "archived" in b.previousSibling:
self.story.setMetadata('datePublished', makeDate(stripHTML(b.text), self.dateformat))
if "updated" in b.previousSibling:
self.story.setMetadata('dateUpdated', makeDate(stripHTML(b.text), self.dateformat))
if "fandom" in b.nextSibling:
self.story.addToList('category', b.text)
for br in box.findAll('br'):
br.replaceWith('split')
genre=box.text.split("Genre:")[1].split("split")[0]
if not "Unspecified" in genre:
self.story.addToList('genre',genre)
if box.find('span') != None and box.find('span').text == "WARNING":
rating=box.findAll('span')[1]
rating.find('br').replaceWith('split')
rating=rating.text.replace("This story is rated",'').split('split')[0]
self.story.setMetadata('rating',rating)
logging.debug(self.story.getMetadata('rating'))
warnings=box.find('ol')
if warnings != None:
warnings=warnings.text.replace(']', '').replace('[', '').split(' ')
for warning in warnings:
self.story.addToList('warnings',warning)
for asoup in authorSoup.findAll('div', {'class':"story bordersolid"}):
if asoup.find('a')['href'] == 'story.php?no='+self.story.getMetadata('storyId'):
if '[ Completed ]' in asoup.text:
self.story.setMetadata('status', 'Completed')
else:
self.story.setMetadata('status', 'In-Progress')
chars=asoup.findNext('div').text.split('Characters')[1].split(']')[0]
for char in chars.split(','):
if not "None" in char:
self.story.addToList('characters',char)
break
# grab the text for an individual chapter.
def getChapterText(self, url):
logging.debug('Getting chapter text from: %s' % url)
soup = bs.BeautifulStoneSoup(self._fetchUrl(url),
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
div = soup.find('div',{'class':"chapter bordersolid"}).findNext('div').findNext('div')
if None == div:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
return self.utf8FromSoup(url,div)
@@ -20,7 +20,8 @@ import logging
import re
import urllib2
import cookielib as cl
import datetime
from datetime import datetime
import json
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
@@ -36,7 +37,7 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
def __init__(self, config, url):
BaseSiteAdapter.__init__(self, config, url)
self.story.setMetadata('siteabbrev','fimficnet')
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
self.story.setMetadata('storyId', self.parsedUrl.path.split('/',)[2])
self._setURL("http://"+self.getSiteDomain()+"/story/"+self.story.getMetadata('storyId')+"/")
self.is_adult = False
@@ -74,6 +75,10 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
self.opener = urllib2.build_opener(cookieproc)
try:
apiResponse = urllib2.urlopen("http://www.fimfiction.net/api/story.php?story=%s" % (self.story.getMetadata("storyId"))).read()
apiData = json.loads(apiResponse)
# Unfortunately, we still need to load the story index page to parse the characters
data = self._fetchUrl(self.url)
except urllib2.HTTPError, e:
if e.code == 404:
@@ -87,9 +92,12 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
if "/images/missing_story.png" in data:
raise exceptions.StoryDoesNotExist(self.url)
if "Invalid story id" in apiData.values():
raise exceptions.StoryDoesNotExist(self.url)
if "This story has been marked as having adult content." in data:
raise exceptions.AdultCheckRequired(self.url)
if self.password:
params = {}
params['password'] = self.password
@@ -100,95 +108,56 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
raise exceptions.FailedToDownload("%s requires story password and fail_on_password is true."%self.url)
else:
raise exceptions.FailedToLogin(self.url,"Story requires individual password",passwdonly=True)
storyMetadata = apiData["story"]
self.story.setMetadata("title", storyMetadata["title"])
self.story.setMetadata("author", storyMetadata["author"]["name"])
self.story.setMetadata("authorId", storyMetadata["author"]["id"])
self.story.setMetadata("authorUrl", "http://%s/user/%s" % (self.getSiteDomain(), storyMetadata["author"]["name"]))
soup = bs.BeautifulSoup(data).find("div", {"class":"content_box post_content_box"})
titleheader = soup.find("h2")
title = titleheader.find("a", href=re.compile(r'^/story/')).text
author = titleheader.find("a", href=re.compile(r'^/user/')).text
self.story.setMetadata("title", title)
self.story.setMetadata("author", author)
self.story.setMetadata("authorId", author) # The author's name will be unique
self.story.setMetadata("authorUrl", "http://%s/user/%s" % (self.getSiteDomain(),author))
chapters = [{"chapterTitle": chapter["title"], "chapterURL": chapter["link"]} for chapter in storyMetadata["chapters"]]
for chapter in chapters:
self.chapterUrls.append((chapter["chapterTitle"], chapter["chapterURL"]))
self.story.setMetadata("numChapters", len(self.chapterUrls))
chapterDates = []
for chapter in soup.findAll("a", {"class":"chapter_link"}):
chapterDates.append(chapter.span.extract().text.strip("()"))
self.chapterUrls.append((chapter.text.strip(), "http://"+self.getSiteDomain() + chapter['href']))
self.story.setMetadata('numChapters',len(self.chapterUrls))
for character in [character_icon['title'] for character_icon in soup.findAll("a", {"class":"character_icon"})]:
self.story.addToList("characters", character)
for category in [category.text for category in soup.find("div", {"class":"categories"}).findAll("a")]:
self.story.addToList("genre", category)
self.story.addToList("category", "My Little Pony")
# The very last list element in the list of chapters contains the status, rating and word count e.g.:
#
# <li>
# Incomplete | Rating:
# <span style="color:#c78238;">Teen</span>
# <div class="word_count"><b>5,203</b>words total</div>
# </li>
#
status_bar = soup.findAll('li')[-1]
# In the case of fimfiction.net, possible statuses are 'Completed', 'Incomplete', 'On Hiatus' and 'Cancelled'
# For the sake of bringing it in line with the other adapters, 'Incomplete' and 'On Hiatus' become 'In-Progress'
# and 'Complete' beomes 'Completed'. 'Cancelled' seems an important enough (not to mention more strictly true)
# status to leave unchanged.
status = status_bar.text.split("|")[0].strip().replace("Incomplete", "In-Progress").replace("On Hiatus", "In-Progress").replace("Complete", "Completed")
self.story.setMetadata('status', status)
self.story.setMetadata('rating', status_bar.span.text)
# This way is less elegant, perhaps, but more robust in face of format changes.
numWords = status_bar.find("div",{"class":"word_count"}).b.text
self.story.setMetadata('numWords', numWords)
description_soup = soup.find("div", {"class":"description"})
# Sometimes the description has an expanding element
# This removes the ellipsis and the expand button
try:
description_soup.find('span', {"id":re.compile(r"description_more_elipses_\d+")}).extract() # Web designer can't spell 'ellipsis'
description_soup.find('a', {"class":"more"}).extract()
except:
pass
status = storyMetadata["status"].replace("Incomplete", "In-Progress").replace("On Hiatus", "In-Progress").replace("Complete", "Completed")
self.story.setMetadata("status", status)
self.story.setMetadata("rating", storyMetadata["content_rating_text"])
for category in storyMetadata["categories"]:
if storyMetadata["categories"][category]:
self.story.addToList("genre", category)
self.story.addToList("category", "My Little Pony")
self.story.setMetadata("numWords", str(storyMetadata["words"]))
# fimfic is the first site with an explicit cover image.
story_img = soup.find('img',{'class':'story_image'})
if self.getConfig('include_images') and story_img:
self.story.addImgUrl(self,self.url,story_img['src'],self._fetchUrlRaw,cover=True)
self.setDescription(self.url,description_soup.text)
#self.story.setMetadata('description', description_soup.text)
if self.getConfig('include_images') and "image" in storyMetadata.keys():
coverurl = storyMetadata["image"]
if coverurl.startswith('//static.fimfiction.net'): # fix for img urls missing 'http:'
coverurl = "http:"+coverurl
self.story.addImgUrl(self,self.url,coverurl,self._fetchUrlRaw,cover=True)
self.setDescription(self.url, storyMetadata["description"])
# Unfortunately, nowhere on the page is the year mentioned.
# Best effort to deal with this:
# Use this year, if that's a date in the future, subtract one year.
# Their earliest story is Jun, so they'll probably change the date
# around then.
now = datetime.datetime.now()
# Dates are in Unix time
# Take the publish date from the first chapter posted
rawDatePublished = storyMetadata["chapters"][0]["date_modified"]
self.story.setMetadata("datePublished", datetime.fromtimestamp(rawDatePublished))
rawDateUpdated = storyMetadata["date_modified"]
self.story.setMetadata("dateUpdated", datetime.fromtimestamp(rawDateUpdated))
dateUpdated_soup = bs.BeautifulSoup(data).find("div", {"class":"calendar"})
dateUpdated_soup.find('span').extract()
dateUpdated = makeDate("%s%s"%(now.year,dateUpdated_soup.text), "%Y%b%d")
if dateUpdated > now :
dateUpdated = dateUpdated.replace(year=now.year-1)
self.story.setMetadata("dateUpdated", dateUpdated)
# Get the date of creation from the first chapter
if len(chapterDates) > 0:
datePublished_text = chapterDates[0]
day, month = datePublished_text.split()
day = re.sub(r"[^\d.]+", '', day)
datePublished = makeDate("%s%s%s"%(now.year,month,day), "%Y%b%d")
if datePublished > now :
datePublished = datePublished.replace(year=now.year-1)
self.story.setMetadata("datePublished", datePublished)
else:
self.story.setMetadata("datePublished", dateUpdated)
soup = bs.BeautifulSoup(data).find("div", {"class":"story"})
for character in [character_icon["title"] for character_icon in soup.findAll("a", {"class":"character_icon"})]:
self.story.addToList("characters", character)
def getChapterText(self, url):
logging.debug('Getting chapter text from: %s' % url)
+3 -2
View File
@@ -179,11 +179,12 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
<div>
<h3>Chapter title from site</h3>
<p><center>Centered text</center></p>
<p>Lorem '''+self.crazystring+''' <i>italics</i>, <b>bold</b>, <u>underline</u> consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem '''+self.crazystring+u''' <i>italics</i>, <b>bold</b>, <u>underline</u> consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
br breaks<br><br>
Puella Magi Madoka Magica/魔法少女まどか★マギカ
<a href="http://code.google.com/p/fanficdownloader/wiki/FanFictionDownLoaderPluginWithReadingList" title="Tilt-a-Whirl by Jim &amp; Sarah, on Flickr"><img src="http://i.imgur.com/bo8eD.png"></a><br/>
br breaks<br><br>
Don't&#8212e;ver&#8212d;o&#8212;that&#8212a;gain, &#27861; &#xE9;
<hr>
horizontal rules
<hr size=1 noshade>
+15 -4
View File
@@ -19,12 +19,21 @@ import re
def _unirepl(match):
"Return the unicode string for a decimal number"
if match.group(1)=='x':
if match.group(1).startswith('x'):
radix=16
s = match.group(1)[1:]
else:
radix=10
value = int(match.group(2), radix)
return "%s%s"%(unichr(value),match.group(3))
s = match.group(1)
try:
value = int(s, radix)
retval = "%s%s"%(unichr(value),match.group(2))
except:
# This way, at least if there's more of entities out there
# that fail, it doesn't blow the entire download.
print "Numeric entity translation failed, skipping: &#x%s%s"%(match.group(1),match.group(2))
retval = ""
return retval
def _replaceNumberEntities(data):
# The same brokenish entity parsing in SGMLParser that inserts ';'
@@ -32,7 +41,9 @@ def _replaceNumberEntities(data):
# entities, including part of the next word if it's a-z.
# "Don't&#8212ever&#8212do&#8212that&#8212again," becomes
# "Don't&#8212e;ver&#8212d;o&#8212;that&#8212a;gain,"
p = re.compile(r'&#(x?)([0-9a-fA-F]{,4})([0-9a-fA-F]*);')
# Also need to allow for 5 digit decimal entities &#27861;
# Last expression didn't allow for 2 digit hex correctly: &#xE9;
p = re.compile(r'&#(x[0-9a-fA-F]{,4}|[0-9]{,5})([0-9a-fA-F]*?);')
return p.sub(_unirepl, data)
def _replaceNotEntities(data):
+6 -5
View File
@@ -218,7 +218,7 @@ class Story:
value = re.sub(p,v,value)
return value
def getMetadata(self, key, removeallentities=False):
def getMetadata(self, key, removeallentities=False, doreplacements=True):
value = None
if self.getLists().has_key(key):
value = ', '.join(self.getList(key))
@@ -232,21 +232,22 @@ class Story:
if key == "datePublished" or key == "dateUpdated":
value = value.strftime("%Y-%m-%d")
value=self.doReplacments(value)
if doreplacements:
value=self.doReplacments(value)
if removeallentities and value != None:
return removeAllEntities(value)
else:
return value
def getAllMetadata(self, removeallentities=False):
def getAllMetadata(self, removeallentities=False, doreplacements=True):
'''
All single value *and* list value metadata as strings.
'''
allmetadata = {}
for k in self.metadata.keys():
allmetadata[k] = self.getMetadata(k, removeallentities)
allmetadata[k] = self.getMetadata(k, removeallentities, doreplacements)
for l in self.listables.keys():
allmetadata[l] = self.getMetadata(l, removeallentities)
allmetadata[l] = self.getMetadata(l, removeallentities, doreplacements)
return allmetadata
+24
View File
@@ -13,8 +13,23 @@
# limitations under the License.
#
import pickle, copy
from google.appengine.ext import db
class ObjectProperty(db.Property):
data_type = db.Blob
def get_value_for_datastore(self, model_instance):
value = self.__get__(model_instance, model_instance.__class__)
pickled_val = pickle.dumps(value,protocol=pickle.HIGHEST_PROTOCOL)
if value is not None: return db.Blob(pickled_val)
def make_value_from_datastore(self, value):
if value is not None: return pickle.loads(value)
def default_value(self):
return copy.copy(self.default)
class DownloadMeta(db.Model):
user = db.UserProperty()
url = db.StringProperty()
@@ -37,3 +52,12 @@ class DownloadData(db.Model):
class UserConfig(db.Model):
user = db.UserProperty()
config = db.BlobProperty()
class SavedMeta(db.Model):
url = db.StringProperty()
title = db.StringProperty()
author = db.StringProperty()
date = db.DateTimeProperty(auto_now_add=True)
count = db.IntegerProperty()
meta = ObjectProperty()
+21 -12
View File
@@ -54,19 +54,24 @@
much easier. </p>
</div>
<!-- put announcements here, h3 is a good title size. -->
<h3>New Site Added</h3>
<h3>Latest Changes</h3>
<p>
Support for ksarchive.com has been added. Thanks for Jade AislinSam implementing this.
Fix for 'Hide this banner' title issue on Archive of Our
Own.
</p>
<p>
Questions? Check out our new
<a href="http://code.google.com/p/fanficdownloader/wiki/FanFictionDownloaderFAQs">FAQs</a>. Thanks to Wyndham for writing these.
Changed implementation of fimfiction to use provided API,
thanks to Althaine.
</p>
<p>
Questions? Check out our
<a href="http://code.google.com/p/fanficdownloader/wiki/FanFictionDownloaderFAQs">FAQs</a>.
</p>
<p>
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-3.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
<a href="http://4-4-7.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
</p>
<div id='error'>
{{ error_message }}
@@ -96,6 +101,9 @@
<p>
Or see your personal list of <a href="/recent">previously downloaded fanfics</a>.
</p>
<p>
See a list of downloaded fanfics by all users by <a href="/allrecent">most popular</a> or <a href="/allrecent?bydate=1">most recent</a>.
</p>
</div>
</form>
{% else %}
@@ -248,10 +256,6 @@
<br /><a href="http://ficbook.net/readfic/93626">http://ficbook.net/readfic/93626</a>.
<br /><a href="http://ficbook.net/readfic/93626/246417#part_content">http://ficbook.net/readfic/93626/246417#part_content</a>.
</dd>
<dt>gayauthors.org</dt>
<dd>
Removed following complaints by the site administration.
</dd>
<dt>fanfiction.mugglenet.com</dt>
<dd>
Use the URL of the story's chapter list, such as
@@ -287,18 +291,23 @@
Use the URL of the story's chapter list, such as
<br /><a href="http://ksarchive.com/viewstory.php?sid=1124">http://ksarchive.com/viewstory.php?sid=1124</a>.
</dd>
<dt>archive.skyehawke.com</dt>
<dd>
Use the URL of the story's summary, such as
<br /><a href="http://archive.skyehawke.com/story.php?no=17466">http://archive.skyehawke.com/story.php?no=17466</a>.
</dd>
</dl>
<p>
A few additional things to know, which will make your life substantially easier:
</p>
<ol>
<li>
First thing to know: I do not use your Google login and password. In fact, all I know about it is your ID &ndash; password
First thing to know: We do not use your Google login and password. In fact, all we know about it is your ID &ndash; password
is being verified by Google and is absolutely, totally unknown to anyone but you.
</li>
<li>
Small <a href="http://www.sigizmund.com/reading-fanfiction-off-line-in-stanza-and-oth">post written by me</a>
Small <a href="http://www.sigizmund.com/reading-fanfiction-off-line-in-stanza-and-oth">post written by Roman</a>
&mdash; how to read fiction in Stanza or any other ebook reader.
</li>
<li>
@@ -314,7 +323,7 @@
</li>
<li>
If you think that something that should work in fact doesn't, post a message to
our <a href="http://groups.google.com/group/fanfic-downloader">Google Group</a>. I also encourage you to join it so
our <a href="http://groups.google.com/group/fanfic-downloader">Google Group</a>. we also encourage you to join it so
you will find out about latest updates and fixes as soon as possible
</li>
</ol>
+3 -8
View File
@@ -1,6 +1,6 @@
indexes:
# AUTOGENERATED
# notAUTOGENERATED
# This index.yaml is automatically updated whenever the dev_appserver
# detects that a new type of query is run. If you want to manage the
@@ -21,13 +21,8 @@ indexes:
- name: date
direction: desc
- kind: DownloadedFanfic
- kind: SavedMeta
properties:
- name: cleared
- name: date
- kind: DownloadedFanfic
properties:
- name: user
- name: count
- name: date
direction: desc
+51
View File
@@ -303,6 +303,39 @@ class RecentFilesServer(webapp2.RequestHandler):
path = os.path.join(os.path.dirname(__file__), 'recent.html')
self.response.out.write(template.render(path, template_values))
class AllRecentFilesServer(webapp2.RequestHandler):
def get(self):
user = users.get_current_user()
if not user:
self.redirect(users.create_login_url(self.request.uri))
return
q = SavedMeta.all()
if self.request.get('bydate'):
q.order('-date')
else:
q.order('-count')
fics = q.fetch(200)
logging.info("Recent fetched %d downloads for user %s."%(len(fics),user.nickname()))
sendslugs = []
for fic in fics:
ficslug = FicSlug(fic)
sendslugs.append(ficslug)
template_values = dict(fics = sendslugs, nickname = user.nickname())
path = os.path.join(os.path.dirname(__file__), 'allrecent.html')
self.response.out.write(template.render(path, template_values))
class FicSlug():
def __init__(self,savedmeta):
self.url = savedmeta.url
self.count = savedmeta.count
for k, v in savedmeta.meta.iteritems():
setattr(self,k,v)
class FanfictionDownloader(UserConfigServer):
def get(self):
self.post()
@@ -464,6 +497,8 @@ class FanfictionDownloaderTask(UserConfigServer):
download.url = adapter.getStory().getMetadata('storyUrl')
download.put()
allmeta = adapter.getStory().getAllMetadata(removeallentities=True,doreplacements=False)
outbuffer = StringIO()
writer.writeStory(outbuffer)
data = outbuffer.getvalue()
@@ -495,6 +530,21 @@ class FanfictionDownloaderTask(UserConfigServer):
download.completed=True
download.put()
smetal = SavedMeta.all().filter('url =', allmeta['storyUrl'] ).fetch(1)
if smetal and smetal[0]:
smeta = smetal[0]
smeta.count += 1
else:
smeta=SavedMeta()
smeta.count = 1
smeta.url = allmeta['storyUrl']
smeta.title = allmeta['title']
smeta.author = allmeta['author']
smeta.meta = allmeta
smeta.date = datetime.datetime.now()
smeta.put()
logging.info("Download finished OK")
del data
@@ -563,6 +613,7 @@ app = webapp2.WSGIApplication([('/', MainHandler),
('/fdown', FanfictionDownloader),
(r'/file.*', FileServer),
('/status', FileStatusServer),
('/allrecent', AllRecentFilesServer),
('/recent', RecentFilesServer),
('/editconfig', EditConfigServer),
('/clearrecent', ClearRecentServer),