mirror of
https://github.com/wassname/FanFicFare.git
synced 2026-09-14 11:14:10 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1a8b69f50 | ||
|
|
1e66108506 | ||
|
|
5ec09f3936 | ||
|
|
3f005cdfbd | ||
|
|
17157498a9 | ||
|
|
191e13bfad | ||
|
|
4bd288c3ac | ||
|
|
95c2cbb77b | ||
|
|
13e5615dea | ||
|
|
1553bd2fac | ||
|
|
4ac6c3dfdd | ||
|
|
9a066be859 | ||
|
|
39bfc6632c |
@@ -4,7 +4,7 @@ from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2015, Jim Miller'
|
||||
__copyright__ = '2016, Jim Miller'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import sys
|
||||
@@ -42,7 +42,7 @@ class FanFicFareBase(InterfaceActionBase):
|
||||
description = _('UI plugin to download FanFiction stories from various sites.')
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
author = 'Jim Miller'
|
||||
version = (2, 2, 16)
|
||||
version = (2, 2, 17)
|
||||
minimum_calibre_version = (1, 48, 0)
|
||||
|
||||
#: This field defines the GUI plugin class that contains all the code
|
||||
|
||||
@@ -608,8 +608,9 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
status_prefix=_("Books UnNewed"))
|
||||
|
||||
def get_unnew_books_loop(self,book,db=None,tdir=None):
|
||||
|
||||
if book['calibre_id'] and db.has_format(book['calibre_id'],'EPUB',index_is_id=True):
|
||||
|
||||
book['changed']=False
|
||||
if book['calibre_id'] and db.has_format(book['calibre_id'],'EPUB',index_is_id=True) and self.get_story_url(db,book['calibre_id']):
|
||||
tmp = PersistentTemporaryFile(prefix='%s-'%book['calibre_id'],
|
||||
suffix='.epub',
|
||||
dir=tdir)
|
||||
|
||||
@@ -380,11 +380,9 @@ sort_ships:false
|
||||
## ships entry and then, after => the string to use to merge the parts
|
||||
## back together. \s == blank space.
|
||||
## Each part will have replace_metadata with key ships_CHARS applied.
|
||||
## By default, sort_ships_splits:/=>/
|
||||
## Here's possible an example for AO3:
|
||||
#sort_ships_splits:
|
||||
# [ ]*/[ ]*=>/
|
||||
# [ ]*&[ ]*=>\s&\s
|
||||
sort_ships_splits:
|
||||
[ ]*/[ ]*=>/
|
||||
[ ]*&[ ]*=>\s&\s
|
||||
|
||||
## join_string_<entry> options -- FanFicFare list entries are comma
|
||||
## separated by default. You can use this to change that. For example,
|
||||
@@ -1482,12 +1480,6 @@ extraships:Sam/Dean
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
|
||||
[sg1-heliopolis.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[sheppardweir.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -2004,9 +1996,18 @@ extracategories:Harry Potter
|
||||
extraships:Harry Potter/Ginny Weasley
|
||||
|
||||
[www.potionsandsnitches.org]
|
||||
extra_valid_entries:stars,reviews,reads,takesplaces,snapeflavours,sitetags
|
||||
stars_label:Frogs
|
||||
takesplaces_label:Takes Place
|
||||
snapeflavours_label:Snape Flavour
|
||||
sitetags_labels:Site Tags
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
|
||||
add_to_include_subject_tags:,takesplaces,snapeflavours,sitetags
|
||||
#add_to_extra_titlepage_entries:,stars,reviews,reads,takesplaces,snapeflavours,sitetags
|
||||
|
||||
[www.potterfics.com]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
@@ -2030,11 +2031,15 @@ extracategories:Psych
|
||||
extracategories:Queer as Folk
|
||||
|
||||
[quotev.com]
|
||||
extra_valid_entries:pages,readers,reads,favorites
|
||||
extra_valid_entries:pages,readers,reads,favorites,searchtags,comments
|
||||
pages_label:Pages
|
||||
readers_label:Readers
|
||||
reads_label:Reads
|
||||
favorites_label:Favorites
|
||||
searchtags:Search Tags
|
||||
comments:Comments
|
||||
|
||||
include_in_category:category,searchtags
|
||||
|
||||
[www.restrictedsection.org]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
@@ -2172,6 +2177,9 @@ extracategories:Twilight
|
||||
collect_series: false
|
||||
|
||||
[www.walkingtheplank.org]
|
||||
extra_valid_entries:reads
|
||||
reads_label:Read Count
|
||||
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
|
||||
@@ -5,15 +5,15 @@
|
||||
# Ettore Atalan <atalanttore@googlemail.com>, 2014-2015
|
||||
# ILB, 2014-2015
|
||||
# Sebastian Keller <Haggard@gmx.de>, 2015
|
||||
# Simon_Schuette <simonschuette@arcor.de>, 2014-2015
|
||||
# Simon_Schuette <simonschuette@arcor.de>, 2014-2016
|
||||
# Simon S, 2015
|
||||
# Valsu <valsu@hylia.de>, 2015
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: calibre-plugins\n"
|
||||
"POT-Creation-Date: 2015-12-13 10:41+Central Standard Time\n"
|
||||
"PO-Revision-Date: 2015-12-14 09:49+0000\n"
|
||||
"Last-Translator: Kovid Goyal <kovid@kovidgoyal.net>\n"
|
||||
"PO-Revision-Date: 2016-01-04 19:34+0000\n"
|
||||
"Last-Translator: Simon_Schuette <simonschuette@arcor.de>\n"
|
||||
"Language-Team: German (http://www.transifex.com/calibre/calibre-plugins/language/de/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -981,7 +981,7 @@ msgstr ""
|
||||
|
||||
#: config.py:1343
|
||||
msgid "Set Calibre Author URL"
|
||||
msgstr ""
|
||||
msgstr "Calibre Autor URL festlegen"
|
||||
|
||||
#: config.py:1344
|
||||
msgid "Set Calibre Author URL to Author's URL on story site."
|
||||
@@ -1944,7 +1944,7 @@ msgstr "Fehler beim Herunterladen der Metadaten"
|
||||
msgid ""
|
||||
"An error has occurred while FanFicFare was updating calibre's metadata for "
|
||||
"<a href='%s'>%s</a>."
|
||||
msgstr ""
|
||||
msgstr "Während FanFicFare die Calibre-Metadaten für <a href='%s'>%s</a> aktualisierte, trat ein Fehler auf."
|
||||
|
||||
#: fff_plugin.py:1517
|
||||
msgid "The ebook has been updated, but the metadata has not."
|
||||
@@ -2094,7 +2094,7 @@ msgstr "FanFiction-Geschichten herunterladen"
|
||||
|
||||
#: jobs.py:94
|
||||
msgid "Download Results:"
|
||||
msgstr ""
|
||||
msgstr "Download Ergebnisse:"
|
||||
|
||||
#: jobs.py:96
|
||||
msgid "Successful:"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -75,7 +75,6 @@ import adapter_ncisfictionnet
|
||||
import adapter_thealphagatecom
|
||||
import adapter_fanfiktionde
|
||||
import adapter_ponyfictionarchivenet
|
||||
import adapter_sg1heliopoliscom
|
||||
import adapter_ncisficcom
|
||||
import adapter_nationallibrarynet
|
||||
import adapter_themasquenet
|
||||
|
||||
@@ -126,6 +126,9 @@ class PotionsAndSnitchesOrgSiteAdapter(BaseSiteAdapter):
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
if 'Read' in label:
|
||||
self.story.setMetadata('reads', value)
|
||||
|
||||
if 'Categories' in label:
|
||||
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
catstext = [cat.string for cat in cats]
|
||||
@@ -142,12 +145,30 @@ class PotionsAndSnitchesOrgSiteAdapter(BaseSiteAdapter):
|
||||
else:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
if 'Warning' in label:
|
||||
warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class'))
|
||||
for warning in warnings:
|
||||
self.story.addToList('warnings',stripHTML(warning))
|
||||
|
||||
if 'Genre' in label:
|
||||
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class'))
|
||||
genrestext = [genre.string for genre in genres]
|
||||
self.genre = ', '.join(genrestext)
|
||||
for genre in genrestext:
|
||||
self.story.addToList('genre',genre.string)
|
||||
for genre in genres:
|
||||
self.story.addToList('genre',stripHTML(genre))
|
||||
|
||||
if 'Takes Place' in label:
|
||||
takesplaces = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class'))
|
||||
for takesplace in takesplaces:
|
||||
self.story.addToList('takesplaces',stripHTML(takesplace))
|
||||
|
||||
if 'Snape flavour' in label:
|
||||
snapeflavours = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class'))
|
||||
for snapeflavour in snapeflavours:
|
||||
self.story.addToList('snapeflavours',stripHTML(snapeflavour))
|
||||
|
||||
if 'Tags' in label:
|
||||
sitetags = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class'))
|
||||
for sitetag in sitetags:
|
||||
self.story.addToList('sitetags',stripHTML(sitetag))
|
||||
|
||||
if 'Completed' in label:
|
||||
if 'Yes' in value:
|
||||
@@ -183,6 +204,14 @@ class PotionsAndSnitchesOrgSiteAdapter(BaseSiteAdapter):
|
||||
# I find it hard to care if the series parsing fails
|
||||
pass
|
||||
|
||||
divsort = soup.find('div',id='sort')
|
||||
stars = len(divsort.find_all('img',src='images/star.gif'))
|
||||
stars = stars + 0.5 * len(divsort.find_all('img',src='images/starhalf.gif'))
|
||||
self.story.setMetadata('stars',stars)
|
||||
|
||||
a = divsort.find_all('a', href=re.compile(r'reviews.php\?type=ST&(amp;)?item='+self.story.getMetadata('storyId')+"$"))[1] # second one.
|
||||
self.story.setMetadata('reviews',stripHTML(a))
|
||||
|
||||
|
||||
def getChapterText(self, url):
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import datetime
|
||||
|
||||
from .. import exceptions
|
||||
from base_adapter import BaseSiteAdapter
|
||||
from ..htmlcleanup import stripHTML
|
||||
|
||||
SITE_DOMAIN = 'quotev.com'
|
||||
STORY_URL_TEMPLATE = 'http://www.quotev.com/story/%s'
|
||||
@@ -53,76 +54,78 @@ class QuotevComAdapter(BaseSiteAdapter):
|
||||
|
||||
soup = self.make_soup(data)
|
||||
|
||||
element = soup.find('div', {'class': 'result_head'})
|
||||
element = soup.find('div', {'class': 'result'})
|
||||
if not element:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
|
||||
self.story.setMetadata('title', element.find('span', recursive=False).get_text())
|
||||
self.story.setMetadata('title', element.find('h1').get_text())
|
||||
|
||||
element = soup.find('div', {'class': 'desc_creator'})
|
||||
if element:
|
||||
a = element('a')[1]
|
||||
self.story.setMetadata('author', a.get_text())
|
||||
self.story.setMetadata('authorId', get_url_path_segments(a['href'])[0])
|
||||
self.story.setMetadata('authorUrl', urlparse.urljoin(self.url, a['href']))
|
||||
|
||||
# Multiple authors
|
||||
else:
|
||||
element = soup.find('div', id='qheadx')
|
||||
for a in element('div', recursive=False)[1]('a'):
|
||||
author = a.get_text()
|
||||
if not a.get_text():
|
||||
continue
|
||||
|
||||
self.story.addToList('author', author)
|
||||
self.story.addToList('authorId', get_url_path_segments(a['href'])[0])
|
||||
# quotev html is all about formatting without any content tagging
|
||||
authdiv = soup.find('div', {'style':"text-align:left;"})
|
||||
if authdiv:
|
||||
#print("div:%s"%authdiv.find_all('a'))
|
||||
for a in authdiv.find_all('a'):
|
||||
self.story.addToList('author', a.get_text())
|
||||
self.story.addToList('authorId', a['href'].split('/')[-1])
|
||||
self.story.addToList('authorUrl', urlparse.urljoin(self.url, a['href']))
|
||||
else:
|
||||
self.story.setMetadata('author','Anonymous')
|
||||
self.story.setMetadata('authorUrl','http://www.quotev.com')
|
||||
self.story.setMetadata('authorId','0')
|
||||
|
||||
self.setDescription(self.url, soup.find('div', id='qdesct'))
|
||||
self.setCoverImage(self.url, urlparse.urljoin(self.url, soup.find('img', {'class': 'logo'})['src']))
|
||||
|
||||
for a in soup.find('div', {'class': 'tag'})('a'):
|
||||
if a['href'] == '#':
|
||||
continue
|
||||
|
||||
for a in soup.find_all('a', {'href': re.compile(SITE_DOMAIN+'/stories/c/')}):
|
||||
self.story.addToList('category', a.get_text())
|
||||
|
||||
elements = soup('span', {'class': 'q_time'})
|
||||
for a in soup.find_all('a', {'href': re.compile(SITE_DOMAIN+'/search/')}):
|
||||
self.story.addToList('searchtags', a.get_text())
|
||||
|
||||
elements = soup.find_all('span', {'class': 'q_time'})
|
||||
self.story.setMetadata('datePublished', datetime.datetime.fromtimestamp(float(elements[0]['ts'])))
|
||||
if len(elements) > 1:
|
||||
self.story.setMetadata('dateUpdated', datetime.datetime.fromtimestamp(float(elements[1]['ts'])))
|
||||
|
||||
metadiv = elements[0].parent
|
||||
|
||||
if 'completed' in stripHTML(metadiv):
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
data = filter(None, (x.strip() for x in stripHTML(metadiv).split(u'\xb7')))
|
||||
|
||||
for datum in data:
|
||||
parts = datum.split()
|
||||
if len(parts) < 2 or parts[1] not in self.getConfig('extra_valid_entries'):
|
||||
continue
|
||||
# Not a valid metadatum
|
||||
# if not len(parts) == 2:
|
||||
# continue
|
||||
|
||||
key, value = parts[1], parts[0]
|
||||
self.story.setMetadata(key, value.replace(',', '').replace('.', ''))
|
||||
|
||||
favspans = soup.find('a',{'id':'fav_btn'}).find_all('span')
|
||||
if len(favspans) > 1:
|
||||
self.story.setMetadata('favorites', stripHTML(favspans[1]).replace(',', ''))
|
||||
|
||||
commentspans = soup.find('a',{'id':'comment_btn'}).find_all('span')
|
||||
#print("commentspans:%s"%commentspans)
|
||||
if len(commentspans) > 0:
|
||||
self.story.setMetadata('comments', stripHTML(commentspans[0]).replace(',', ''))
|
||||
|
||||
for a in soup.find('div', id='rselect')('a'):
|
||||
self.chapterUrls.append((a.get_text(), urlparse.urljoin(self.url, a['href'])))
|
||||
|
||||
self.story.setMetadata('numChapters', len(self.chapterUrls))
|
||||
|
||||
element = soup.find('div', {'class': 't'})('div', recursive=False)[1].div
|
||||
data = filter(None, (x.strip() for x in element.get_text().split(u'\xb7')))
|
||||
if 'completed' in data:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
data.remove('completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
for datum in data:
|
||||
parts = datum.split()
|
||||
# Not a valid metadatum
|
||||
if not len(parts) == 2:
|
||||
continue
|
||||
|
||||
key, value = parts
|
||||
self.story.setMetadata(key, value.replace(',', '').replace('.', ''))
|
||||
|
||||
self.story.setMetadata('favorites', soup.find('div', id='favqn').get_text())
|
||||
element = soup.find('a', id='comment_btn').span
|
||||
self.story.setMetadata('comments', element.get_text() if element else 0)
|
||||
|
||||
|
||||
def getChapterText(self, url):
|
||||
data = self._fetchUrl(url)
|
||||
soup = self.make_soup(data)
|
||||
|
||||
element = soup.find('div', id='restxt')
|
||||
element = soup.find('div', id='rescontent')
|
||||
for a in element('a'):
|
||||
a.unwrap()
|
||||
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2012 Fanficdownloader team, 2015 FanFicFare 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.
|
||||
#
|
||||
|
||||
# Software: eFiction
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
|
||||
def getClass():
|
||||
return SG1HeliopolisComAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class SG1HeliopolisComAdapter(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])
|
||||
|
||||
self.section=self.parsedUrl.path.split('/',)[1]
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/'+self.section+'/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','sghp')
|
||||
|
||||
# If all stories from the site fall into the same category,
|
||||
# the site itself isn't likely to label them as such, so we
|
||||
# do. Can't use extracategories, could be Atlantis or SG-1
|
||||
if 'atlantis' in self.section:
|
||||
self.story.addToList("category","Stargate: Atlantis")
|
||||
else:
|
||||
self.story.addToList("category","Stargate: SG-1")
|
||||
|
||||
# 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 'sg1-heliopolis.com'
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/archive/viewstory.php?sid=1234 http://"+cls.getSiteDomain()+"/adult/viewstory.php?sid=1234 http://"+cls.getSiteDomain()+"/atlantis/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return r"http://sg1-heliopolis.com/(archive|adult|atlantis)?/viewstory.php\?sid=\d+$"
|
||||
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
# Weirdly, different sites use different warning numbers.
|
||||
# If the title search below fails, there's a good chance
|
||||
# you need a different number. print data at that point
|
||||
# and see what the 'click here to continue' url says.
|
||||
addurl = "&warning=4"
|
||||
else:
|
||||
addurl=""
|
||||
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data)
|
||||
if m != None:
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
# We tried the default and still got a warning, so
|
||||
# let's pull the warning number from the 'continue'
|
||||
# link and reload data.
|
||||
addurl = m.group(1)
|
||||
# correct stupid & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
url = self.url+'&index=1'+addurl
|
||||
logger.debug("URL 2nd try: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
else:
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = self.make_soup(data)
|
||||
# print data
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
## Title
|
||||
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
|
||||
self.story.setMetadata('title',stripHTML(a))
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
|
||||
self.story.setMetadata('authorId',a['href'].split('=')[1])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href'])
|
||||
self.story.setMetadata('author',a.string)
|
||||
|
||||
# Find the chapters:
|
||||
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")):
|
||||
# just in case there's tags, like <i> in chapter titles.
|
||||
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+self.section+'/'+chapter['href']+addurl))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
# eFiction sites don't help us out a lot with their meta data
|
||||
# formating, so it's a little ugly.
|
||||
|
||||
# utility method
|
||||
def defaultGetattr(d,k):
|
||||
try:
|
||||
return d[k]
|
||||
except:
|
||||
return ""
|
||||
|
||||
# <span class="label">Rated:</span> NC-17<br /> etc
|
||||
labels = soup.findAll('span',{'class':'label'})
|
||||
for labelspan in labels:
|
||||
value = labelspan.nextSibling
|
||||
label = labelspan.string
|
||||
|
||||
if 'Summary' in label:
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while 'label' not in defaultGetattr(value,'class'):
|
||||
svalue += unicode(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
if 'Rated' in label:
|
||||
self.story.setMetadata('rating', value)
|
||||
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
if 'Categories' in label:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
if 'Published' in label:
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
if 'Updated' in label:
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
try:
|
||||
# Find Series name from series URL.
|
||||
a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+"))
|
||||
series_name = a.string
|
||||
series_url = 'http://'+self.host+'/'+self.section+'/'+a['href']
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
seriessoup = self.make_soup(self._fetchUrl(series_url))
|
||||
# can't use ^viewstory...$ in case of higher rated stories with javascript href.
|
||||
storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+'))
|
||||
i=1
|
||||
for a in storyas:
|
||||
# skip 'report this' and 'TOC' links
|
||||
if 'contact.php' not in a['href'] and 'index' not in a['href']:
|
||||
if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
|
||||
self.setSeries(series_name, i)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
i+=1
|
||||
|
||||
except:
|
||||
# I find it hard to care if the series parsing fails
|
||||
pass
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
soup = self.make_soup(self._fetchUrl(url))
|
||||
|
||||
div = soup.find('div', {'id' : 'story'})
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
@@ -3,7 +3,7 @@ import re
|
||||
import urllib2
|
||||
import urlparse
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from bs4.element import Tag
|
||||
from ..htmlcleanup import stripHTML
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
@@ -98,7 +98,7 @@ class SpikeluverComAdapter(BaseSiteAdapter):
|
||||
query_data = urlparse.parse_qs(components.query)
|
||||
|
||||
self.story.setMetadata('author', stripHTML(author_anchor))
|
||||
self.story.setMetadata('authorId', query_data['uid'])
|
||||
self.story.setMetadata('authorId', query_data['uid'][0])
|
||||
self.story.setMetadata('authorUrl', url)
|
||||
|
||||
sort_div = soup.find('div', id='sort')
|
||||
@@ -122,7 +122,7 @@ class SpikeluverComAdapter(BaseSiteAdapter):
|
||||
keep_summary_html = self.getConfig('keep_summary_html')
|
||||
|
||||
for sibling in _yield_next_siblings(span_tag):
|
||||
if isinstance(sibling, BeautifulSoup.Tag):
|
||||
if isinstance(sibling, Tag):
|
||||
# Encountered next label, break. Not as bad as other
|
||||
# e-fiction sites, let's hope this is enough for proper
|
||||
# parsing.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2011 Fanficdownloader team, 2015 FanFicFare team
|
||||
# Copyright 2011 Fanficdownloader team, 2016 FanFicFare team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -172,7 +172,7 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
|
||||
descurl=authorurl
|
||||
authorsoup = self.make_soup(authordata)
|
||||
# author can have several pages, scan until we find it.
|
||||
while( not authorsoup.find('a', href=re.compile(r"^/Story-"+self.story.getMetadata('storyId'))) ):
|
||||
while( not authorsoup.find('a', href=re.compile(r"^/Story-"+self.story.getMetadata('storyId')+'/')) ):
|
||||
nextarrow = authorsoup.find('a', {'class':'arrowf'})
|
||||
if not nextarrow:
|
||||
## if rating is set lower than story, it won't be
|
||||
|
||||
@@ -138,7 +138,7 @@ class Voracity2EficComAdapter(BaseSiteAdapter):
|
||||
query_data = urlparse.parse_qs(components.query)
|
||||
|
||||
self.story.setMetadata('author', author_anchor.string)
|
||||
self.story.setMetadata('authorId', query_data['uid'])
|
||||
self.story.setMetadata('authorId', query_data['uid'][0])
|
||||
self.story.setMetadata('authorUrl', url)
|
||||
|
||||
sort_div = soup.find('div', id='sort')
|
||||
|
||||
@@ -161,6 +161,9 @@ class WalkingThePlankOrgAdapter(BaseSiteAdapter):
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
if 'Read' in label:
|
||||
self.story.setMetadata('reads', value)
|
||||
|
||||
if 'Categories' in label:
|
||||
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
catstext = [cat.string for cat in cats]
|
||||
|
||||
@@ -168,7 +168,14 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
threadmarksa = soup.find('a',{'class':'threadmarksTrigger'})
|
||||
if threadmarksa:
|
||||
soupmarks = self.make_soup(self._fetchUrl(self.getURLPrefix()+'/'+threadmarksa['href']))
|
||||
markas = soupmarks.find('ol',{'class':'overlayScroll'}).find_all('a')
|
||||
markas = []
|
||||
ol = soupmarks.find('ol',{'class':'overlayScroll'})
|
||||
if ol:
|
||||
markas = ol.find_all('a')
|
||||
else:
|
||||
## SV changed their threadmarks. Not isolated to
|
||||
## SV only incase SB or QQ make the same change.
|
||||
markas = soupmarks.find('div',{'class':'threadmarks'}).find_all('a',{'class':'PreviewTooltip'})
|
||||
if len(markas) > 1:
|
||||
for (atag,url,name) in [ (x,x['href'],stripHTML(x)) for x in markas ]:
|
||||
date = self.make_date(atag.find_next_sibling('div',{'class':'extra'}))
|
||||
|
||||
+20
-12
@@ -379,11 +379,9 @@ sort_ships:false
|
||||
## ships entry and then, after => the string to use to merge the parts
|
||||
## back together. \s == blank space.
|
||||
## Each part will have replace_metadata with key ships_CHARS applied.
|
||||
## By default, sort_ships_splits:/=>/
|
||||
## Here's possible an example for AO3:
|
||||
#sort_ships_splits:
|
||||
# [ ]*/[ ]*=>/
|
||||
# [ ]*&[ ]*=>\s&\s
|
||||
sort_ships_splits:
|
||||
[ ]*/[ ]*=>/
|
||||
[ ]*&[ ]*=>\s&\s
|
||||
|
||||
## join_string_<entry> options -- FanFicFare list entries are comma
|
||||
## separated by default. You can use this to change that. For example,
|
||||
@@ -1470,12 +1468,6 @@ extraships:Sam/Dean
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
|
||||
[sg1-heliopolis.com]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
#is_adult:true
|
||||
|
||||
[sheppardweir.com]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
@@ -1986,9 +1978,18 @@ extracategories:Harry Potter
|
||||
extraships:Harry Potter/Ginny Weasley
|
||||
|
||||
[www.potionsandsnitches.org]
|
||||
extra_valid_entries:stars,reviews,reads,takesplaces,snapeflavours,sitetags
|
||||
stars_label:Frogs
|
||||
takesplaces_label:Takes Place
|
||||
snapeflavours_label:Snape Flavour
|
||||
sitetags_labels:Site Tags
|
||||
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
|
||||
add_to_include_subject_tags:,takesplaces,snapeflavours,sitetags
|
||||
#add_to_extra_titlepage_entries:,stars,reviews,reads,takesplaces,snapeflavours,sitetags
|
||||
|
||||
[www.potterfics.com]
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Harry Potter
|
||||
@@ -2012,11 +2013,15 @@ extracategories:Psych
|
||||
extracategories:Queer as Folk
|
||||
|
||||
[quotev.com]
|
||||
extra_valid_entries:pages,readers,reads,favorites
|
||||
extra_valid_entries:pages,readers,reads,favorites,searchtags,comments
|
||||
pages_label:Pages
|
||||
readers_label:Readers
|
||||
reads_label:Reads
|
||||
favorites_label:Favorites
|
||||
searchtags:Search Tags
|
||||
comments:Comments
|
||||
|
||||
include_in_category:category,searchtags
|
||||
|
||||
[www.restrictedsection.org]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
@@ -2154,6 +2159,9 @@ extracategories:Twilight
|
||||
collect_series: false
|
||||
|
||||
[www.walkingtheplank.org]
|
||||
extra_valid_entries:reads
|
||||
reads_label:Read Count
|
||||
|
||||
## Some sites do not require a login, but do require the user to
|
||||
## confirm they are adult for adult content. In commandline version,
|
||||
## this should go in your personal.ini, not defaults.ini.
|
||||
|
||||
+11
-2
@@ -816,10 +816,15 @@ class Story(Configurable):
|
||||
for x in retlist:
|
||||
curlist = []
|
||||
for y in re.split(splitre,x):
|
||||
## logger.debug("x:(%s) y:(%s)"%(x,y))
|
||||
## for SPLIT_META(\,)
|
||||
if doreplacements:
|
||||
if x != y and doreplacements: # doreplacements always true here (currently)
|
||||
y = self.doReplacements(y,'ships_CHARS',return_list=True)
|
||||
else:
|
||||
## needs to be a list to extend curlist.
|
||||
y=[x]
|
||||
curlist.extend(y)
|
||||
## logger.debug("curlist:%s"%(curlist,))
|
||||
newretlist.append( splitmerge.join(sorted(curlist)) )
|
||||
|
||||
retlist = newretlist
|
||||
@@ -1109,4 +1114,8 @@ def commaGroups(s):
|
||||
def unique_list(seq):
|
||||
seen = set()
|
||||
seen_add = seen.add
|
||||
return [x for x in seq if not (x in seen or seen_add(x))]
|
||||
try:
|
||||
return [x for x in seq if not (x in seen or seen_add(x))]
|
||||
except:
|
||||
print("unique_list exception seq:%s"%seq)
|
||||
raise
|
||||
|
||||
@@ -23,7 +23,7 @@ setup(
|
||||
# Versions should comply with PEP440. For a discussion on single-sourcing
|
||||
# the version across setup.py and the project code, see
|
||||
# https://packaging.python.org/en/latest/single_source_version.html
|
||||
version="2.2.166",
|
||||
version="2.2.17",
|
||||
|
||||
description='A tool for downloading fanfiction to eBook formats',
|
||||
long_description=long_description,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# ffd-retief-hrd fanficfare
|
||||
application: fanficfare
|
||||
version: 2-2-16
|
||||
version: 2-2-17
|
||||
runtime: python27
|
||||
api_version: 1
|
||||
threadsafe: true
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
If you have any problems with this application, please
|
||||
report them in
|
||||
the <a href="http://groups.google.com/group/fanfic-downloader">FanFicFare Google Group</a>. The
|
||||
<a href="http://2-2-15.fanficfare.appspot.com">previous version
|
||||
<a href="http://2-2-16.fanficfare.appspot.com">previous version
|
||||
</a> is also available for you to use if necessary.
|
||||
</p>
|
||||
<div id='error'>
|
||||
|
||||
Reference in New Issue
Block a user