mirror of
https://github.com/JimmXinu/FanFicFare.git
synced 2025-12-06 08:52:55 +01:00
472 lines
17 KiB
Python
472 lines
17 KiB
Python
#!/usr/bin/env python
|
|
#
|
|
# Copyright 2007 Google Inc.
|
|
#
|
|
# 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
|
|
logging.getLogger().setLevel(logging.DEBUG)
|
|
|
|
import os
|
|
from os.path import dirname, basename, normpath
|
|
import sys
|
|
import zlib
|
|
import urllib
|
|
|
|
import traceback
|
|
import StringIO
|
|
|
|
from google.appengine.runtime import DeadlineExceededError
|
|
|
|
from google.appengine.api import taskqueue
|
|
from google.appengine.ext.webapp import template
|
|
from google.appengine.api import users
|
|
from google.appengine.ext import webapp
|
|
from google.appengine.ext.webapp import util
|
|
|
|
from fanficdownloader.downloader import *
|
|
from fanficdownloader.ffnet import *
|
|
from fanficdownloader.output import *
|
|
from fanficdownloader import twilighted
|
|
from fanficdownloader import adastrafanfic
|
|
|
|
from google.appengine.ext import db
|
|
|
|
from fanficdownloader.zipdir import *
|
|
|
|
from ffstorage import *
|
|
|
|
from fanficdownloader import adapters, writers, exceptions
|
|
import ConfigParser
|
|
|
|
class MainHandler(webapp.RequestHandler):
|
|
def get(self):
|
|
user = users.get_current_user()
|
|
if user:
|
|
error = self.request.get('error')
|
|
template_values = {'nickname' : user.nickname(), 'authorized': True}
|
|
url = self.request.get('url')
|
|
template_values['url'] = url
|
|
|
|
if error:
|
|
if error == 'login_required':
|
|
template_values['error_message'] = 'This story (or one of the chapters) requires you to be logged in.'
|
|
elif error == 'bad_url':
|
|
template_values['error_message'] = 'Unsupported URL: ' + url
|
|
elif error == 'custom':
|
|
template_values['error_message'] = 'Error happened: ' + self.request.get('errtext')
|
|
elif error == 'configsaved':
|
|
template_values['error_message'] = 'Configuration Saved'
|
|
|
|
filename = self.request.get('file')
|
|
if len(filename) > 1:
|
|
template_values['yourfile'] = '''<div id='yourfile'><a href='/file?id=%s'>"%s" by %s</a></div>''' % (filename, self.request.get('name'), self.request.get('author'))
|
|
|
|
self.response.headers['Content-Type'] = 'text/html'
|
|
path = os.path.join(os.path.dirname(__file__), 'index.html')
|
|
|
|
self.response.out.write(template.render(path, template_values))
|
|
else:
|
|
logging.debug(users.create_login_url('/'))
|
|
url = users.create_login_url(self.request.uri)
|
|
template_values = {'login_url' : url, 'authorized': False}
|
|
path = os.path.join(os.path.dirname(__file__), 'index.html')
|
|
self.response.out.write(template.render(path, template_values))
|
|
|
|
|
|
class EditConfigServer(webapp.RequestHandler):
|
|
def get(self):
|
|
self.post()
|
|
|
|
def post(self):
|
|
user = users.get_current_user()
|
|
if not user:
|
|
self.redirect(users.create_login_url(self.request.uri))
|
|
return
|
|
|
|
template_values = {'nickname' : user.nickname(), 'authorized': True}
|
|
|
|
## Pull user's config record.
|
|
l = UserConfig.all().filter('user =', user).fetch(1)
|
|
if l:
|
|
uconfig=l[0]
|
|
else:
|
|
uconfig=None
|
|
|
|
if self.request.get('update'):
|
|
if uconfig is None:
|
|
uconfig = UserConfig()
|
|
uconfig.user = user
|
|
uconfig.config = self.request.get('config').encode('utf8')[:10000] ## just in case.
|
|
uconfig.put()
|
|
self.redirect("/?error=configsaved")
|
|
else: # not update, assume display for edit
|
|
if uconfig is not None and uconfig.config:
|
|
config = uconfig.config
|
|
else:
|
|
configfile = open("example.ini","rb")
|
|
config = configfile.read()
|
|
configfile.close()
|
|
template_values['config'] = config
|
|
|
|
configfile = open("defaults.ini","rb")
|
|
config = configfile.read()
|
|
configfile.close()
|
|
template_values['defaultsini'] = config
|
|
|
|
path = os.path.join(os.path.dirname(__file__), 'editconfig.html')
|
|
self.response.headers['Content-Type'] = 'text/html'
|
|
self.response.out.write(template.render(path, template_values))
|
|
|
|
|
|
class FileServer(webapp.RequestHandler):
|
|
|
|
def get(self):
|
|
fileId = self.request.get('id')
|
|
|
|
if fileId == None or len(fileId) < 3:
|
|
self.redirect('/')
|
|
return
|
|
|
|
key = db.Key(fileId)
|
|
fanfic = db.get(key)
|
|
|
|
# check for completed & failure.
|
|
|
|
name = fanfic.name.encode('utf-8')
|
|
|
|
#name = urllib.quote(name)
|
|
|
|
logging.info("Serving file: %s" % name)
|
|
|
|
if name.endswith('.epub'):
|
|
self.response.headers['Content-Type'] = 'application/epub+zip'
|
|
elif name.endswith('.html'):
|
|
self.response.headers['Content-Type'] = 'text/html'
|
|
elif name.endswith('.txt'):
|
|
self.response.headers['Content-Type'] = 'text/plain'
|
|
elif name.endswith('.zip'):
|
|
self.response.headers['Content-Type'] = 'application/zip'
|
|
else:
|
|
self.response.headers['Content-Type'] = 'application/octet-stream'
|
|
|
|
self.response.headers['Content-disposition'] = 'attachment; filename="%s"' % name
|
|
|
|
data = DownloadData.all().filter("download =", fanfic).order("index")
|
|
# epubs are all already compressed.
|
|
# Each chunk is compress individually to avoid having
|
|
# to hold the whole in memory just for the
|
|
# compress/uncompress
|
|
if fanfic.format != 'epub':
|
|
def dc(data):
|
|
try:
|
|
return zlib.decompress(data)
|
|
# if error, assume it's a chunk from before we started compessing.
|
|
except zlib.error:
|
|
return data
|
|
else:
|
|
def dc(data):
|
|
return data
|
|
|
|
for datum in data:
|
|
self.response.out.write(dc(datum.blob))
|
|
|
|
class FileStatusServer(webapp.RequestHandler):
|
|
def get(self):
|
|
user = users.get_current_user()
|
|
if not user:
|
|
self.redirect(users.create_login_url(self.request.uri))
|
|
return
|
|
|
|
fileId = self.request.get('id')
|
|
|
|
if fileId == None or len(fileId) < 3:
|
|
self.redirect('/')
|
|
|
|
key = db.Key(fileId)
|
|
fic = db.get(key)
|
|
|
|
logging.info("Status url: %s" % fic.url)
|
|
if fic.completed and fic.format=='epub':
|
|
escaped_url = urlEscape(self.request.host_url+"/file/"+fic.name+"."+fic.format+"?id="+fileId+"&fake=file."+fic.format)
|
|
else:
|
|
escaped_url=False
|
|
template_values = dict(fic = fic,
|
|
nickname = user.nickname(),
|
|
escaped_url = escaped_url
|
|
)
|
|
path = os.path.join(os.path.dirname(__file__), 'status.html')
|
|
self.response.out.write(template.render(path, template_values))
|
|
|
|
class RecentFilesServer(webapp.RequestHandler):
|
|
def get(self):
|
|
user = users.get_current_user()
|
|
if not user:
|
|
self.redirect(users.create_login_url(self.request.uri))
|
|
return
|
|
|
|
q = DownloadMeta.all()
|
|
q.filter('user =', user).order('-date')
|
|
fics = q.fetch(100)
|
|
|
|
for fic in fics:
|
|
if fic.completed and fic.format == 'epub':
|
|
fic.escaped_url = urlEscape(self.request.host_url+"/file/"+fic.name+"."+fic.format+"?id="+str(fic.key())+"&fake=file."+fic.format)
|
|
|
|
template_values = dict(fics = fics, nickname = user.nickname())
|
|
path = os.path.join(os.path.dirname(__file__), 'recent.html')
|
|
self.response.out.write(template.render(path, template_values))
|
|
|
|
class UserConfigServer(webapp.RequestHandler):
|
|
def getUserConfig(self,user):
|
|
config = ConfigParser.SafeConfigParser()
|
|
|
|
logging.debug('reading defaults.ini config file')
|
|
config.read('defaults.ini')
|
|
|
|
## Pull user's config record.
|
|
l = UserConfig.all().filter('user =', user).fetch(1)
|
|
## TEST THIS
|
|
if l and l[0].config:
|
|
uconfig=l[0]
|
|
#logging.debug('reading config from UserConfig(%s)'%uconfig.config)
|
|
config.readfp(StringIO.StringIO(uconfig.config))
|
|
|
|
return config
|
|
|
|
class FanfictionDownloader(UserConfigServer):
|
|
def get(self):
|
|
self.post()
|
|
|
|
def post(self):
|
|
logging.getLogger().setLevel(logging.DEBUG)
|
|
user = users.get_current_user()
|
|
if not user:
|
|
self.redirect(users.create_login_url(self.request.uri))
|
|
return
|
|
|
|
format = self.request.get('format')
|
|
url = self.request.get('url')
|
|
login = self.request.get('login')
|
|
password = self.request.get('password')
|
|
is_adult = self.request.get('is_adult') == "on"
|
|
|
|
logging.info("Queuing Download: " + url)
|
|
|
|
# use existing record if available.
|
|
q = DownloadMeta.all().filter('user =', user).filter('url =',url).filter('format =',format).fetch(1)
|
|
if( q is None or len(q) < 1 ):
|
|
download = DownloadMeta()
|
|
else:
|
|
download = q[0]
|
|
download.completed=False
|
|
download.failure=None
|
|
for c in download.data_chunks:
|
|
c.delete()
|
|
|
|
download.user = user
|
|
download.url = url
|
|
download.format = format
|
|
|
|
adapter = None
|
|
try:
|
|
config = self.getUserConfig(user)
|
|
adapter = adapters.getAdapter(config,url)
|
|
logging.info('Created an adaper: %s' % adapter)
|
|
|
|
if len(login) > 1:
|
|
adapter.username=login
|
|
adapter.password=password
|
|
adapter.is_adult=is_adult
|
|
## This scrapes the metadata, which will be
|
|
## duplicated in the queue task, but it
|
|
## detects bad URLs, bad login, bad story, etc
|
|
## without waiting for the queue. So I think
|
|
## it's worth the double up. Could maybe save
|
|
## it all in the download object someday.
|
|
story = adapter.getStoryMetadataOnly()
|
|
download.title = story.getMetadata('title')
|
|
download.author = story.getMetadata('author')
|
|
download.put()
|
|
|
|
taskqueue.add(url='/fdowntask',
|
|
queue_name="download",
|
|
params={'format':format,
|
|
'url':url,
|
|
'login':login,
|
|
'password':password,
|
|
'user':user.email(),
|
|
'is_adult':is_adult})
|
|
|
|
logging.info("enqueued download key: " + str(download.key()))
|
|
|
|
except (exceptions.FailedToLogin,exceptions.AdultCheckRequired), e:
|
|
logging.exception(e)
|
|
download.failure = str(e)
|
|
download.put()
|
|
logging.debug('Need to Login, display log in page')
|
|
is_login= ( isinstance(e, exceptions.FailedToLogin) )
|
|
template_values = dict(nickname = user.nickname(),
|
|
url = url,
|
|
format = format,
|
|
site = adapter.getSiteDomain(),
|
|
fic = download,
|
|
is_login=is_login,
|
|
)
|
|
# thewriterscoffeeshop.com can do adult check *and* user required.
|
|
if isinstance(e,exceptions.AdultCheckRequired):
|
|
template_values['login']=login
|
|
template_values['password']=password
|
|
|
|
path = os.path.join(os.path.dirname(__file__), 'login.html')
|
|
self.response.out.write(template.render(path, template_values))
|
|
return
|
|
except Exception, e:
|
|
logging.exception(e)
|
|
download.failure = str(e)
|
|
download.put()
|
|
|
|
self.redirect('/status?id='+str(download.key()))
|
|
|
|
return
|
|
|
|
|
|
class FanfictionDownloaderTask(UserConfigServer):
|
|
def _printableVersion(self, text):
|
|
text = removeEntities(text)
|
|
try:
|
|
d = text.decode('utf-8')
|
|
except:
|
|
d = text
|
|
return d
|
|
|
|
|
|
def post(self):
|
|
logging.getLogger().setLevel(logging.DEBUG)
|
|
format = self.request.get('format')
|
|
url = self.request.get('url')
|
|
login = self.request.get('login')
|
|
password = self.request.get('password')
|
|
is_adult = self.request.get('is_adult')
|
|
# User object can't pass, just email address
|
|
user = users.User(self.request.get('user'))
|
|
|
|
logging.info("Downloading: " + url + " for user: "+user.nickname())
|
|
|
|
adapter = None
|
|
writerClass = None
|
|
|
|
# use existing record if available.
|
|
q = DownloadMeta.all().filter('user =', user).filter('url =',url).filter('format =',format).fetch(1)
|
|
if( q is None or len(q) < 1 ):
|
|
download = DownloadMeta()
|
|
else:
|
|
download = q[0]
|
|
download.completed=False
|
|
for c in download.data_chunks:
|
|
c.delete()
|
|
|
|
download.user = user
|
|
download.url = url
|
|
download.format = format
|
|
download.put()
|
|
logging.info('Creating adapter...')
|
|
|
|
try:
|
|
config = self.getUserConfig(user)
|
|
adapter = adapters.getAdapter(config,url)
|
|
except Exception, e:
|
|
logging.exception(e)
|
|
download.failure = str(e)
|
|
download.put()
|
|
return
|
|
|
|
logging.info('Created an adapter: %s' % adapter)
|
|
|
|
if len(login) > 1:
|
|
adapter.username=login
|
|
adapter.password=password
|
|
adapter.is_adult=is_adult
|
|
|
|
try:
|
|
# adapter.getStory() is what does all the heavy lifting.
|
|
writer = writers.getWriter(format,config,adapter.getStory())
|
|
except Exception, e:
|
|
logging.exception(e)
|
|
download.failure = str(e)
|
|
download.put()
|
|
return
|
|
|
|
download.name = writer.getOutputFileName()
|
|
download.title = adapter.getStory().getMetadata('title')
|
|
download.author = adapter.getStory().getMetadata('author')
|
|
download.put()
|
|
index=0
|
|
|
|
outbuffer = StringIO.StringIO()
|
|
writer.writeStory(outbuffer)
|
|
data = outbuffer.getvalue()
|
|
outbuffer.close()
|
|
del writer
|
|
del adapter
|
|
|
|
# epubs are all already compressed.
|
|
# Each chunk is compressed individually to avoid having
|
|
# to hold the whole in memory just for the
|
|
# compress/uncompress.
|
|
if format != 'epub':
|
|
def c(data):
|
|
return zlib.compress(data)
|
|
else:
|
|
def c(data):
|
|
return data
|
|
|
|
while( len(data) > 0 ):
|
|
DownloadData(download=download,
|
|
index=index,
|
|
blob=c(data[:1000000])).put()
|
|
index += 1
|
|
data = data[1000000:]
|
|
download.completed=True
|
|
download.put()
|
|
|
|
logging.info("Download finished OK")
|
|
return
|
|
|
|
def toPercentDecimal(match):
|
|
"Return the %decimal number for the character for url escaping"
|
|
s = match.group(1)
|
|
return "%%%02x" % ord(s)
|
|
|
|
def urlEscape(data):
|
|
"Escape text, including unicode, for use in URLs"
|
|
p = re.compile(r'([^\w])')
|
|
return p.sub(toPercentDecimal, data.encode("utf-8"))
|
|
|
|
def main():
|
|
application = webapp.WSGIApplication([('/', MainHandler),
|
|
('/fdowntask', FanfictionDownloaderTask),
|
|
('/fdown', FanfictionDownloader),
|
|
(r'/file.*', FileServer),
|
|
('/status', FileStatusServer),
|
|
('/recent', RecentFilesServer),
|
|
('/editconfig', EditConfigServer),
|
|
],
|
|
debug=False)
|
|
util.run_wsgi_app(application)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
logging.getLogger().setLevel(logging.DEBUG)
|
|
main()
|