mirror of
https://github.com/JimmXinu/FanFicFare.git
synced 2025-12-06 08:52:55 +01:00
4-0-1 Add mobi back in, changes to recent&status appearance, more reliability.
This commit is contained in:
parent
f7857ad6dc
commit
50b80b6d68
12 changed files with 803 additions and 64 deletions
2
app.yaml
2
app.yaml
|
|
@ -1,6 +1,6 @@
|
||||||
# fanfictionloader ffd-retief
|
# fanfictionloader ffd-retief
|
||||||
application: fanfictionloader
|
application: fanfictionloader
|
||||||
version: 4-0-0
|
version: 4-0-1
|
||||||
runtime: python
|
runtime: python
|
||||||
api_version: 1
|
api_version: 1
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ body
|
||||||
|
|
||||||
#greeting
|
#greeting
|
||||||
{
|
{
|
||||||
margin-bottom: 1em;
|
# margin-bottom: 1em;
|
||||||
border-color: #efefef;
|
border-color: #efefef;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,6 +66,8 @@ div.field
|
||||||
|
|
||||||
#error
|
#error
|
||||||
{
|
{
|
||||||
font-size: small;
|
|
||||||
color: #f00;
|
color: #f00;
|
||||||
}
|
}
|
||||||
|
.recent {
|
||||||
|
font-size: large;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ extratags: FanFiction
|
||||||
|
|
||||||
[txt]
|
[txt]
|
||||||
## Add URLs since there aren't links.
|
## Add URLs since there aren't links.
|
||||||
titlepage_entries: category,genre,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,numWords,site,storyUrl, author URL, description
|
titlepage_entries: category,genre,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,numWords,site,storyUrl, authorUrl, description
|
||||||
|
|
||||||
## use \r\n for line endings, the windows convention. text output only.
|
## use \r\n for line endings, the windows convention. text output only.
|
||||||
windows_eol: true
|
windows_eol: true
|
||||||
|
|
@ -132,6 +132,10 @@ titlepage_use_table: false
|
||||||
## When using tables, make these span both columns.
|
## When using tables, make these span both columns.
|
||||||
wide_titlepage_entries: description, storyUrl, author URL
|
wide_titlepage_entries: description, storyUrl, author URL
|
||||||
|
|
||||||
|
[mobi]
|
||||||
|
## mobi TOC cannot be turned off right now.
|
||||||
|
#include_tocpage: true
|
||||||
|
|
||||||
|
|
||||||
## Each site has a section that overrides [defaults] *and* the format
|
## Each site has a section that overrides [defaults] *and* the format
|
||||||
## sections test1.com specifically is not a real story site. Instead,
|
## sections test1.com specifically is not a real story site. Instead,
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import time
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import fanficdownloader.BeautifulSoup as bs
|
import fanficdownloader.BeautifulSoup as bs
|
||||||
|
|
@ -105,6 +106,10 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
|
||||||
if self.story.getMetadata('storyId') == '667':
|
if self.story.getMetadata('storyId') == '667':
|
||||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s!" % url)
|
raise exceptions.FailedToDownload("Error downloading Chapter: %s!" % url)
|
||||||
|
|
||||||
|
if self.story.getMetadata('storyId') == '670' and self.getConfig('slow_down_sleep_time'):
|
||||||
|
time.sleep(float(self.getConfig('slow_down_sleep_time')))
|
||||||
|
|
||||||
|
|
||||||
if "chapter=1" in url :
|
if "chapter=1" in url :
|
||||||
text=u'''
|
text=u'''
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -116,6 +121,7 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
|
||||||
<p>http://test1.com?sid=667 - raises FailedToDownload on chapter 1</p>
|
<p>http://test1.com?sid=667 - raises FailedToDownload on chapter 1</p>
|
||||||
<p>http://test1.com?sid=668 - raises FailedToLogin unless username='Me'</p>
|
<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=669 - Succeeds with Updated Date=now</p>
|
||||||
|
<p>http://test1.com?sid=670 - Succeeds, but applies slow_down_sleep_time</p>
|
||||||
<p>And other storyId will succeed with the same output.</p>
|
<p>And other storyId will succeed with the same output.</p>
|
||||||
</div>
|
</div>
|
||||||
'''
|
'''
|
||||||
|
|
|
||||||
126
fanficdownloader/html.py
Normal file
126
fanficdownloader/html.py
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# Copyright(c) 2009 Andrew Chatham and Vijay Pandurangan
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import StringIO
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
from BeautifulSoup import BeautifulSoup
|
||||||
|
|
||||||
|
class HtmlProcessor:
|
||||||
|
WHITESPACE_RE = re.compile(r'\s')
|
||||||
|
# Look for </blockquote <p>
|
||||||
|
BAD_TAG_RE = re.compile(r'<[^>]+<', re.MULTILINE)
|
||||||
|
|
||||||
|
def __init__(self, html, unfill=0):
|
||||||
|
self.unfill = unfill
|
||||||
|
html = self._ProcessRawHtml(html)
|
||||||
|
self._soup = BeautifulSoup(html)
|
||||||
|
if self._soup.title:
|
||||||
|
self.title = self._soup.title.contents[0]
|
||||||
|
else:
|
||||||
|
self.title = None
|
||||||
|
|
||||||
|
def _ProcessRawHtml(self, html):
|
||||||
|
new_html, count = HtmlProcessor.BAD_TAG_RE.subn('<', html)
|
||||||
|
if count:
|
||||||
|
print >>sys.stderr, 'Replaced %d bad tags' % count
|
||||||
|
return new_html
|
||||||
|
|
||||||
|
def _StubInternalAnchors(self):
|
||||||
|
'''Replace each internal anchor with a fixed-size filepos anchor.
|
||||||
|
|
||||||
|
Looks for every anchor with <a href="#myanchor"> and replaces that
|
||||||
|
with <a filepos="00000000050">. Stores anchors in self._anchor_references'''
|
||||||
|
self._anchor_references = []
|
||||||
|
anchor_num = 0
|
||||||
|
# anchor links
|
||||||
|
anchorlist = self._soup.findAll('a', href=re.compile('^#'))
|
||||||
|
# treat reference tags like a tags for TOCTOP.
|
||||||
|
anchorlist.extend(self._soup.findAll('reference', href=re.compile('^#')))
|
||||||
|
for anchor in anchorlist:
|
||||||
|
self._anchor_references.append((anchor_num, anchor['href']))
|
||||||
|
del anchor['href']
|
||||||
|
anchor['filepos'] = '%.10d' % anchor_num
|
||||||
|
anchor_num += 1
|
||||||
|
|
||||||
|
def _ReplaceAnchorStubs(self):
|
||||||
|
# TODO: Browsers allow extra whitespace in the href names.
|
||||||
|
# use __str__ instead of prettify--it inserts extra spaces.
|
||||||
|
assembled_text = self._soup.__str__('utf8')
|
||||||
|
del self._soup # shouldn't touch this anymore
|
||||||
|
for anchor_num, original_ref in self._anchor_references:
|
||||||
|
ref = urllib.unquote(original_ref[1:]) # remove leading '#'
|
||||||
|
# Find the position of ref in the utf-8 document.
|
||||||
|
# TODO(chatham): Using regexes and looking for name= would be better.
|
||||||
|
newpos = assembled_text.rfind(ref.encode('utf-8'))
|
||||||
|
if newpos == -1:
|
||||||
|
print >>sys.stderr, 'Could not find anchor "%s"' % original_ref
|
||||||
|
continue
|
||||||
|
newpos += len(ref) + 2 # don't point into the middle of the <a name> tag
|
||||||
|
old_filepos = 'filepos="%.10d"' % anchor_num
|
||||||
|
new_filepos = 'filepos="%.10d"' % newpos
|
||||||
|
assert assembled_text.find(old_filepos) != -1
|
||||||
|
assembled_text = assembled_text.replace(old_filepos, new_filepos, 1)
|
||||||
|
return assembled_text
|
||||||
|
|
||||||
|
def _FixPreTags(self):
|
||||||
|
'''Replace <pre> tags with HTML-ified text.'''
|
||||||
|
pres = self._soup.findAll('pre')
|
||||||
|
for pre in pres:
|
||||||
|
pre.replaceWith(self._FixPreContents(str(pre.contents[0])))
|
||||||
|
|
||||||
|
def _FixPreContents(self, text):
|
||||||
|
if self.unfill:
|
||||||
|
line_splitter = '\n\n'
|
||||||
|
line_joiner = '<p>'
|
||||||
|
else:
|
||||||
|
line_splitter = '\n'
|
||||||
|
line_joiner = '<br>'
|
||||||
|
lines = []
|
||||||
|
for line in text.split(line_splitter):
|
||||||
|
lines.append(self.WHITESPACE_RE.subn(' ', line)[0])
|
||||||
|
return line_joiner.join(lines)
|
||||||
|
|
||||||
|
def _RemoveUnsupported(self):
|
||||||
|
'''Remove any tags which the kindle cannot handle.'''
|
||||||
|
# TODO(chatham): <link> tags to script?
|
||||||
|
unsupported_tags = ('script', 'style')
|
||||||
|
for tag_type in unsupported_tags:
|
||||||
|
for element in self._soup.findAll(tag_type):
|
||||||
|
element.extract()
|
||||||
|
|
||||||
|
def RenameAnchors(self, prefix):
|
||||||
|
'''Rename every internal anchor to have the given prefix, then
|
||||||
|
return the contents of the body tag.'''
|
||||||
|
for anchor in self._soup.findAll('a', href=re.compile('^#')):
|
||||||
|
anchor['href'] = '#' + prefix + anchor['href'][1:]
|
||||||
|
for a in self._soup.findAll('a'):
|
||||||
|
if a.get('name'):
|
||||||
|
a['name'] = prefix + a['name']
|
||||||
|
|
||||||
|
# TODO(chatham): figure out how to fix this. sometimes body comes out
|
||||||
|
# as NoneType.
|
||||||
|
content = []
|
||||||
|
if self._soup.body is not None:
|
||||||
|
content = [unicode(c) for c in self._soup.body.contents]
|
||||||
|
return '\n'.join(content)
|
||||||
|
|
||||||
|
def CleanHtml(self):
|
||||||
|
# TODO(chatham): fix_html_br, fix_html
|
||||||
|
self._RemoveUnsupported()
|
||||||
|
self._StubInternalAnchors()
|
||||||
|
self._FixPreTags()
|
||||||
|
return self._ReplaceAnchorStubs()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
FILE ='/tmp/documentation.html'
|
||||||
|
#FILE = '/tmp/multipre.html'
|
||||||
|
FILE = '/tmp/view.html'
|
||||||
|
import codecs
|
||||||
|
d = open(FILE).read()
|
||||||
|
h = HtmlProcessor(d)
|
||||||
|
s = h.CleanHtml()
|
||||||
|
#print s
|
||||||
384
fanficdownloader/mobi.py
Normal file
384
fanficdownloader/mobi.py
Normal file
|
|
@ -0,0 +1,384 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# Copyright(c) 2009 Andrew Chatham and Vijay Pandurangan
|
||||||
|
|
||||||
|
|
||||||
|
import StringIO
|
||||||
|
import struct
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from html import HtmlProcessor
|
||||||
|
|
||||||
|
# http://wiki.mobileread.com/wiki/MOBI
|
||||||
|
# http://membres.lycos.fr/microfirst/palm/pdb.html
|
||||||
|
|
||||||
|
encoding = {
|
||||||
|
'UTF-8' : 65001,
|
||||||
|
'latin-1' : 1252,
|
||||||
|
}
|
||||||
|
|
||||||
|
languages = {"en-us" : 0x0409,
|
||||||
|
"sv" : 0x041d,
|
||||||
|
"fi" : 0x000b,
|
||||||
|
"en" : 0x0009,
|
||||||
|
"en-gb" : 0x0809}
|
||||||
|
|
||||||
|
def ToHex(s):
|
||||||
|
v = ['%.2x' % ord(c) for c in s]
|
||||||
|
return ' '.join(v)
|
||||||
|
|
||||||
|
class _SubEntry:
|
||||||
|
def __init__(self, pos, html_data):
|
||||||
|
self.pos = pos
|
||||||
|
self.html = HtmlProcessor(html_data)
|
||||||
|
self.title = self.html.title
|
||||||
|
self._name = 'mobi_article_%d' % pos
|
||||||
|
if not self.title:
|
||||||
|
self.title = 'Article %d' % self.pos
|
||||||
|
|
||||||
|
def TocLink(self):
|
||||||
|
return '<a href="#%s_MOBI_START">%.80s</a>' % (self._name, self.title)
|
||||||
|
|
||||||
|
def Anchor(self):
|
||||||
|
return '<a name="%s_MOBI_START">' % self._name
|
||||||
|
|
||||||
|
def Body(self):
|
||||||
|
return self.html.RenameAnchors(self._name + '_')
|
||||||
|
|
||||||
|
class Converter:
|
||||||
|
def __init__(self, refresh_url='', title='Unknown', author='Unknown', publisher='Unknown'):
|
||||||
|
self._header = Header()
|
||||||
|
self._header.SetTitle(title)
|
||||||
|
self._header.SetAuthor(author)
|
||||||
|
self._header.SetPublisher(publisher)
|
||||||
|
self._refresh_url = refresh_url
|
||||||
|
|
||||||
|
def ConvertString(self, s):
|
||||||
|
out = StringIO.StringIO()
|
||||||
|
self._ConvertStringToFile(s, out)
|
||||||
|
return out.getvalue()
|
||||||
|
|
||||||
|
def ConvertStrings(self, html_strs):
|
||||||
|
out = StringIO.StringIO()
|
||||||
|
self._ConvertStringsToFile(html_strs, out)
|
||||||
|
return out.getvalue()
|
||||||
|
|
||||||
|
def ConvertFile(self, html_file, out_file):
|
||||||
|
self._ConvertStringToFile(open(html_file,'rb').read(),
|
||||||
|
open(out_file, 'wb'))
|
||||||
|
|
||||||
|
def ConvertFiles(self, html_files, out_file):
|
||||||
|
html_strs = [open(f,'rb').read() for f in html_files]
|
||||||
|
self._ConvertStringsToFile(html_strs, open(out_file, 'wb'))
|
||||||
|
|
||||||
|
def MakeOneHTML(self, html_strs):
|
||||||
|
"""This takes a list of HTML strings and returns a big HTML file with
|
||||||
|
all contents consolidated. It constructs a table of contents and adds
|
||||||
|
anchors within the text
|
||||||
|
"""
|
||||||
|
title_html = []
|
||||||
|
toc_html = []
|
||||||
|
body_html = []
|
||||||
|
|
||||||
|
PAGE_BREAK = '<mbp:pagebreak>'
|
||||||
|
|
||||||
|
# pull out the title page, assumed first html_strs.
|
||||||
|
htmltitle = html_strs[0]
|
||||||
|
entrytitle = _SubEntry(1, htmltitle)
|
||||||
|
title_html.append(entrytitle.Body())
|
||||||
|
|
||||||
|
title_html.append(PAGE_BREAK)
|
||||||
|
toc_html.append('<a name="TOCTOP"><h3>Table of Contents</h3><br />')
|
||||||
|
|
||||||
|
for pos, html in enumerate(html_strs[1:]):
|
||||||
|
entry = _SubEntry(pos+1, html)
|
||||||
|
toc_html.append('%s<br />' % entry.TocLink())
|
||||||
|
|
||||||
|
# give some space between bodies of work.
|
||||||
|
body_html.append(PAGE_BREAK)
|
||||||
|
|
||||||
|
body_html.append(entry.Anchor())
|
||||||
|
|
||||||
|
body_html.append(entry.Body())
|
||||||
|
|
||||||
|
# TODO: this title can get way too long with RSS feeds. Not sure how to fix
|
||||||
|
# cheat slightly and use the <a href> code to set filepos in references.
|
||||||
|
header = '''<html>
|
||||||
|
<head>
|
||||||
|
<title>Bibliorize %s GMT</title>
|
||||||
|
<guide>
|
||||||
|
<reference href="#TOCTOP" type="toc" title="Table of Contents"/>
|
||||||
|
</guide>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
''' % time.ctime(time.time())
|
||||||
|
|
||||||
|
footer = '</body></html>'
|
||||||
|
all_html = header + '\n'.join(title_html + toc_html + body_html) + footer
|
||||||
|
#print "%s" % all_html.encode('utf8')
|
||||||
|
return all_html
|
||||||
|
|
||||||
|
def _ConvertStringsToFile(self, html_strs, out_file):
|
||||||
|
try:
|
||||||
|
tmp = self.MakeOneHTML(html_strs)
|
||||||
|
self._ConvertStringToFile(tmp, out_file)
|
||||||
|
except Exception, e:
|
||||||
|
logging.error('Error %s', e)
|
||||||
|
logging.debug('Details: %s' % html_strs)
|
||||||
|
|
||||||
|
def _ConvertStringToFile(self, html_data, out):
|
||||||
|
html = HtmlProcessor(html_data)
|
||||||
|
data = html.CleanHtml()
|
||||||
|
|
||||||
|
# collect offsets of '<mbp:pagebreak>' tags, use to make index list.
|
||||||
|
# indexlist = [] # list of (offset,length) tuples.
|
||||||
|
# not in current use.
|
||||||
|
|
||||||
|
# j=0
|
||||||
|
# lastj=0
|
||||||
|
# while True:
|
||||||
|
# j=data.find('<mbp:pagebreak>',lastj+10) # plus a bit so we find the next.
|
||||||
|
# if j < 0:
|
||||||
|
# break
|
||||||
|
# indexlist.append((lastj,j-lastj))
|
||||||
|
# print "index offset: %d length: %d" % (lastj,j-lastj)
|
||||||
|
# lastj=j
|
||||||
|
|
||||||
|
records = []
|
||||||
|
# title = html.title
|
||||||
|
# if title:
|
||||||
|
# self._header.SetTitle(title)
|
||||||
|
record_id = 1
|
||||||
|
for start_pos in range(0, len(data), Record.MAX_SIZE):
|
||||||
|
end = min(len(data), start_pos + Record.MAX_SIZE)
|
||||||
|
record_data = data[start_pos:end]
|
||||||
|
records.append(self._header.AddRecord(record_data, record_id))
|
||||||
|
#print "HTML Record %03d: (size:%d) [[%s ... %s]]" % ( record_id, len(record_data), record_data[:20], record_data[-20:] )
|
||||||
|
record_id += 1
|
||||||
|
self._header.SetImageRecordIndex(record_id)
|
||||||
|
records[0:0] = [self._header.MobiHeader()]
|
||||||
|
|
||||||
|
header, rec_offset = self._header.PDBHeader(len(records))
|
||||||
|
out.write(header)
|
||||||
|
for record in records:
|
||||||
|
record.WriteHeader(out, rec_offset)
|
||||||
|
#print "rec_offset: %d len(record.data): %d" % (rec_offset,len(record.data))
|
||||||
|
rec_offset += (len(record.data)+1) # plus one for trailing null
|
||||||
|
|
||||||
|
# Write to nuls for some reason
|
||||||
|
out.write('\0\0')
|
||||||
|
for record in records:
|
||||||
|
record.WriteData(out)
|
||||||
|
out.write('\0')
|
||||||
|
# needs a trailing null, I believe it indicates zero length 'overlap'.
|
||||||
|
# otherwise, the readers eat the last char of each html record.
|
||||||
|
# Calibre writes another 6-7 bytes of stuff after that, but we seem
|
||||||
|
# to be getting along without it.
|
||||||
|
|
||||||
|
class Record:
|
||||||
|
MAX_SIZE = 4096
|
||||||
|
INDEX_LEN = 8
|
||||||
|
_unique_id_seed = 28 # should be arbitrary, but taken from MobiHeader
|
||||||
|
|
||||||
|
# TODO(chatham): Record compression doesn't look that hard.
|
||||||
|
|
||||||
|
def __init__(self, data, record_id):
|
||||||
|
assert len(data) <= self.MAX_SIZE
|
||||||
|
self.data = data
|
||||||
|
if record_id != 0:
|
||||||
|
self._id = record_id
|
||||||
|
else:
|
||||||
|
Record._unique_id_seed += 1
|
||||||
|
self._id = 0
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'Record: id=%d len=%d' % (self._id, len(self.data))
|
||||||
|
|
||||||
|
def _SetUniqueId(self):
|
||||||
|
Record._unique_id_seed += 1
|
||||||
|
# TODO(chatham): Wraparound crap
|
||||||
|
self._id = Record._unique_id_seed
|
||||||
|
|
||||||
|
def WriteData(self, out):
|
||||||
|
out.write(self.data)
|
||||||
|
|
||||||
|
def WriteHeader(self, out, rec_offset):
|
||||||
|
attributes = 64 # dirty?
|
||||||
|
header = struct.pack('>IbbH',
|
||||||
|
rec_offset,
|
||||||
|
attributes,
|
||||||
|
0, self._id)
|
||||||
|
assert len(header) == Record.INDEX_LEN
|
||||||
|
out.write(header)
|
||||||
|
|
||||||
|
EXTH_HEADER_FIELDS = {
|
||||||
|
'author' : 100,
|
||||||
|
'publisher' : 101,
|
||||||
|
}
|
||||||
|
|
||||||
|
class Header:
|
||||||
|
EPOCH_1904 = 2082844800
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._length = 0
|
||||||
|
self._record_count = 0
|
||||||
|
self._title = '2008_2_34'
|
||||||
|
self._author = 'Unknown author'
|
||||||
|
self._publisher = 'Unknown publisher'
|
||||||
|
self._first_image_index = 0
|
||||||
|
|
||||||
|
def SetAuthor(self, author):
|
||||||
|
self._author = author.encode('ascii','ignore')
|
||||||
|
|
||||||
|
def SetTitle(self, title):
|
||||||
|
# TODO(chatham): Reevaluate whether this needs to be ASCII.
|
||||||
|
# maybe just do sys.setdefaultencoding('utf-8')? Problems
|
||||||
|
# appending self._title with other things.
|
||||||
|
self._title = title.encode('ascii','ignore')
|
||||||
|
|
||||||
|
def SetPublisher(self, publisher):
|
||||||
|
self._publisher = publisher.encode('ascii','ignore')
|
||||||
|
|
||||||
|
def AddRecord(self, data, record_id):
|
||||||
|
self.max_record_size = max(Record.MAX_SIZE, len(data))
|
||||||
|
self._record_count += 1
|
||||||
|
self._length += len(data)
|
||||||
|
return Record(data, record_id)
|
||||||
|
|
||||||
|
def _ReplaceWord(self, data, pos, word):
|
||||||
|
return data[:pos] + struct.pack('>I', word) + data[pos+4:]
|
||||||
|
|
||||||
|
def PalmDocHeader(self):
|
||||||
|
compression = 1 # no compression
|
||||||
|
unused = 0
|
||||||
|
encryption_type = 0 # no ecryption
|
||||||
|
records = self._record_count + 1 # the header record itself
|
||||||
|
palmdoc_header = struct.pack('>HHIHHHH',
|
||||||
|
compression,
|
||||||
|
unused,
|
||||||
|
self._length,
|
||||||
|
records,
|
||||||
|
Record.MAX_SIZE,
|
||||||
|
encryption_type,
|
||||||
|
unused)
|
||||||
|
assert len(palmdoc_header) == 16
|
||||||
|
return palmdoc_header
|
||||||
|
|
||||||
|
def PDBHeader(self, num_records):
|
||||||
|
HEADER_LEN = 32+2+2+9*4
|
||||||
|
RECORD_INDEX_HEADER_LEN = 6
|
||||||
|
RESOURCE_INDEX_LEN = 10
|
||||||
|
|
||||||
|
index_len = RECORD_INDEX_HEADER_LEN + num_records * Record.INDEX_LEN
|
||||||
|
rec_offset = HEADER_LEN + index_len + 2
|
||||||
|
|
||||||
|
short_title = self._title[0:31]
|
||||||
|
attributes = 0
|
||||||
|
version = 0
|
||||||
|
ctime = self.EPOCH_1904 + int(time.time())
|
||||||
|
mtime = self.EPOCH_1904 + int(time.time())
|
||||||
|
backup_time = self.EPOCH_1904 + int(time.time())
|
||||||
|
modnum = 0
|
||||||
|
appinfo_offset = 0
|
||||||
|
sort_offset = 0
|
||||||
|
type = 'BOOK'
|
||||||
|
creator = 'MOBI'
|
||||||
|
id_seed = 36
|
||||||
|
header = struct.pack('>32sHHII',
|
||||||
|
short_title, attributes, version,
|
||||||
|
ctime, mtime)
|
||||||
|
header += struct.pack('>IIII', backup_time, modnum,
|
||||||
|
appinfo_offset, sort_offset)
|
||||||
|
header += struct.pack('>4s4sI',
|
||||||
|
type, creator, id_seed)
|
||||||
|
next_record = 0 # not used?
|
||||||
|
header += struct.pack('>IH', next_record, num_records)
|
||||||
|
return header, rec_offset
|
||||||
|
|
||||||
|
def _GetExthHeader(self):
|
||||||
|
# They set author, publisher, coveroffset, thumboffset
|
||||||
|
data = {'author' : self._author,
|
||||||
|
'publisher' : self._publisher,
|
||||||
|
}
|
||||||
|
# Turn string type names into EXTH typeids.
|
||||||
|
r = []
|
||||||
|
for key, value in data.items():
|
||||||
|
typeid = EXTH_HEADER_FIELDS[key]
|
||||||
|
length_encoding_len = 8
|
||||||
|
r.append(struct.pack('>LL', typeid, len(value) + length_encoding_len,) + value)
|
||||||
|
content = ''.join(r)
|
||||||
|
|
||||||
|
# Pad to word boundary
|
||||||
|
while len(content) % 4:
|
||||||
|
content += '\0'
|
||||||
|
TODO_mysterious = 12
|
||||||
|
exth = 'EXTH' + struct.pack('>LL', len(content) + TODO_mysterious, len(data)) + content
|
||||||
|
return exth
|
||||||
|
|
||||||
|
def SetImageRecordIndex(self, idx):
|
||||||
|
self._first_image_index = idx
|
||||||
|
|
||||||
|
def MobiHeader(self):
|
||||||
|
exth_header = self._GetExthHeader();
|
||||||
|
palmdoc_header = self.PalmDocHeader()
|
||||||
|
|
||||||
|
fs = 0xffffffff
|
||||||
|
|
||||||
|
# Record 0
|
||||||
|
header_len = 0xE4 # TODO
|
||||||
|
mobi_type = 2 # BOOK
|
||||||
|
text_encoding = encoding['UTF-8']
|
||||||
|
unique_id = random.randint(1, 1<<32)
|
||||||
|
creator_version = 4
|
||||||
|
reserved = '%c' % 0xff * 40
|
||||||
|
nonbook_index = fs
|
||||||
|
full_name_offset = header_len + len(palmdoc_header) + len(exth_header) # put full name after header
|
||||||
|
language = languages['en-us']
|
||||||
|
unused = 0
|
||||||
|
mobi_header = struct.pack('>4sIIIII40sIIIIII',
|
||||||
|
'MOBI',
|
||||||
|
header_len,
|
||||||
|
mobi_type,
|
||||||
|
text_encoding,
|
||||||
|
unique_id,
|
||||||
|
creator_version,
|
||||||
|
reserved,
|
||||||
|
nonbook_index,
|
||||||
|
full_name_offset,
|
||||||
|
len(self._title),
|
||||||
|
language,
|
||||||
|
fs, fs)
|
||||||
|
assert len(mobi_header) == 104 - 16
|
||||||
|
|
||||||
|
unknown_fields = chr(0) * 32
|
||||||
|
drm_offset = 0
|
||||||
|
drm_count = 0
|
||||||
|
drm_size = 0
|
||||||
|
drm_flags = 0
|
||||||
|
exth_flags = 0x50
|
||||||
|
header_end = chr(0) * 64
|
||||||
|
mobi_header += struct.pack('>IIIIIII',
|
||||||
|
creator_version,
|
||||||
|
self._first_image_index,
|
||||||
|
fs,
|
||||||
|
unused,
|
||||||
|
fs,
|
||||||
|
unused,
|
||||||
|
exth_flags)
|
||||||
|
mobi_header += '\0' * 112 # TODO: Why this much padding?
|
||||||
|
# Set some magic offsets to be 0xFFFFFFF.
|
||||||
|
for pos in (0x94, 0x98, 0xb0, 0xb8, 0xc0, 0xc8, 0xd0, 0xd8, 0xdc):
|
||||||
|
mobi_header = self._ReplaceWord(mobi_header, pos, fs)
|
||||||
|
|
||||||
|
# 16 bytes?
|
||||||
|
padding = '\0' * 48 * 4 # why?
|
||||||
|
total_header = palmdoc_header + mobi_header + exth_header + self._title + padding
|
||||||
|
|
||||||
|
return self.AddRecord(total_header, 0)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
m = Converter(title='Testing Mobi', author='Mobi Author', publisher='mobi converter')
|
||||||
|
m.ConvertFiles(sys.argv[1:], 'test.mobi')
|
||||||
|
#m.ConvertFile(sys.argv[1], 'test.mobi')
|
||||||
|
|
@ -23,6 +23,7 @@ from fanficdownloader.exceptions import FailedToDownload
|
||||||
from writer_html import HTMLWriter
|
from writer_html import HTMLWriter
|
||||||
from writer_txt import TextWriter
|
from writer_txt import TextWriter
|
||||||
from writer_epub import EpubWriter
|
from writer_epub import EpubWriter
|
||||||
|
from writer_mobi import MobiWriter
|
||||||
|
|
||||||
def getWriter(type,config,story):
|
def getWriter(type,config,story):
|
||||||
if type == "html":
|
if type == "html":
|
||||||
|
|
@ -31,5 +32,7 @@ def getWriter(type,config,story):
|
||||||
return TextWriter(config,story)
|
return TextWriter(config,story)
|
||||||
if type == "epub":
|
if type == "epub":
|
||||||
return EpubWriter(config,story)
|
return EpubWriter(config,story)
|
||||||
|
if type == "mobi":
|
||||||
|
return MobiWriter(config,story)
|
||||||
|
|
||||||
raise FailedToDownload("(%s) is not a supported download format."%type)
|
raise FailedToDownload("(%s) is not a supported download format."%type)
|
||||||
|
|
|
||||||
196
fanficdownloader/writers/writer_mobi.py
Normal file
196
fanficdownloader/writers/writer_mobi.py
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
# -*- 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 logging
|
||||||
|
import string
|
||||||
|
import StringIO
|
||||||
|
|
||||||
|
from base_writer import *
|
||||||
|
from fanficdownloader.htmlcleanup import stripHTML
|
||||||
|
from fanficdownloader.mobi import Converter
|
||||||
|
|
||||||
|
class MobiWriter(BaseStoryWriter):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def getFormatName():
|
||||||
|
return 'mobi'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def getFormatExt():
|
||||||
|
return '.mobi'
|
||||||
|
|
||||||
|
def __init__(self, config, story):
|
||||||
|
BaseStoryWriter.__init__(self, config, story)
|
||||||
|
|
||||||
|
self.MOBI_TITLE_PAGE_START = string.Template('''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<title>${title} by ${author}</title>
|
||||||
|
<link href="stylesheet.css" type="text/css" charset="UTF-8" rel="stylesheet"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h3><a href="${storyUrl}">${title}</a> by <a href="${authorUrl}">${author}</a></h3>
|
||||||
|
<div>
|
||||||
|
''')
|
||||||
|
|
||||||
|
self.MOBI_TITLE_ENTRY = string.Template('''
|
||||||
|
<b>${label}:</b> ${value}<br />
|
||||||
|
''')
|
||||||
|
|
||||||
|
self.MOBI_TITLE_PAGE_END = string.Template('''
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
''')
|
||||||
|
|
||||||
|
self.MOBI_TABLE_TITLE_PAGE_START = string.Template('''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<title>${title} by ${author}</title>
|
||||||
|
<link href="stylesheet.css" type="text/css" charset="UTF-8" rel="stylesheet"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h3><a href="${storyUrl}">${title}</a> by <a href="${authorUrl}">${author}</a></h3>
|
||||||
|
<table class="full">
|
||||||
|
''')
|
||||||
|
|
||||||
|
self.MOBI_TABLE_TITLE_ENTRY = string.Template('''
|
||||||
|
<tr><td><b>${label}:</b></td><td>${value}</td></tr>
|
||||||
|
''')
|
||||||
|
|
||||||
|
self.MOBI_TABLE_TITLE_WIDE_ENTRY = string.Template('''
|
||||||
|
<tr><td colspan="2"><b>${label}:</b> ${value}</td></tr>
|
||||||
|
''')
|
||||||
|
|
||||||
|
self.MOBI_TABLE_TITLE_PAGE_END = string.Template('''
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
''')
|
||||||
|
|
||||||
|
self.MOBI_TOC_PAGE_START = string.Template('''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<title>${title} by ${author}</title>
|
||||||
|
<link href="stylesheet.css" type="text/css" charset="UTF-8" rel="stylesheet"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<h3>Table of Contents</h3>
|
||||||
|
''')
|
||||||
|
|
||||||
|
self.MOBI_TOC_ENTRY = string.Template('''
|
||||||
|
<a href="file${index}.xhtml">${chapter}</a><br />
|
||||||
|
''')
|
||||||
|
|
||||||
|
self.MOBI_TOC_PAGE_END = string.Template('''
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
''')
|
||||||
|
|
||||||
|
self.MOBI_CHAPTER_START = string.Template('''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<title>${chapter}</title>
|
||||||
|
<link href="stylesheet.css" type="text/css" charset="UTF-8" rel="stylesheet"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h3>${chapter}</h3>
|
||||||
|
''')
|
||||||
|
|
||||||
|
self.MOBI_CHAPTER_END = string.Template('''
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
''')
|
||||||
|
|
||||||
|
def getMetadata(self,key):
|
||||||
|
return stripHTML(self.story.getMetadata(key))
|
||||||
|
|
||||||
|
def writeStoryImpl(self, out):
|
||||||
|
|
||||||
|
files = []
|
||||||
|
|
||||||
|
# write title page.
|
||||||
|
if self.getConfig("titlepage_use_table"):
|
||||||
|
TITLE_PAGE_START = self.MOBI_TABLE_TITLE_PAGE_START
|
||||||
|
TITLE_ENTRY = self.MOBI_TABLE_TITLE_ENTRY
|
||||||
|
WIDE_TITLE_ENTRY = self.MOBI_TABLE_TITLE_WIDE_ENTRY
|
||||||
|
TITLE_PAGE_END = self.MOBI_TABLE_TITLE_PAGE_END
|
||||||
|
else:
|
||||||
|
TITLE_PAGE_START = self.MOBI_TITLE_PAGE_START
|
||||||
|
TITLE_ENTRY = self.MOBI_TITLE_ENTRY
|
||||||
|
WIDE_TITLE_ENTRY = self.MOBI_TITLE_ENTRY # same, only wide in tables.
|
||||||
|
TITLE_PAGE_END = self.MOBI_TITLE_PAGE_END
|
||||||
|
|
||||||
|
titlepageIO = StringIO.StringIO()
|
||||||
|
self.writeTitlePage(out=titlepageIO,
|
||||||
|
START=TITLE_PAGE_START,
|
||||||
|
ENTRY=TITLE_ENTRY,
|
||||||
|
WIDE_ENTRY=WIDE_TITLE_ENTRY,
|
||||||
|
END=TITLE_PAGE_END)
|
||||||
|
if titlepageIO.getvalue(): # will be false if no title page.
|
||||||
|
files.append(titlepageIO.getvalue())
|
||||||
|
titlepageIO.close()
|
||||||
|
|
||||||
|
## MOBI always has a TOC injected by mobi.py because there's
|
||||||
|
## no meta-data TOC.
|
||||||
|
# # write toc page.
|
||||||
|
# tocpageIO = StringIO.StringIO()
|
||||||
|
# self.writeTOCPage(tocpageIO,
|
||||||
|
# self.MOBI_TOC_PAGE_START,
|
||||||
|
# self.MOBI_TOC_ENTRY,
|
||||||
|
# self.MOBI_TOC_PAGE_END)
|
||||||
|
# if tocpageIO.getvalue(): # will be false if no toc page.
|
||||||
|
# files.append(tocpageIO.getvalue())
|
||||||
|
# tocpageIO.close()
|
||||||
|
|
||||||
|
for index, (title,html) in enumerate(self.story.getChapters()):
|
||||||
|
logging.debug('Writing chapter text for: %s' % title)
|
||||||
|
fullhtml = self.MOBI_CHAPTER_START.substitute({'chapter':title, 'index':index+1}) + html + self.MOBI_CHAPTER_END.substitute({'chapter':title, 'index':index+1})
|
||||||
|
# ffnet(& maybe others) gives the whole chapter text as
|
||||||
|
# one line. This causes problems for nook(at least) when
|
||||||
|
# the chapter size starts getting big (200k+)
|
||||||
|
fullhtml = fullhtml.replace('</p>','</p>\n').replace('<br />','<br />\n')
|
||||||
|
files.append(fullhtml.encode('utf-8'))
|
||||||
|
del fullhtml
|
||||||
|
|
||||||
|
c = Converter(title=self.getMetadata('title'),
|
||||||
|
author=self.getMetadata('author'),
|
||||||
|
publisher=self.getMetadata('site'))
|
||||||
|
mobidata = c.ConvertStrings(files)
|
||||||
|
out.write(mobidata)
|
||||||
|
|
||||||
|
del files
|
||||||
|
del mobidata
|
||||||
|
|
||||||
|
## Utility method for creating new tags.
|
||||||
|
def newTag(dom,name,attrs=None,text=None):
|
||||||
|
tag = dom.createElement(name)
|
||||||
|
if( attrs is not None ):
|
||||||
|
for attr in attrs.keys():
|
||||||
|
tag.setAttribute(attr,attrs[attr])
|
||||||
|
if( text is not None ):
|
||||||
|
tag.appendChild(dom.createTextNode(text))
|
||||||
|
return tag
|
||||||
|
|
||||||
|
|
@ -57,7 +57,9 @@
|
||||||
Login/Password is only asked for when required now, as is 'Are you an Adult?' where required.
|
Login/Password is only asked for when required now, as is 'Are you an Adult?' where required.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Mobi support (for Kindle) is only via EPub conversion in this version.
|
The same (rather crude) Mobi support (for Kindle) that we
|
||||||
|
had before is restored. Mobi via EPub conversion is still
|
||||||
|
available and provides better output.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
If you have any problems with this new version, please
|
If you have any problems with this new version, please
|
||||||
|
|
@ -81,12 +83,12 @@
|
||||||
<input type='radio' name='format' value='epub' checked>EPub</input>
|
<input type='radio' name='format' value='epub' checked>EPub</input>
|
||||||
<input type='radio' name='format' value='html'>HTML</input>
|
<input type='radio' name='format' value='html'>HTML</input>
|
||||||
<input type='radio' name='format' value='txt'>Plain Text</input>
|
<input type='radio' name='format' value='txt'>Plain Text</input>
|
||||||
|
<input type='radio' name='format' value='mobi'>Mobi(Kindle)</input>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<br />
|
<br />
|
||||||
<input type="submit" value="Download">
|
<input type="submit" value="Download">
|
||||||
<p><i>For most readers, including Sony Reader, Nook and iPad, use EPub.</i></p>
|
<p><i>For most readers, including Sony Reader, Nook and iPad, use EPub.</i></p>
|
||||||
<p><i>For Kindle and other Mobi readers, select EPub and use the Convert link when it's finished.</i></p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id='typebox'>
|
<div id='typebox'>
|
||||||
|
|
|
||||||
40
main.py
40
main.py
|
|
@ -158,6 +158,8 @@ class FileServer(webapp.RequestHandler):
|
||||||
self.response.headers['Content-Type'] = 'text/html'
|
self.response.headers['Content-Type'] = 'text/html'
|
||||||
elif name.endswith('.txt'):
|
elif name.endswith('.txt'):
|
||||||
self.response.headers['Content-Type'] = 'text/plain'
|
self.response.headers['Content-Type'] = 'text/plain'
|
||||||
|
elif name.endswith('.mobi'):
|
||||||
|
self.response.headers['Content-Type'] = 'application/x-mobipocket-ebook'
|
||||||
elif name.endswith('.zip'):
|
elif name.endswith('.zip'):
|
||||||
self.response.headers['Content-Type'] = 'application/zip'
|
self.response.headers['Content-Type'] = 'application/zip'
|
||||||
else:
|
else:
|
||||||
|
|
@ -334,7 +336,8 @@ class FanfictionDownloader(UserConfigServer):
|
||||||
|
|
||||||
taskqueue.add(url='/fdowntask',
|
taskqueue.add(url='/fdowntask',
|
||||||
queue_name="download",
|
queue_name="download",
|
||||||
params={'format':format,
|
params={'id':str(download.key()),
|
||||||
|
'format':format,
|
||||||
'url':download.url,
|
'url':download.url,
|
||||||
'login':login,
|
'login':login,
|
||||||
'password':password,
|
'password':password,
|
||||||
|
|
@ -390,6 +393,7 @@ class FanfictionDownloaderTask(UserConfigServer):
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
fileId = self.request.get('id')
|
||||||
format = self.request.get('format')
|
format = self.request.get('format')
|
||||||
url = self.request.get('url')
|
url = self.request.get('url')
|
||||||
login = self.request.get('login')
|
login = self.request.get('login')
|
||||||
|
|
@ -399,27 +403,42 @@ class FanfictionDownloaderTask(UserConfigServer):
|
||||||
user = users.User(self.request.get('user'))
|
user = users.User(self.request.get('user'))
|
||||||
|
|
||||||
logging.info("Downloading: " + url + " for user: "+user.nickname())
|
logging.info("Downloading: " + url + " for user: "+user.nickname())
|
||||||
|
logging.info("ID: " + fileId)
|
||||||
|
|
||||||
adapter = None
|
adapter = None
|
||||||
writerClass = None
|
writerClass = None
|
||||||
|
|
||||||
|
if fileId:
|
||||||
|
try:
|
||||||
|
## try to get download rec from passed id first.
|
||||||
|
## may need to fall back to user/url/format during transition.
|
||||||
|
download = db.get(db.Key(fileId))
|
||||||
|
logging.info("DownloadMeta found by ID:"+fileId)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not download:
|
||||||
# use existing record if available.
|
# use existing record if available.
|
||||||
q = DownloadMeta.all().filter('user =', user).filter('url =',url).filter('format =',format).fetch(1)
|
q = DownloadMeta.all().filter('user =', user).filter('url =',url).filter('format =',format).fetch(1)
|
||||||
if( q is None or len(q) < 1 ):
|
if( q is None or len(q) < 1 ):
|
||||||
|
logging.info("New DownloadMeta")
|
||||||
download = DownloadMeta()
|
download = DownloadMeta()
|
||||||
else:
|
else:
|
||||||
|
logging.info("DownloadMeta found by user/url/format")
|
||||||
download = q[0]
|
download = q[0]
|
||||||
|
|
||||||
|
## populate DownloadMeta, regardless of how found or created.
|
||||||
download.failure=None
|
download.failure=None
|
||||||
download.date=datetime.datetime.now()
|
download.date=datetime.datetime.now()
|
||||||
download.completed=False
|
download.completed=False
|
||||||
for c in download.data_chunks:
|
|
||||||
c.delete()
|
|
||||||
|
|
||||||
download.version = "%s:%s" % (os.environ['APPLICATION_ID'],os.environ['CURRENT_VERSION_ID'])
|
download.version = "%s:%s" % (os.environ['APPLICATION_ID'],os.environ['CURRENT_VERSION_ID'])
|
||||||
download.user = user
|
download.user = user
|
||||||
download.url = url
|
download.url = url
|
||||||
download.format = format
|
download.format = format
|
||||||
|
for c in download.data_chunks:
|
||||||
|
c.delete()
|
||||||
download.put()
|
download.put()
|
||||||
|
|
||||||
logging.info('Creating adapter...')
|
logging.info('Creating adapter...')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -441,12 +460,6 @@ class FanfictionDownloaderTask(UserConfigServer):
|
||||||
try:
|
try:
|
||||||
# adapter.getStory() is what does all the heavy lifting.
|
# adapter.getStory() is what does all the heavy lifting.
|
||||||
writer = writers.getWriter(format,config,adapter)
|
writer = writers.getWriter(format,config,adapter)
|
||||||
except Exception, e:
|
|
||||||
logging.exception(e)
|
|
||||||
download.failure = unicode(e)
|
|
||||||
download.put()
|
|
||||||
return
|
|
||||||
|
|
||||||
download.name = writer.getOutputFileName()
|
download.name = writer.getOutputFileName()
|
||||||
logging.debug('output_filename:'+writer.getConfig('output_filename'))
|
logging.debug('output_filename:'+writer.getConfig('output_filename'))
|
||||||
logging.debug('getOutputFileName:'+writer.getOutputFileName())
|
logging.debug('getOutputFileName:'+writer.getOutputFileName())
|
||||||
|
|
@ -454,7 +467,11 @@ class FanfictionDownloaderTask(UserConfigServer):
|
||||||
download.author = adapter.getStory().getMetadata('author')
|
download.author = adapter.getStory().getMetadata('author')
|
||||||
download.url = adapter.getStory().getMetadata('storyUrl')
|
download.url = adapter.getStory().getMetadata('storyUrl')
|
||||||
download.put()
|
download.put()
|
||||||
index=0
|
except Exception, e:
|
||||||
|
logging.exception(e)
|
||||||
|
download.failure = unicode(e)
|
||||||
|
download.put()
|
||||||
|
return
|
||||||
|
|
||||||
outbuffer = StringIO.StringIO()
|
outbuffer = StringIO.StringIO()
|
||||||
writer.writeStory(outbuffer)
|
writer.writeStory(outbuffer)
|
||||||
|
|
@ -474,6 +491,7 @@ class FanfictionDownloaderTask(UserConfigServer):
|
||||||
def c(data):
|
def c(data):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
index=0
|
||||||
while( len(data) > 0 ):
|
while( len(data) > 0 ):
|
||||||
DownloadData(download=download,
|
DownloadData(download=download,
|
||||||
index=index,
|
index=index,
|
||||||
|
|
|
||||||
22
recent.html
22
recent.html
|
|
@ -49,20 +49,20 @@
|
||||||
{% for fic in fics %}
|
{% for fic in fics %}
|
||||||
<p>
|
<p>
|
||||||
{% if fic.completed %}
|
{% if fic.completed %}
|
||||||
<a href="/file?id={{ fic.key }}">Download {{ fic.title }}</a>
|
<span class="recent"><a href="/file?id={{ fic.key }}">Download {{ fic.title }}</a></span>
|
||||||
by {{ fic.author }} ({{ fic.format }})<br/>
|
by {{ fic.author }} ({{ fic.format }})
|
||||||
{% if fic.escaped_url %}
|
|
||||||
<a href="http://www.convertfiles.com/index.php?url={{ fic.escaped_url }}">Convert {{ fic.title }} to other formats</a><br />
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% if fic.failure %}
|
|
||||||
<div id='error'>{{ fic.failure }}</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not fic.completed and not fic.failure %}
|
{% if not fic.completed and not fic.failure %}
|
||||||
Request Processing...<br />
|
<span class="recent">Processing {{ fic.title }}</span>
|
||||||
|
by {{ fic.author }} ({{ fic.format }})
|
||||||
|
{% endif %}
|
||||||
|
{% if fic.failure %}
|
||||||
|
<span id='error'>{{ fic.failure }}</span>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ fic.url }}" title="Link to original story">Source</a>
|
||||||
|
{% if fic.completed and fic.escaped_url %}
|
||||||
|
<a href="http://www.convertfiles.com/index.php?url={{ fic.escaped_url }}" title="Convert to other formats using Convertfiles.com">Convert</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<small><a href="{{ fic.url }}">{{ fic.url }}</a></small>
|
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
32
status.html
32
status.html
|
|
@ -44,29 +44,27 @@
|
||||||
<div id='urlbox'>
|
<div id='urlbox'>
|
||||||
{% if fic.url %}
|
{% if fic.url %}
|
||||||
<div id='greeting'>
|
<div id='greeting'>
|
||||||
<p><a href='{{ fic.url }}'>{{ fic.url }}</a></p>
|
<p>
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div>
|
|
||||||
{% if fic.completed %}
|
{% if fic.completed %}
|
||||||
<p>Your fic has finished processing and you can download it now:</p>
|
<p>Your fic has finished processing and you can download it now.</p>
|
||||||
<p><a href="/file?id={{ fic.key }}">Download {{ fic.title }}</a>
|
<span class="recent"><a href="/file?id={{ fic.key }}">Download {{ fic.title }}</a></span>
|
||||||
by {{ fic.author }} ({{ fic.format }})</p>
|
by {{ fic.author }} ({{ fic.format }})
|
||||||
{% if escaped_url %}
|
|
||||||
<p><a href="http://www.convertfiles.com/index.php?url={{ escaped_url }}">Convert {{ fic.title }} to other formats</a></p>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
|
||||||
{% if fic.failure %}
|
{% if fic.failure %}
|
||||||
Your fic failed to process. Please check the URL and the error message below.<br />
|
<span id='error'>{{ fic.failure }}</span>
|
||||||
<div id='error'>
|
|
||||||
{{ fic.failure }}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<p>Not done yet. This page will periodically poll to see if your story has finished.</p>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if not fic.completed and not fic.failure %}
|
||||||
|
<span class="recent">Processing {{ fic.title }}</span>
|
||||||
|
by {{ fic.author }} ({{ fic.format }})
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p>Or see your personal list of <a href="/recent">previously downloaded fanfics</a>.</p>
|
<a href="{{ fic.url }}" title="Link to original story">Source</a>
|
||||||
|
{% if fic.completed and escaped_url %}
|
||||||
|
<a href="http://www.convertfiles.com/index.php?url={{ escaped_url }}" title="Convert to other formats using Convertfiles.com">Convert</a>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<p>See your personal list of <a href="/recent">previously downloaded fanfics</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
<div style='text-align: center'>
|
<div style='text-align: center'>
|
||||||
<img src="http://code.google.com/appengine/images/appengine-silver-120x30.gif"
|
<img src="http://code.google.com/appengine/images/appengine-silver-120x30.gif"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue