Compare commits

...
Author SHA1 Message Date
Jim Miller c76facd40c Update CLI download zip. 2014-06-04 21:44:30 -05:00
Jim Miller f1d52834d1 Bump versions. 2014-06-04 21:43:17 -05:00
Jim Miller e8ac7f8a89 cryzed's changes: New site voracity2.e-fic.com, pprint -m from downloader. 2014-06-04 21:42:20 -05:00
Jim Miller 3e0f92d8ce Added tag calibre-plugin-1.8.21 for changeset 943e3d21e5fa 2014-05-23 20:17:51 -05:00
Jim Miller 2682d0fe36 Added tag FanFictionDownLoader-4.5.02 for changeset 943e3d21e5fa 2014-05-23 20:17:43 -05:00
Jim Miller 125e29091f Update CLI download zip. 2014-05-23 20:17:27 -05:00
Jim Miller d0c73d5444 Bump versions. 2014-05-23 20:15:49 -05:00
Jim Miller bd8e54edcf Fixes for literotica.com: URLs using //site, allow https, ch01 as storyId, multi ch only. 2014-05-17 21:12:43 -05:00
Jim Miller d680a86f0c Fix for dark-solace.org Rating. 2014-05-17 15:55:20 -05:00
Jim Miller f075ae582d Added tag FanFictionDownLoader-4.5.01 for changeset b3a1ca11a76d 2014-05-13 20:18:11 -05:00
Jim Miller 928ebb9751 Added tag calibre-plugin-1.8.20 for changeset b3a1ca11a76d 2014-05-13 20:17:57 -05:00
Jim Miller ce262be162 Update CLI download zip. 2014-05-13 20:17:44 -05:00
Jim Miller 6faa6850af Bump versions. 2014-05-13 20:16:22 -05:00
Jim Miller f392c6dd77 Fix for dark-solace.org metadata parsing. 2014-05-13 12:00:58 -05:00
Jim Miller af0dff28b4 Fix for fictionpad.com removing 'dislikes' in some(all?) cases. 2014-05-10 14:30:53 -05:00
Jim Miller 02a75a821f Fix for storiesonline.net changing urls. 2014-05-10 14:30:29 -05:00
Jim Miller c06028b498 Fix for AO3 story not found. 2014-05-10 14:30:06 -05:00
Jim Miller de4b95af9b Added tag FanFictionDownLoader-4.5.00 for changeset fbdadac26cf9 2014-05-05 12:54:38 -05:00
Jim Miller 5366355d96 Added tag calibre-plugin-1.8.19 for changeset fbdadac26cf9 2014-05-05 12:54:23 -05:00
13 changed files with 204 additions and 51 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# ffd-retief-hrd fanfictiondownloader
application: fanfictiondownloader
version: 4-5-00
version: 4-5-03
runtime: python27
api_version: 1
threadsafe: true
+1 -1
View File
@@ -42,7 +42,7 @@ class FanFictionDownLoaderBase(InterfaceActionBase):
description = _('UI plugin to download FanFiction stories from various sites.')
supported_platforms = ['windows', 'osx', 'linux']
author = 'Jim Miller'
version = (1, 8, 19)
version = (1, 8, 22)
minimum_calibre_version = (1, 13, 0)
#: This field defines the GUI plugin class that contains all the code
+2 -1
View File
@@ -23,6 +23,7 @@ import getpass
import string
import ConfigParser
from subprocess import call
import pprint
import logging
if sys.version_info >= (2, 7):
@@ -292,7 +293,7 @@ def main(argv,
else:
# regular download
if options.metaonly:
print adapter.getStoryMetadataOnly().getAllMetadata()
pprint.pprint(adapter.getStoryMetadataOnly().getAllMetadata())
output_filename=writeStory(configuration,adapter,options.format,options.metaonly)
Binary file not shown.
+1
View File
@@ -123,6 +123,7 @@ import adapter_fictionpadcom
import adapter_storiesonlinenet
import adapter_trekiverseorg
import adapter_literotica
import adapter_voracity2eficcom
## This bit of complexity allows adapters to be added by just adding
## importing. It eliminates the long if/else clauses we used to need
@@ -145,10 +145,13 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
except urllib2.HTTPError, e:
if e.code == 404:
raise exceptions.StoryDoesNotExist(self.meta)
raise exceptions.StoryDoesNotExist(self.url)
else:
raise e
if "Sorry, we couldn't find the work you were looking for." in data:
raise exceptions.StoryDoesNotExist(self.url)
if self.needToLoginCheck(data):
# need to log in for this one.
self.performLogin(url,data)
@@ -182,6 +182,11 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
# first a tag in pagetitle is title
self.story.setMetadata('title',stripHTML(div.find('a')))
div.find('a').extract()
# only thing left in div(pagetitle) now should be 'by' and rating.
rating = stripHTML(div)
if '[' in rating:
self.story.setMetadata('rating', rating[rating.index('[')+1:-1])
for chapa in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+
self.story.getMetadata('storyId')+'&chapter=\d+')):
@@ -234,31 +239,28 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
self.setDescription(url,svalue)
#self.story.setMetadata('description',stripHTML(svalue))
if 'Rated' in label:
self.story.setMetadata('rating', value[:len(value)-2])
if 'Word count' in label:
self.story.setMetadata('numWords', value)
if 'Categories' in label:
cats = labelspan.parent.findAll('a',href=re.compile(r'categories.php\?catid=\d+'))
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
for cat in cats:
self.story.addToList('category',cat.string)
if 'Characters' in label:
for char in value.string.split(', '):
if not 'None' in char:
self.story.addToList('characters',char)
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
for char in chars:
self.story.addToList('characters',char.string)
if 'Genre' in label:
for genre in value.string.split(', '):
if not 'None' in genre:
self.story.addToList('genre',genre)
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1'))
for genre in genres:
self.story.addToList('genre',genre.string)
if 'Warnings' in label:
for warning in value.string.split(', '):
if not 'None' in warning:
self.story.addToList('warnings',warning)
warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2'))
for warning in warnings:
self.story.addToList('warnings',warning.string)
if 'Completed' in label:
if 'Yes' in value:
@@ -133,6 +133,7 @@ class FictionPadSiteAdapter(BaseSiteAdapter):
author = tables['users'][0]
story = tables['stories'][0]
story_ver = tables['story_versions'][0]
print("story:%s"%story)
self.story.setMetadata('authorId',author['id'])
self.story.setMetadata('author',author['display_name'])
@@ -151,7 +152,8 @@ class FictionPadSiteAdapter(BaseSiteAdapter):
self.story.setMetadata('comments',story['comments_count'])
self.story.setMetadata('views',story['views_count'])
self.story.setMetadata('likes',int(story['likes'])) # no idea why they floated these.
self.story.setMetadata('dislikes',int(story['dislikes']))
if 'dislikes' in story:
self.story.setMetadata('dislikes',int(story['dislikes']))
if story_ver['is_complete']:
self.story.setMetadata('status', 'Completed')
+22 -12
View File
@@ -42,16 +42,19 @@ class LiteroticaSiteAdapter(BaseSiteAdapter):
self.story.setMetadata('siteabbrev','litero')
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
# normalize to first chapter. Not sure if they ever have more than 2 digits.
storyid = self.parsedUrl.path.split('/',)[2]
if re.match(r'-ch\d\d$',storyid):
storyid = storyid[:-2]+'01'
self.story.setMetadata('storyId',storyid)
self.origurl = url
if "http://www.i." in self.origurl:
if "//www.i." in self.origurl:
## accept m(mobile)url, but use www.
self.origurl = self.origurl.replace("http://www.i.","http://www.")
self.origurl = self.origurl.replace("//www.i.","//www.")
# normalized story URL.
self._setURL("http://"+self.getSiteDomain()\
self._setURL(url[:url.index('//')+2]+self.getSiteDomain()\
+"/s/"+self.story.getMetadata('storyId'))
# The date format will vary from site to site.
@@ -69,10 +72,10 @@ class LiteroticaSiteAdapter(BaseSiteAdapter):
@classmethod
def getSiteExampleURLs(self):
#return "http://www.literotica.com/s/story-title http://www.literotica.com/stories/showstory.php?id=1234 http://www.i.literotica.com/stories/showstory.php?id=1234"
return "http://www.literotica.com/s/story-title"
return "http://www.literotica.com/s/story-title https://www.literotica.com/s/story-title"
def getSiteURLPattern(self):
return r"http://www(\.i)?\.literotica\.com/s/([a-zA-Z0-9_-]+)"
return r"https?://www(\.i)?\.literotica\.com/s/([a-zA-Z0-9_-]+)"
def extractChapterUrlsAndMetadata(self):
@@ -97,20 +100,24 @@ class LiteroticaSiteAdapter(BaseSiteAdapter):
# author
a = soup1.find("span", "b-story-user-y")
self.story.setMetadata('authorId', urlparse.parse_qs(a.a['href'].split('?')[1])['uid'])
self.story.setMetadata('authorUrl', a.a['href'])
authorurl = a.a['href']
if authorurl.startswith('//'):
authorurl = self.parsedUrl.scheme+':'+authorurl
self.story.setMetadata('authorUrl', authorurl)
self.story.setMetadata('author', a.text)
# get the author page
try:
dataAuth = self._fetchUrl(a.a['href'])
dataAuth = self._fetchUrl(authorurl)
soupAuth = bs.BeautifulSoup(dataAuth)
except urllib2.HTTPError, e:
if e.code == 404:
raise exceptions.StoryDoesNotExist(a.a['href'])
raise exceptions.StoryDoesNotExist(authorurl)
else:
raise e
storyLink = soupAuth.find('a', href=url1)
## site has started using //domain.name/asdf urls remove https?: from front
storyLink = soupAuth.find('a', href=url1[url1.index(':')+1:])
if storyLink is not None:
# pull the published date from the author page
@@ -166,7 +173,10 @@ class LiteroticaSiteAdapter(BaseSiteAdapter):
self.story.setMetadata('datePublished',makeDate(stripHTML(row.find('td',{'class':'dt'})), self.dateformat))
while row['class'] == 'sl':
# pages include full URLs.
self.chapterUrls.append((row.a.string,row.a['href']))
chapurl = row.a['href']
if chapurl.startswith('//'):
chapurl = self.parsedUrl.scheme+':'+chapurl
self.chapterUrls.append((row.a.string,chapurl))
if not row.nextSibling:
break
row = row.nextSibling
@@ -66,7 +66,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
return "http://"+self.getSiteDomain()+"/s/1234 http://"+self.getSiteDomain()+"/s/1234:4010"
def getSiteURLPattern(self):
return re.escape("http://"+self.getSiteDomain())+r"/s/\d+((:\d+)?(;\d+)?$|(:i)?$)"
return re.escape("http://"+self.getSiteDomain())+r"/s/\d+((:\d+)?(;\d+)?$|(:i)?$)?"
## Login seems to be reasonably standard across eFiction sites.
def needToLoginCheck(self, data):
@@ -171,7 +171,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
a = asoup.findAll('td', {'class' : 'lc2'})
for lc2 in a:
if lc2.find('a')['href'] == '/s/'+self.story.getMetadata('storyId'):
if lc2.find('a', href=re.compile(r'^/s/'+self.story.getMetadata('storyId'))):
i=1
break
if a[len(a)-1] == lc2:
@@ -0,0 +1,150 @@
import re
import urllib2
import urlparse
from .. import BeautifulSoup
from base_adapter import BaseSiteAdapter, makeDate
from .. import exceptions
def getClass():
return Voracity2EficComAdapter
class Voracity2EficComAdapter(BaseSiteAdapter):
SITE_DOMAIN = 'voracity2.e-fic.com'
BASE_URL = 'http://' + SITE_DOMAIN
LOGIN_URL = BASE_URL + '/user.php?action=login'
VIEW_STORY_URL_TEMPLATE = BASE_URL + '/viewstory.php?sid=%d'
METADATA_URL_SUFFIX = '&index=1'
AGE_CONSENT_URL_SUFFIX = '&ageconsent=ok&warning=4'
DATETIME_FORMAT = '%m/%d/%Y'
def __init__(self, config, url):
BaseSiteAdapter.__init__(self, config, url)
query_data = urlparse.parse_qs(self.parsedUrl.query)
story_id = query_data['sid'][0]
self.story.setMetadata('storyId', story_id)
self._setURL(self.VIEW_STORY_URL_TEMPLATE % int(story_id))
self.story.setMetadata('siteabbrev', 'voe')
def _login(self):
# Apparently self.password is only set when login fails, i.e.
# the FailedToLogin exception is raised, so the adapter gets new
# login data and tries again
if self.password:
password = self.password
username = self.username
else:
username = self.getConfig('username')
password = self.getConfig('password')
parameters = {
'penname': username,
'password': password,
'submit': 'Submit'}
class CustomizedFailedToLogin(exceptions.FailedToLogin):
def __init__(self, url, passwdonly=False):
# Use username variable from outer scope
exceptions.FailedToLogin.__init__(self, url, username, passwdonly)
soup = self._customized_fetch_url(self.LOGIN_URL, CustomizedFailedToLogin, parameters)
div = soup.find('div', id='useropts')
if not div:
raise CustomizedFailedToLogin(self.LOGIN_URL)
def _customized_fetch_url(self, url, exception, parameters=None):
try:
data = self._fetchUrl(url, parameters)
except urllib2.HTTPError:
raise exception(self.url)
return BeautifulSoup.BeautifulSoup(data)
@staticmethod
def getSiteDomain():
return Voracity2EficComAdapter.SITE_DOMAIN
@classmethod
def getSiteExampleURLs(cls):
return cls.VIEW_STORY_URL_TEMPLATE % 1234
def getSiteURLPattern(self):
return re.escape(self.VIEW_STORY_URL_TEMPLATE[:-2]) + r'\d+$'
def extractChapterUrlsAndMetadata(self):
soup = self._customized_fetch_url(self.url + self.METADATA_URL_SUFFIX, exceptions.StoryDoesNotExist)
# Check if the story is for "Registered Users Only", i.e. has adult
# content. Based on the "is_adult" attributes either login or raise an
# error.
div = soup.find('div', {'class': 'errortext'})
if div and div.contents[0] == 'Registered Users Only':
if not (self.is_adult or self.getConfig('is_adult')):
raise exceptions.AdultCheckRequired(self.url)
self._login()
url = ''.join([self.url, self.METADATA_URL_SUFFIX, self.AGE_CONSENT_URL_SUFFIX])
soup = self._customized_fetch_url(url, exceptions.StoryDoesNotExist)
pagetitle_div = soup.find('div', id='pagetitle')
self.story.setMetadata('title', pagetitle_div.a.string)
author_anchor = pagetitle_div.a.findNextSibling('a')
url = urlparse.urljoin(self.BASE_URL, author_anchor['href'])
components = urlparse.urlparse(url)
query_data = urlparse.parse_qs(components.query)
self.story.setMetadata('author', author_anchor.string)
self.story.setMetadata('authorId', query_data['uid'])
self.story.setMetadata('authorUrl', url)
metadata = {}
for b_tag in soup.find('div', {'class': 'listbox'})('b'):
key = b_tag.string.strip()[:-1]
value = b_tag.nextSibling.string.strip()
if key == 'Category':
for sibling in b_tag.findNextSiblings(['a', 'br']):
if sibling.name == 'br':
break
self.story.addToList('category', sibling.string)
elif key == 'Characters':
for sibling in b_tag.findNextSiblings(['a', 'br']):
if sibling.name == 'br':
break
self.story.addToList('characters', sibling.string)
elif key == 'Series':
a = b_tag.findNextSibling('a')
if not a:
continue
self.story.setMetadata('series', a.string)
self.story.setMetadata('seriesUrl', urlparse.urljoin(self.BASE_URL, a['href']))
else:
metadata[key] = value
self.story.setMetadata('description', metadata['Summary'])
self.story.setMetadata('rating', metadata['Rating'])
self.story.setMetadata('numChapters', int(metadata['Chapter']))
self.story.setMetadata('status', 'Completed' if metadata['Completed'] == 'Yes' else 'In-Progress')
self.story.setMetadata('numWords', metadata['Words'])
self.story.setMetadata('datePublished', makeDate(metadata['Published'], self.DATETIME_FORMAT))
self.story.setMetadata('dateUpdated', makeDate(metadata['Updated'], self.DATETIME_FORMAT))
for b_tag in soup.find('div', id='output').findNextSiblings('b'):
chapter_anchor = b_tag.a
title = chapter_anchor.string
url = urlparse.urljoin(self.BASE_URL, chapter_anchor['href'])
self.chapterUrls.append((title, url))
def getChapterText(self, url):
url += self.AGE_CONSENT_URL_SUFFIX
soup = self._customized_fetch_url(url, exceptions.FailedToDownload)
return self.utf8FromSoup(url, soup.find('div', id='story'))
+2 -12
View File
@@ -46,13 +46,6 @@
{{yourfile}}
<!-- </div> -->
<h3>fanfiction.net / fimfiction.net</h3>
<p>
As of Jan 13, 2014, fanfiction.net &amp; fimfiction.net
are working again. I'd ask that users limit the number of
stories they download from those sites, thanks.
</p>
{% if authorized %}
<form action="/fdown" method="post">
<div id='urlbox'>
@@ -64,10 +57,7 @@
<h3>Changes:</h3>
<p>
<ul>
<li>Allow https URLs for fimfiction.net.</li>
<li>Both allow https URLs for squidge.org/peja and change canonical URLs for squidge.org/peja to https.</li>
<li>Fix for some stories' summaries on onedirectionfanfiction.com.</li>
<li>Add include/exclude metadata feature.</li>
<li>New site: voracity2.e-fic.com -- Thanks, cryzed!</li>
</ul>
</p>
<p>
@@ -78,7 +68,7 @@
If you have any problems with this application, please
report them in
the <a href="http://groups.google.com/group/fanfic-downloader">FanFictionDownLoader Google Group</a>. The
<a href="http://4-4-99.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
<a href="http://4-5-02.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
</p>
<div id='error'>
{{ error_message }}
-6
View File
@@ -51,12 +51,6 @@
by {{ fic.author }} ({{ fic.format }})
{% endif %}
{% if fic.failure %}
<h3>fanfiction.net / fimfiction.net</h3>
<p>
As of Jan 13, 2014, fanfiction.net &amp; fimfiction.net
are working again. I'd ask that users limit the number of
stories they download from those sites, thanks.
</p>
<span id='error'>{{ fic.failure }}</span>
{% endif %}
{% if not fic.completed and not fic.failure %}