Compare commits

...
28 Commits
Author SHA1 Message Date
M Clark a0b4332da8 better description selector 2016-10-28 15:07:25 +08:00
M Clark a1bd9c8379 avoid stripping html from description 2016-10-27 12:18:53 +08:00
Jim Miller e02c969371 Bump test version. 2016-10-26 11:02:35 -05:00
Jim Miller b18b2177b0 Merge pull request #146 from wassname/master
fix list index out of range error
2016-10-26 11:01:40 -05:00
gitea 5ca33837d4 fix list index out of range error
- fixes "Failed List Index Out of Range" on url
"http://royalroadl.com/fiction/1612"
2016-10-26 14:22:10 +08:00
Jim Miller eade66f513 Bump test version. 2016-10-25 19:38:08 -05:00
Jim Miller 28c4557d22 Allow '_u#.xhtml' file names in updates. For Calibre Convert on Anthologies, then manually split. 2016-10-25 19:37:27 -05:00
Jim Miller d6eda82767 Change royalroadl rating to stars to match usual rating definition. 2016-10-24 12:17:58 -05:00
Jim Miller 28eff8ac12 Update inis and bump test version for royalroadl.com 2016-10-24 12:07:55 -05:00
Jim Miller 7ae40e539d Merge pull request #144 from wassname/master
Add royalroad adapter,  Thanks @wassname
2016-10-24 12:03:53 -05:00
gitea ab6436ca0b add royalroad adapter 2016-10-24 12:35:22 +08:00
Jim Miller b5a04b0f97 Remove reviews count from wraithbait.com, no longer displayed. 2016-10-22 12:44:08 -05:00
Jim Miller c7bbb765b2 Change default for post_process_apply_filename_safepattern to false. 2016-10-20 14:28:12 -05:00
Jim Miller 8a84043d29 Allow --infile missing last newline. 2016-10-20 14:13:09 -05:00
Jim Miller f115d68e52 Add post_process_apply_filename_safepattern setting (default:true). 2016-10-20 14:03:04 -05:00
Jim Miller e2707f5459 Bump versions to 2.5.0. 2016-10-18 10:47:00 -05:00
Jim Miller aa85efd6d9 Update translations. 2016-10-18 10:45:32 -05:00
Jim Miller 9e2c0a3563 Correct a typo in an ini comment. 2016-10-18 10:44:17 -05:00
Jim Miller d4f3fee053 Fix ini comments for internalize_text_links 2016-10-06 23:50:09 -05:00
Jim Miller e87c7b7009 Bump micro version for test version. 2016-10-06 23:23:48 -05:00
Jim Miller 5b6228166c Update translations. 2016-10-06 23:23:32 -05:00
Jim Miller f91092d9d8 Add internalize_text_links option for epub and html formats. Also fix for growing whitespace in epub updates and missing always_reload_first_chapter in highlight list. 2016-10-06 23:21:01 -05:00
Jim Miller a40383bada Adding normalize_chapterurl() for xenforoforum and normalize_text_links option. 2016-10-06 20:56:37 -05:00
Jim Miller c9205dd6bc Tiny fix to html output--close TOCTOP <a> tag. 2016-10-06 20:22:44 -05:00
Jim Miller a0acbb8893 Fix for CLI not working correctly with -u epubfile 2016-10-05 17:46:56 -05:00
Jim Miller 7d66d93b70 Adding words_added metadata for epub logpage only. 2016-10-05 17:33:26 -05:00
Jim Miller 277b1ef92d Fix for unusual (changed?) chapter URLs on storiesonline.net. 2016-10-02 12:57:02 -05:00
Jim Miller 72217423c7 Fix CLI post_process_cmd to use processed metadata instead of raw. 2016-10-01 12:28:53 -05:00
20 changed files with 576 additions and 215 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ class FanFicFareBase(InterfaceActionBase):
description = _('UI plugin to download FanFiction stories from various sites.')
supported_platforms = ['windows', 'osx', 'linux']
author = 'Jim Miller'
version = (2, 4, 0)
version = (2, 5, 3)
minimum_calibre_version = (1, 48, 0)
#: This field defines the GUI plugin class that contains all the code
+21 -3
View File
@@ -150,7 +150,7 @@ extratags: FanFiction
## Can also be used for other metadata values
#default_value_category:FanFiction
## number of seconds to sleep between calls to the story site. May by
## number of seconds to sleep between calls to the story site. May be
## useful if pulling large numbers of stories or if the site is slow.
#slow_down_sleep_time:0.5
@@ -720,6 +720,18 @@ remove_transparency: true
## true--replace_br_with_p also fixes the problem.
nook_img_fix:true
## Apply adapter's normalize_chapterurl() to all links in chapter
## texts, if they match chapter URLs. Currently only implemented by
## base_xenforoforum adapters.
#normalize_text_links:false
## Search all links in chapter texts and, if they match any included
## chapter URLs, replace them with links to the chapter in the
## download. Only works with epub and html output formats.
## base_xenforoforum adapters should also use normalize_text_links
## with this.
#internalize_text_links:false
[mobi]
## mobi TOC cannot be turned off right now.
#include_tocpage: true
@@ -1563,6 +1575,11 @@ comments_label:Comments
include_in_category:category,searchtags
[royalroadl.com]
extra_valid_entries:stars
#add_to_extra_titlepage_entries:,stars
[samandjack.net]
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
@@ -2334,8 +2351,9 @@ extracharacters:Wolverine,Rogue
## Site dedicated to these categories/characters/ships
extracategories:Stargate: Atlantis
extra_valid_entries:reviews
reviews_label:Reviews
##site stopped showing reviews ~ Oct 2016
#extra_valid_entries:reviews
#reviews_label:Reviews
[buffygiles.velocitygrass.com]
## Some sites do not require a login, but do require the user to
+4 -4
View File
@@ -13,8 +13,8 @@ msgid ""
msgstr ""
"Project-Id-Version: calibre-plugins\n"
"POT-Creation-Date: 2016-09-18 17:33+Central Daylight Time\n"
"PO-Revision-Date: 2016-09-19 12:10+0000\n"
"Last-Translator: dario hereñu <magallania@gmail.com>\n"
"PO-Revision-Date: 2016-09-30 17:28+0000\n"
"Last-Translator: Jellby <jellby@yahoo.com>\n"
"Language-Team: Spanish (http://www.transifex.com/calibre/calibre-plugins/language/es/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -239,7 +239,7 @@ msgid ""
"Check for existing Series Anthology books using each new story's series URL before downloading.\n"
"Offer to skip downloading if a Series Anthology is found.\n"
"Doesn't work when Collect Metadata in Background is selected."
msgstr "Comprobar si existen libros de antologías de serie usando el URL de la serie de cada nueva historia antes de descargar.\nOfrece omitir su descarga si se encuentra una antología de serie.\nNo funciona cuando la recopilación de metadatos en segundo plano está activada."
msgstr "Comprobar si existen libros de antología de serie usando el URL de la serie de cada nueva historia antes de descargar.\nOfrece la posibilidad de no descargar si se encuentra una antología de serie.\nNo funciona cuando la recopilación de metadatos en segundo plano está activada."
#: config.py:479 config.py:596
msgid "Reject Without Confirmation?"
@@ -250,7 +250,7 @@ msgid ""
"Automatically reject storys with existing Series Anthology books.\n"
"Only works if 'Check for existing Series Anthology books' is on.\n"
"Doesn't work when Collect Metadata in Background is selected."
msgstr "Automáticamente rechazar historias con libros de series de antología.\nSolo funciona si está activado 'Comprobar si existen libros de series de antología'.\nNo funciona cuando la recopilación de metadatos en segundo plano está activada."
msgstr "Rechazar automáticamente historias con libros de antología de serie existentes.\nSólo funciona si se activa «Comprobar si existen libros de antología de serie».\nNo funciona cuando la recopilación de metadatos en segundo plano está activada."
#: config.py:487
msgid "Check for changed Story URL?"
+15 -14
View File
@@ -3,14 +3,15 @@
#
# Translators:
# Çağla Turhan, 2016
# doruk tolga atasoy <doruk.tolga.a@gmail.com>, 2016
# Emre Deniz, 2014
# MUSTAFA ANAR <mstf.anar@gmail.com>, 2016
msgid ""
msgstr ""
"Project-Id-Version: calibre-plugins\n"
"POT-Creation-Date: 2016-09-18 17:33+Central Daylight Time\n"
"PO-Revision-Date: 2016-09-19 10:04+0000\n"
"Last-Translator: Kovid Goyal <kovid@kovidgoyal.net>\n"
"PO-Revision-Date: 2016-10-12 20:04+0000\n"
"Last-Translator: doruk tolga atasoy <doruk.tolga.a@gmail.com>\n"
"Language-Team: Turkish (http://www.transifex.com/calibre/calibre-plugins/language/tr/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -186,7 +187,7 @@ msgstr "Calibre Seçenekleri Güncelleniyor"
#: config.py:454
msgid "Delete other existing formats?"
msgstr "Diğer biçimler(formatlar) silinsin mi?"
msgstr "Varolan diğer formatlar silinsin mi?"
#: config.py:455
msgid ""
@@ -239,7 +240,7 @@ msgstr ""
#: config.py:479 config.py:596
msgid "Reject Without Confirmation?"
msgstr ""
msgstr "Doğrulama olmadan reddedilsin mi ?"
#: config.py:480
msgid ""
@@ -250,7 +251,7 @@ msgstr ""
#: config.py:487
msgid "Check for changed Story URL?"
msgstr "Değiştirilmiş Yazı URL'sini işaretlesin mi?"
msgstr "Değiştirilmiş Yazı URL'si işaretlensin mi?"
#: config.py:488
msgid ""
@@ -351,7 +352,7 @@ msgstr "Kitaplar seçilirse, üst HayranKurguTarife eklenti düğmesi Güncellem
#: config.py:553
msgid "Keep 'Add New from URL(s)' dialog on top?"
msgstr ""
msgstr "'URL(ler)'den yeni ekleme yap' iletisi en üstte tutulsun mu ?"
#: config.py:554
msgid ""
@@ -361,15 +362,15 @@ msgstr ""
#: config.py:558
msgid "Show estimated time left?"
msgstr ""
msgstr "Kalan tahmini süre gösterilsin mi ?"
#: config.py:559
msgid "When a Progress Bar is shown, show a rough estimate of the time left."
msgstr ""
msgstr "Bir ilerleme çubuğu gösterildiğinde, kalan süre için kaba bir tahmin değeri görüntüle."
#: config.py:563
msgid "Misc Options"
msgstr ""
msgstr "Çeşitli Seçenekler"
#: config.py:567
msgid "Inject calibre Series when none found?"
@@ -383,7 +384,7 @@ msgstr ""
#: config.py:572
msgid "Search by Title/Author(s) for If Story Already Exists?"
msgstr ""
msgstr "Eğer kayıt mevcutsa, Yazının Başlığı veya Yazar(Yazarlar) aracılığıyla arama gerçekleştirilsin mi? "
#: config.py:573
msgid ""
@@ -394,7 +395,7 @@ msgstr ""
#: config.py:577
msgid "Reject List"
msgstr ""
msgstr "Reddedilenler Listesi"
#: config.py:581
msgid "Edit Reject URL List"
@@ -430,15 +431,15 @@ msgstr ""
#: config.py:648
msgid "Reject Reasons"
msgstr ""
msgstr "Reddetme Sebepleri"
#: config.py:649
msgid "Customize Reject List Reasons"
msgstr ""
msgstr "Reddetme sebepleri listesini özelleştir"
#: config.py:658
msgid "Reason why I rejected it"
msgstr ""
msgstr "Neden reddettiğime dair sebep"
#: config.py:658
msgid "Title by Author"
+1
View File
@@ -144,6 +144,7 @@ import adapter_kiarepositorymujajinet
import adapter_fanfictionlucifaelcom
import adapter_adultfanfictionorg
import adapter_fictionhuntcom
import adapter_royalroadl
## This bit of complexity allows adapters to be added by just adding
## importing. It eliminates the long if/else clauses we used to need
+181
View File
@@ -0,0 +1,181 @@
# -*- coding: utf-8 -*-
# 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.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import time
import logging
logger = logging.getLogger(__name__)
import re
import urllib2
import cookielib as cl
from datetime import datetime
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, makeDate
def getClass():
return RoyalRoadAdapter
# Class name has to be unique. Our convention is camel case the
# sitename with Adapter at the end. www is skipped.
class RoyalRoadAdapter(BaseSiteAdapter):
def __init__(self, config, url):
BaseSiteAdapter.__init__(self, config, url)
self.decode = ["utf8",
"Windows-1252"
] # 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 fiction/1234
self.story.setMetadata('storyId',re.match('/fiction/(\d+)(:/.+)?$',self.parsedUrl.path).groups()[0])
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/fiction/'+self.story.getMetadata('storyId'))
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','rylrdl')
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = '%d/%m/%Y %H:%M:%S %p'
def make_date(self, parenttag):
# locale dates differ but the timestamp is easily converted
ts = parenttag.find('time')['unixtime']
return datetime.fromtimestamp(float(ts))
@staticmethod # must be @staticmethod, don't remove it.
def getSiteDomain():
# The site domain. Does have www here, if it uses it.
return 'royalroadl.com'
@classmethod
def getAcceptDomains(cls):
return ['royalroadl.com','www.royalroadl.com']
@classmethod
def getSiteExampleURLs(cls):
return "https://royalroadl.com/fiction/3056"
def getSiteURLPattern(self):
return "https?"+re.escape("://")+r"(www\.|)royalroadl\.com/fiction/\d+$"
def use_pagecache(self):
'''
adapters that will work with the page cache need to implement
this and change it to True.
'''
return True
## Getting the chapter list and the meta data, plus 'is adult' checking.
def extractChapterUrlsAndMetadata(self):
url = self.url
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
# use BeautifulSoup HTML parser to make everything easier to find.
soup = self.make_soup(data)
# print data
## Title
title=soup.h2.text
self.story.setMetadata('title',title)
# Find authorid and URL from... author url.
author = soup.find('',{'class':'mt-card-social'})
author_link = author.findAll('li')[-1]
if author_link:
authorId = author_link.a['href'].split('=')[-1]
self.story.setMetadata('authorId', authorId)
self.story.setMetadata('authorUrl','http://'+self.host+'/member.php?action=profile&uid='+authorId)
self.story.setMetadata('author',soup.find(attrs=dict(property="books:author"))['content'])
chapters = soup.find('table',{'id':'chapters'}).find('tbody')
tds = [tr.findAll('td')[0] for tr in chapters.findAll('tr')]
for td in tds:
chapterUrl = 'http://' + self.getSiteDomain() + td.a['href']
self.chapterUrls.append((stripHTML(td.text), chapterUrl))
self.story.setMetadata('numChapters',len(self.chapterUrls))
# this is forum based so it's a bit ugly
description = soup.find('div', {'property': 'description', 'class': 'hidden-content'})
self.setDescription(url,description)
dates = [tr.findAll('td')[1] for tr in chapters.findAll('tr')]
self.story.setMetadata('dateUpdated', self.make_date(dates[-1]))
self.story.setMetadata('datePublished', self.make_date(dates[0]))
genre=[tag.text for tag in soup.find('input',{'property':'genre'}).parent.findChildren('span')]
if not "Unspecified" in genre:
for tag in genre:
self.story.addToList('genre',tag)
# 'rating' in FFF speak means G, PG, Teen, Restricted, etc.
# 'stars' is used instead for RR's 1-5 stars rating.
stars=soup.find(attrs=dict(property="books:rating:value"))['content']
self.story.setMetadata('stars',stars)
logger.debug(self.story.getMetadata('stars'))
warning = soup.find('strong',text='Warning')
if warning != None:
warnings=[c.text for c in warning.parent.children if getattr(c,'text',None)][1:]
for warntag in warnings:
self.story.addToList('warnings',warntag)
# get cover
img = soup.find('',{'class':'row fic-header'}).find('img')
if img:
cover_url = img['src']
self.setCoverImage(url,cover_url)
# some content is show as tables, this will preserve them
# 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',{'class':"chapter-inner chapter-content"})
# TODO: these stories often have tables in, but these wont render correctly
if None == div:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
return self.utf8FromSoup(url,div)
@@ -171,7 +171,7 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
self.story.addToList('author',stripHTML(a).replace("'s Page",""))
# Find the chapters:
chapters = soup.findAll('a', href=re.compile(r'^/s/'+self.story.getMetadata('storyId')+":\d+$"))
chapters = soup.findAll('a', href=re.compile(r'^/s/'+self.story.getMetadata('storyId')+":\d+(/.*)?$"))
if len(chapters) != 0:
for chapter in chapters:
# just in case there's tags, like <i> in chapter titles.
+4 -3
View File
@@ -125,9 +125,10 @@ class WraithBaitComAdapter(BaseSiteAdapter):
rating=pt.text.split('[')[1].split(']')[0]
self.story.setMetadata('rating', rating)
st = soup.find('div', {'class' : 'storytitle'})
a = st.findAll('a', href=re.compile(r'reviews.php\?type=ST&item='+self.story.getMetadata('storyId')+"$"))[1] # second one.
self.story.setMetadata('reviews',stripHTML(a))
# site stopped showing reviews ~ Oct 2016
# st = soup.find('div', {'class' : 'storytitle'})
# a = st.findAll('a', href=re.compile(r'reviews.php\?type=ST&item='+self.story.getMetadata('storyId')+"$"))[1] # second one.
# self.story.setMetadata('reviews',stripHTML(a))
# Find the chapters:
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")):
+85 -60
View File
@@ -84,7 +84,7 @@ class BaseSiteAdapter(Configurable):
def __init__(self, configuration, url):
Configurable.__init__(self, configuration)
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
self.password = ""
self.is_adult=False
@@ -113,7 +113,7 @@ class BaseSiteAdapter(Configurable):
self.logfile = None
self.pagecache = self.get_empty_pagecache()
## order of preference for decoding.
self.decode = ["utf8",
"Windows-1252"] # 1252 is a superset of
@@ -135,17 +135,17 @@ class BaseSiteAdapter(Configurable):
saveheaders = self.opener.addheaders
self.opener = u2.build_opener(u2.HTTPCookieProcessor(self.cookiejar),GZipProcessor())
self.opener.addheaders = saveheaders
def load_cookiejar(self,filename):
'''
Needs to be called after adapter create, but before any fetchs
are done. Takes file *name*.
'''
self.get_cookiejar().load(filename, ignore_discard=True, ignore_expires=True)
def get_pagecache(self):
return self.pagecache
def set_pagecache(self,d):
self.pagecache=d
@@ -159,7 +159,7 @@ class BaseSiteAdapter(Configurable):
def _has_cachekey(self,cachekey):
return self.use_pagecache() and cachekey in self.get_pagecache()
def _get_from_pagecache(self,cachekey):
if self.use_pagecache():
return self.get_pagecache().get(cachekey)
@@ -176,18 +176,18 @@ class BaseSiteAdapter(Configurable):
this and change it to True.
'''
return False
# def story_load(self,filename):
# d = pickle.load(self.story.metadata,filename)
# self.story.metadata = d['metadata']
# self.chapterUrls = d['chapterlist']
# self.story.metadataDone = True
def _setURL(self,url):
self.url = url
self.parsedUrl = up.urlparse(url)
self.host = self.parsedUrl.netloc
self.path = self.parsedUrl.path
self.path = self.parsedUrl.path
self.story.setMetadata('storyUrl',self.url,condremoveentities=False)
## website encoding(s)--in theory, each website reports the character
@@ -201,7 +201,7 @@ class BaseSiteAdapter(Configurable):
decode = self.getConfigList('website_encodings')
else:
decode = self.decode
for code in decode:
try:
#print code
@@ -230,7 +230,7 @@ class BaseSiteAdapter(Configurable):
usecache=True):
'''
When should cache be cleared or not used? logins...
extrasleep is primarily for ffnet adapter which has extra
sleeps. Passed into fetchs so it can be bypassed when
cache hits.
@@ -240,7 +240,7 @@ class BaseSiteAdapter(Configurable):
logger.debug("#####################################\npagecache HIT: %s"%safe_url(cachekey))
data,redirecturl = self._get_from_pagecache(cachekey)
return data
logger.debug("#####################################\npagecache MISS: %s"%safe_url(cachekey))
self.do_sleep(extrasleep)
@@ -261,19 +261,19 @@ class BaseSiteAdapter(Configurable):
parameters=None,
extrasleep=None,
usecache=True):
return self._fetchUrlRawOpened(url,
parameters,
extrasleep,
usecache)[0]
def _fetchUrlRawOpened(self, url,
parameters=None,
extrasleep=None,
usecache=True):
'''
When should cache be cleared or not used? logins...
extrasleep is primarily for ffnet adapter which has extra
sleeps. Passed into fetchs so it can be bypassed when
cache hits.
@@ -289,7 +289,7 @@ class BaseSiteAdapter(Configurable):
def geturl(self): return self.url
def read(self): return self.data
return (data,FakeOpened(data,redirecturl))
logger.debug("#####################################\npagecache MISS: %s"%safe_url(cachekey))
self.do_sleep(extrasleep)
if parameters != None:
@@ -298,13 +298,13 @@ class BaseSiteAdapter(Configurable):
opened = self.opener.open(url.replace(' ','%20'),None,float(self.getConfig('connect_timeout',30.0)))
data = opened.read()
self._set_to_pagecache(cachekey,data,opened.url)
return (data,opened)
def set_sleep(self,val):
logger.debug("\n===========\n set sleep time %s\n==========="%val)
self.override_sleep = val
def do_sleep(self,extrasleep=None):
if extrasleep:
time.sleep(float(extrasleep))
@@ -312,7 +312,7 @@ class BaseSiteAdapter(Configurable):
time.sleep(float(self.override_sleep))
elif self.getConfig('slow_down_sleep_time'):
time.sleep(float(self.getConfig('slow_down_sleep_time')))
def _fetchUrl(self, url,
parameters=None,
usecache=True,
@@ -330,7 +330,7 @@ class BaseSiteAdapter(Configurable):
excpt=None
for sleeptime in [0, 0.5, 4, 9]:
time.sleep(sleeptime)
time.sleep(sleeptime)
try:
(data,opened)=self._fetchUrlRawOpened(url,
parameters=parameters,
@@ -345,7 +345,7 @@ class BaseSiteAdapter(Configurable):
except Exception, e:
excpt=e
logger.warn("Caught an exception reading URL: %s sleeptime(%s) Exception %s."%(unicode(safe_url(url)),sleeptime,unicode(e)))
logger.error("Giving up on %s" %safe_url(url))
logger.debug(excpt, exc_info=True)
raise(excpt)
@@ -357,12 +357,16 @@ class BaseSiteAdapter(Configurable):
if last:
self.chapterLast=int(last)-1
self.story.set_chapters_range(first,last)
# Does the download the first time it's called.
def getStory(self):
if not self.storyDone:
self.getStoryMetadataOnly(get_cover=True)
## one-off step to normalize old chapter URLs if present.
if self.oldchaptersmap:
self.oldchaptersmap = dict((self.normalize_chapterurl(key), value) for (key, value) in self.oldchaptersmap.items())
for index, (title,url) in enumerate(self.chapterUrls):
newchap = False
if (self.chapterFirst!=None and index < self.chapterFirst) or \
@@ -388,7 +392,7 @@ class BaseSiteAdapter(Configurable):
url in self.oldchaptersdata and (
self.oldchaptersdata[url]['chapterorigtitle'] !=
self.oldchaptersdata[url]['chaptertitle']) )
if not data:
data = self.getChapterText(url)
# if had to fetch and has existing chapters
@@ -400,13 +404,13 @@ class BaseSiteAdapter(Configurable):
# anyway--only if it's replaced during an
# update.
newchap = False
self.story.addChapter(url,
removeEntities(title),
removeEntities(data),
newchap)
self.storyDone = True
# include image, but no cover from story, add default_cover_image cover.
if self.getConfig('include_images') and \
not self.story.cover and \
@@ -423,26 +427,30 @@ class BaseSiteAdapter(Configurable):
if not self.story.cover and self.oldcover:
self.story.oldcover = self.oldcover
self.story.setMetadata('cover_image','old')
# cheesy way to carry calibre bookmark file forward across update.
if self.calibrebookmark:
self.story.calibrebookmark = self.calibrebookmark
if self.logfile:
self.story.logfile = self.logfile
return self.story
def getStoryMetadataOnly(self,get_cover=True):
if not self.metadataDone:
self.doExtractChapterUrlsAndMetadata(get_cover=get_cover)
if not self.story.getMetadataRaw('dateUpdated'):
if self.story.getMetadataRaw('datePublished'):
self.story.setMetadata('dateUpdated',self.story.getMetadataRaw('datePublished'))
else:
self.story.setMetadata('dateUpdated',self.story.getMetadataRaw('dateCreated'))
self.story.setMetadata('dateUpdated',self.story.getMetadataRaw('dateCreated'))
self.metadataDone = True
# normalize chapter urls.
for index, (title,url) in enumerate(self.chapterUrls):
self.chapterUrls[index] = (title,self.normalize_chapterurl(url))
return self.story
def setStoryMetadata(self,metahtml):
@@ -453,36 +461,36 @@ class BaseSiteAdapter(Configurable):
if self.story.getMetadataRaw('datePublished'):
self.story.setMetadata('dateUpdated',self.story.getMetadataRaw('datePublished'))
else:
self.story.setMetadata('dateUpdated',self.story.getMetadataRaw('dateCreated'))
self.story.setMetadata('dateUpdated',self.story.getMetadataRaw('dateCreated'))
def hookForUpdates(self,chaptercount):
"Usually not needed."
return chaptercount
###############################
@staticmethod
def getSiteDomain():
"Needs to be overriden in each adapter class."
return 'no such domain'
@classmethod
def getConfigSection(cls):
"Only needs to be overriden if != site domain."
return cls.getSiteDomain()
@classmethod
def getConfigSections(cls):
"Only needs to be overriden if has additional ini sections."
return [cls.getConfigSection()]
@classmethod
def stripURLParameters(cls,url):
"Only needs to be overriden if URL contains more than one parameter"
## remove any trailing '&' parameters--?sid=999 will be left.
## that's all that any of the current adapters need or want.
return re.sub(r"&.*$","",url)
## URL pattern validation is done *after* picking an adaptor based
## on domain instead of *as* the adaptor selector so we can offer
## the user example(s) for that particular site.
@@ -490,7 +498,7 @@ class BaseSiteAdapter(Configurable):
def getSiteURLPattern(self):
"Used to validate URL. Should be override in each adapter class."
return '^http://'+re.escape(self.getSiteDomain())
@classmethod
def getSiteExampleURLs(cls):
"""
@@ -500,7 +508,7 @@ class BaseSiteAdapter(Configurable):
validateURL method.
"""
return 'no such example'
def doExtractChapterUrlsAndMetadata(self,get_cover=True):
'''
There are a handful of adapters that fetch a cover image while
@@ -509,7 +517,7 @@ class BaseSiteAdapter(Configurable):
this instead of extractChapterUrlsAndMetadata()
'''
return self.extractChapterUrlsAndMetadata()
def extractChapterUrlsAndMetadata(self):
"Needs to be overriden in each adapter class. Populates self.story metadata and self.chapterUrls"
pass
@@ -561,7 +569,7 @@ class BaseSiteAdapter(Configurable):
# bs4
return soup.attrs.keys()
return []
# This gives us a unicode object, not just a string containing bytes.
# (I gave soup a unicode string, you'd think it could give it back...)
# Now also does a bunch of other common processing for us.
@@ -570,12 +578,12 @@ class BaseSiteAdapter(Configurable):
fetch=self._fetchUrlRaw
acceptable_attributes = self.getConfigList('keep_html_attrs',['href','name','class','id'])
if self.getConfig("keep_style_attr"):
acceptable_attributes.append('style')
if self.getConfig("keep_title_attr"):
acceptable_attributes.append('title')
#print("include_images:"+self.getConfig('include_images'))
if self.getConfig('include_images'):
acceptable_attributes.extend(('src','alt','longdesc'))
@@ -592,6 +600,19 @@ class BaseSiteAdapter(Configurable):
if attr not in acceptable_attributes:
del soup[attr] ## strip all tag attributes except href and name
## apply adapter's normalize_chapterurls to all links in
## chapter texts, if they match chapter URLs. While this will
## be occasionally helpful by itself, it's really for the next
## feature: internal text links.
if self.getConfig('normalize_text_links'):
for alink in soup.find_all('a'):
# try:
if alink.has_attr('href'):
# logger.debug("normalize_text_links %s -> %s"%(alink['href'],self.normalize_chapterurl(alink['href'])))
alink['href'] = self.normalize_chapterurl(alink['href'])
# except AttributeError as ae:
# logger.info("Parsing for normalize_text_links failed...")
try:
# as a generator, each tag will be returned even if there's a
# mismatch at the end.
@@ -599,8 +620,8 @@ class BaseSiteAdapter(Configurable):
for attr in self.get_attr_keys(t):
if attr not in acceptable_attributes:
del t[attr] ## strip all tag attributes except acceptable_attributes
# these are not acceptable strict XHTML. But we do already have
# these are not acceptable strict XHTML. But we do already have
# CSS classes of the same names defined
if t and hasattr(t,'name') and t.name is not None:
if t.name in self.getConfigList('replace_tags_with_spans',['u']):
@@ -616,11 +637,11 @@ class BaseSiteAdapter(Configurable):
# remove script tags cross the board.
if t.name=='script':
t.extract()
except AttributeError, ae:
if "%s"%ae != "'NoneType' object has no attribute 'next_element'":
logger.error("Error parsing HTML, probably poor input HTML. %s"%ae)
retval = unicode(soup)
if self.getConfig('nook_img_fix') and not self.getConfig('replace_br_with_p'):
@@ -629,16 +650,16 @@ class BaseSiteAdapter(Configurable):
# that under the text for the rest of the chapter.
retval = re.sub(r"(?!<(div|p)>)\s*(?P<imgtag><img[^>]+>)\s*(?!</(div|p)>)",
"<div>\g<imgtag></div>",retval)
# Don't want html, head or body tags in chapter html--writers add them.
# This is primarily for epub updates.
retval = re.sub(r"</?(html|head|body)[^>]*>\r?\n?","",retval)
if self.getConfig("replace_br_with_p") and allow_replace_br_with_p:
# Apply heuristic processing to replace <br> paragraph
# breaks with <p> tags.
retval = replace_br_with_p(retval)
if self.getConfig('replace_hr'):
# replacing a self-closing tag with a container tag in the
# soup is more difficult than it first appears. So cheat.
@@ -648,31 +669,35 @@ class BaseSiteAdapter(Configurable):
def make_soup(self,data):
'''
Convenience method for getting a bs4 soup. Older and
non-updated adapters call the included bs3 library themselves.
Convenience method for getting a bs4 soup. bs3 has been removed.
'''
## html5lib handles <noscript> oddly. See:
## https://bugs.launchpad.net/beautifulsoup/+bug/1277464
## This should 'hide' and restore <noscript> tags.
data = data.replace("noscript>","fff_hide_noscript>")
## soup and re-soup because BS4/html5lib is more forgiving of
## incorrectly nested tags that way.
soup = bs4.BeautifulSoup(data,'html5lib')
soup = bs4.BeautifulSoup(unicode(soup),'html5lib')
for ns in soup.find_all('fff_hide_noscript'):
ns.name = 'noscript'
return soup
## For adapters, especially base_xenforoforum to override. Make
## sure to return unchanged URL if it's NOT a chapter URL...
def normalize_chapterurl(self,url):
return url
def cachedfetch(realfetch,cache,url):
if url in cache:
return cache[url]
else:
return realfetch(url)
fullmon = {u"January":u"01", u"February":u"02", u"March":u"03", u"April":u"04", u"May":u"05",
u"June":u"06","July":u"07", u"August":u"08", u"September":u"09", u"October":u"10",
u"November":u"11", u"December":u"12" }
@@ -687,7 +712,7 @@ def makeDate(string,dateform):
# lie. It has to do something even more complicated to get
# Russian month names correct everywhere.
do_abbrev = "%b" in dateform
if u"%B" in dateform or do_abbrev:
dateform = dateform.replace(u"%B",u"%m").replace(u"%b",u"%m")
for (name,num) in fullmon.items():
@@ -708,10 +733,10 @@ def makeDate(string,dateform):
string = string.replace(u"AM",u"").replace(u"PM",u"").replace(u"am",u"").replace(u"pm",u"")
date = datetime.strptime(string.encode('utf-8'),dateform.encode('utf-8'))
if add_hours:
date += timedelta(hours=12)
return date
# .? for AO3's ']' in param names.
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2015 FanFicFare team
# Copyright 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.
@@ -85,6 +85,62 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
def getSiteURLPattern(self):
return r"https?://"+re.escape(self.getSiteDomain())+r"/(?P<tp>threads|posts)/(.+\.)?(?P<id>\d+)/?[^#]*?(#post-(?P<anchorpost>\d+))?$"
## For adapters, especially base_xenforoforum to override. Make
## sure to return unchanged URL if it's NOT a chapter URL. This
## is most helpful for xenforoforum because threadmarks use
## thread-name URLs--which can change if the thread name changes.
def normalize_chapterurl(self,url):
(is_chapter_url,normalized_url) = self._is_normalize_chapterurl(url)
if is_chapter_url:
return normalized_url
else:
return url
## returns (is_chapter_url,normalized_url)
def _is_normalize_chapterurl(self,url):
is_chapter_url = False
## moved from extract metadata to share with normalize_chapterurl.
if not url.startswith('http'):
url = self.getURLPrefix()+'/'+url
if ( url.startswith(self.getURLPrefix()) or
url.startswith('http://'+self.getSiteDomain()) or
url.startswith('https://'+self.getSiteDomain()) ) and \
( '/posts/' in url or '/threads/' in url or 'showpost.php' in url or 'goto/post' in url):
# brute force way to deal with SB's http->https change when hardcoded http urls.
url = url.replace('http://'+self.getSiteDomain(),self.getURLPrefix())
# http://forums.spacebattles.com/showpost.php?p=4755532&postcount=9
url = re.sub(r'showpost\.php\?p=([0-9]+)(&postcount=[0-9]+)?',r'/posts/\1/',url)
# http://forums.spacebattles.com/goto/post?id=15222406#post-15222406
url = re.sub(r'/goto/post\?id=([0-9]+)(#post-[0-9]+)?',r'/posts/\1/',url)
url = re.sub(r'(^[\'"]+|[\'"]+$)','',url) # strip leading or trailing '" from incorrect quoting.
url = re.sub(r'like$','',url) # strip 'like' if incorrect 'like' link instead of proper post URL.
#### moved from getChapterText()
## there's some history of stories with links to the wrong
## page. This changes page#post URLs to perma-link URLs.
## Which will be redirected back to page#posts, but the
## *correct* ones.
# http://forums.sufficientvelocity.com/threads/harry-potter-and-the-not-fatal-at-all-cultural-exchange-program.330/page-4#post-39915
# https://forums.sufficientvelocity.com/posts/39915/
if '#post-' in url:
url = self.getURLPrefix()+'/posts/'+url.split('#post-')[1]+'/'
## Same as above except for for case where author mistakenly
## used the reply link instead of normal link to post.
# "http://forums.spacebattles.com/threads/manager-worm-story-thread-iv.301602/reply?quote=15962513"
# https://forums.spacebattles.com/posts/
if 'reply?quote=' in url:
url = self.getURLPrefix()+'/posts/'+url.split('reply?quote=')[1]+'/'
is_chapter_url = True
return (is_chapter_url,url)
def use_pagecache(self):
'''
adapters that will work with the page cache need to implement
@@ -119,7 +175,7 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
# params[soup.find('input', {'id':'password'})['name']] = params['password']
d = self._fetchUrl(loginUrl, params)
if "Log Out" not in d :
logger.info("Failed to login to URL %s as %s" % (loginUrl,
params['login']))
@@ -183,7 +239,7 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
threadmark_chaps = True
if self.getConfig('always_include_first_post'):
self.chapterUrls.append((first_post_title,useurl))
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'}))
if not self.story.getMetadataRaw('datePublished') or date < self.story.getMetadataRaw('datePublished'):
@@ -202,7 +258,7 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
if self.getConfig('capitalize_forumtags'):
tstr = tstr.title()
self.story.addToList('forumtags',tstr)
# Now go hunting for the 'chapter list'.
bq = soup.find('blockquote') # assume first posting contains TOC urls.
@@ -222,28 +278,9 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
if not self.chapterUrls:
self.chapterUrls.append((first_post_title,useurl))
for (url,name) in [ (x['href'],stripHTML(x)) for x in bq.find_all('a') ]:
#logger.debug("found chapurl:%s"%url)
if not url.startswith('http'):
url = self.getURLPrefix()+'/'+url
if ( url.startswith(self.getURLPrefix()) or
url.startswith('http://'+self.getSiteDomain()) or
url.startswith('https://'+self.getSiteDomain()) ) and \
( '/posts/' in url or '/threads/' in url or 'showpost.php' in url or 'goto/post' in url):
# brute force way to deal with SB's http->https change when hardcoded http urls.
url = url.replace('http://'+self.getSiteDomain(),self.getURLPrefix())
# http://forums.spacebattles.com/showpost.php?p=4755532&postcount=9
url = re.sub(r'showpost\.php\?p=([0-9]+)(&postcount=[0-9]+)?',r'/posts/\1/',url)
# http://forums.spacebattles.com/goto/post?id=15222406#post-15222406
url = re.sub(r'/goto/post\?id=([0-9]+)(#post-[0-9]+)?',r'/posts/\1/',url)
url = re.sub(r'(^[\'"]+|[\'"]+$)','',url) # strip leading or trailing '" from incorrect quoting.
url = re.sub(r'like$','',url) # strip 'like' if incorrect 'like' link instead of proper post URL.
logger.debug("(ch:%s)used chapurl:%s"%(len(self.chapterUrls)+1,url))
(is_chapter_url,url) = self._is_normalize_chapterurl(url)
if is_chapter_url:
self.chapterUrls.append((name,url))
if url == useurl and first_post_title == self.chapterUrls[0][0] \
and not self.getConfig('always_include_first_post',False):
@@ -286,22 +323,6 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
def getChapterText(self, url):
logger.debug('Getting chapter text from: %s' % url)
## there's some history of stories with links to the wrong
## page. This changes page#post URLs to perma-link URLs.
## Which will be redirected back to page#posts, but the
## *correct* ones.
# http://forums.sufficientvelocity.com/threads/harry-potter-and-the-not-fatal-at-all-cultural-exchange-program.330/page-4#post-39915
# https://forums.sufficientvelocity.com/posts/39915/
if '#post-' in url:
url = self.getURLPrefix()+'/posts/'+url.split('#post-')[1]+'/'
## Same as above except for for case where author mistakenly
## used the reply link instead of normal link to post.
# "http://forums.spacebattles.com/threads/manager-worm-story-thread-iv.301602/reply?quote=15962513"
# https://forums.spacebattles.com/posts/
if 'reply?quote=' in url:
url = self.getURLPrefix()+'/posts/'+url.split('reply?quote=')[1]+'/'
try:
origurl = url
(data,opened) = self._fetchUrlOpened(url)
@@ -309,20 +330,20 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
if '#' in origurl and '#' not in url:
url = url + origurl[origurl.index('#'):]
logger.debug("chapter URL redirected to: %s"%url)
soup = self.make_soup(data)
if '#' in url:
anchorid = url.split('#')[1]
soup = soup.find('li',id=anchorid)
bq = soup.find('blockquote')
bq.name='div'
for iframe in bq.find_all('iframe'):
iframe.extract() # calibre book reader & editor don't like iframes to youtube.
for qdiv in bq.find_all('div',{'class':'quoteExpand'}):
qdiv.extract() # Remove <div class="quoteExpand">click to expand</div>
@@ -330,7 +351,7 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
## include lazy load images.
for img in bq.find_all('img',{'class':'lazyload'}):
img['src'] = img['data-src']
except Exception as e:
if self.getConfig('continue_on_chapter_error'):
bq = self.make_soup("""<div>
+13 -8
View File
@@ -26,7 +26,7 @@ import pprint
import string
import sys
version="2.4.3"
version="2.5.1"
if sys.version_info < (2, 5):
print 'This program requires Python 2.5 or newer.'
@@ -217,10 +217,10 @@ def main(argv=None,
if options.infile:
with open(options.infile,"r") as infile:
#print "File exists and is readable"
#fileurls = [line.strip() for line in infile]
for url in infile:
url = url[:url.find('#')].strip()
if '#' in url:
url = url[:url.find('#')].strip()
url = url.strip()
if len(url) > 0:
#print "URL: (%s)"%url
urls.append(url)
@@ -275,7 +275,8 @@ def do_download(arg,
passed_defaultsini,
passed_personalini,
options,
chaptercount)
chaptercount,
output_filename)
try:
adapter = adapters.getAdapter(configuration, url)
@@ -377,7 +378,10 @@ def do_download(arg,
output_filename = write_story(configuration, adapter, options.format, options.metaonly)
if not options.metaonly and adapter.getConfig('post_process_cmd'):
metadata = adapter.story.metadata
if adapter.getConfig('post_process_apply_filename_safepattern'):
metadata = adapter.story.get_filename_safe_metadata()
else:
metadata = adapter.story.getAllMetadata()
metadata['output_filename'] = output_filename
call(string.Template(adapter.getConfig('post_process_cmd')).substitute(metadata), shell=True)
@@ -396,7 +400,8 @@ def get_configuration(url,
passed_defaultsini,
passed_personalini,
options,
chaptercount=None):
chaptercount=None,
output_filename=None):
try:
configuration = Configuration(adapters.getConfigSectionsFor(url), options.format)
except exceptions.UnknownSite, e:
@@ -441,7 +446,7 @@ def get_configuration(url,
if options.force:
configuration.set('overrides', 'always_overwrite', 'true')
if options.update and chaptercount:
if options.update and chaptercount and output_filename:
configuration.set('overrides', 'output_filename', output_filename)
if options.update and not options.updatecover:
+56 -39
View File
@@ -40,7 +40,7 @@ import adapters
def re_compile(regex,line):
try:
return re.compile(regex)
except Exception, e:
except Exception, e:
raise exceptions.RegularExpresssionFailed(e,regex,line)
# fall back labels.
@@ -59,6 +59,7 @@ titleLabels = {
'warnings':'Warnings',
'numChapters':'Chapters',
'numWords':'Words',
'words_added':'Words Added', # logpage only
'site':'Site',
'storyId':'Story ID',
'authorId':'Author ID',
@@ -78,7 +79,7 @@ formatsections = ['html','txt','epub','mobi']
othersections = ['defaults','overrides']
def get_valid_sections():
sites = adapters.getConfigSections()
sites = adapters.getConfigSections()
sitesections = list(othersections)
for section in sites:
sitesections.append(section)
@@ -90,7 +91,7 @@ def get_valid_sections():
else:
# add w/ www if doesn't www
sitesections.append('www.%s'%section)
allowedsections = []
allowedsections.extend(formatsections)
@@ -99,7 +100,7 @@ def get_valid_sections():
for f in formatsections:
allowedsections.append('%s:%s'%(section,f))
return allowedsections
def get_valid_list_entries():
return list(['category',
'genre',
@@ -127,6 +128,12 @@ def get_valid_set_options():
This is to further restrict keywords to certain sections and/or
values. get_valid_keywords() below is the list of allowed
keywords. Any keyword listed here must also be listed there.
This is what's used by the code when you save personal.ini in
plugin that stops and points out possible errors in keyword
*values*. It doesn't flag 'bad' keywords. Note that it's
separate from color highlighting and most keywords need to be
added to both.
'''
valdict = {'collect_series':(None,None,boollist),
@@ -144,15 +151,15 @@ def get_valid_set_options():
'strip_chapter_numbers':(None,None,boollist),
'mark_new_chapters':(None,None,boollist),
'titlepage_use_table':(None,None,boollist),
'use_ssl_unverified_context':(None,None,boollist),
'add_chapter_numbers':(None,None,boollist+['toconly']),
'check_next_chapter':(['fanfiction.net'],None,boollist),
'tweak_fg_sleep':(['fanfiction.net'],None,boollist),
'skip_author_cover':(['fanfiction.net'],None,boollist),
'fix_fimf_blockquotes':(['fimfiction.net'],None,boollist),
'fail_on_password':(['fimfiction.net'],None,boollist),
'do_update_hook':(['fimfiction.net',
@@ -174,15 +181,17 @@ def get_valid_set_options():
# kept forgetting to add them, so now it's automatic.
'bulk_load':(adapters.get_bulk_load_sites(),
None,boollist),
'include_logpage':(None,['epub'],boollist+['smart']),
'logpage_at_end':(None,['epub'],boollist),
'windows_eol':(None,['txt'],boollist),
'include_images':(None,['epub','html'],boollist),
'grayscale_images':(None,['epub','html'],boollist),
'no_image_processing':(None,['epub','html'],boollist),
'normalize_text_links':(None,['epub','html'],boollist),
'internalize_text_links':(None,['epub','html'],boollist),
'capitalize_forumtags':(base_xenforo_list,None,boollist),
'continue_on_chapter_error':(base_xenforo_list,None,boollist),
@@ -205,6 +214,7 @@ def get_valid_scalar_entries():
'rating',
'numChapters',
'numWords',
'words_added', # logpage only.
'site',
'storyId',
'title',
@@ -227,6 +237,11 @@ def get_valid_entries():
# *known* keywords -- or rather regexps for them.
def get_valid_keywords():
'''
Among other things, this list is used by the color highlighting in
personal.ini editing in plugin. Note that it's separate from
value checking and most keywords need to be added to both.
'''
return list(['(in|ex)clude_metadata_(pre|post)',
'add_chapter_numbers',
'add_genre_when_multi_category',
@@ -359,7 +374,9 @@ def get_valid_keywords():
'minimum_threadmarks',
'first_post_title',
'always_include_first_post',
'',
'always_reload_first_chapter',
'normalize_text_links',
'internalize_text_links',
])
# *known* entry keywords -- or rather regexps for them.
@@ -376,9 +393,9 @@ def make_generate_cover_settings(param):
(template,regexp,setting) = map( lambda x: x.strip(), line.split("=>") )
re_compile(regexp,line)
vlist.append((template,regexp,setting))
except Exception, e:
except Exception, e:
raise exceptions.PersonalIniFailed(e,line,param)
return vlist
@@ -389,9 +406,9 @@ class Configuration(ConfigParser.SafeConfigParser):
ConfigParser.SafeConfigParser.__init__(self)
self.lightweight = lightweight
self.linenos=dict() # key by section or section,key -> lineno
## [injected] section has even less priority than [defaults]
self.sectionslist = ['defaults','injected']
@@ -399,17 +416,17 @@ class Configuration(ConfigParser.SafeConfigParser):
## but before site-specific.
for section in sections[:-1]:
self.addConfigSection(section)
if site.startswith("www."):
sitewith = site
sitewithout = site.replace("www.","")
else:
sitewith = "www."+site
sitewithout = site
self.addConfigSection(sitewith)
self.addConfigSection(sitewithout)
if fileform:
self.addConfigSection(fileform)
## add other sections:fileform (not including site DN)
@@ -419,9 +436,9 @@ class Configuration(ConfigParser.SafeConfigParser):
self.addConfigSection(sitewith+":"+fileform)
self.addConfigSection(sitewithout+":"+fileform)
self.addConfigSection("overrides")
self.listTypeEntries = get_valid_list_entries()
self.validEntries = get_valid_entries()
self.url_config_set = False
@@ -446,7 +463,7 @@ class Configuration(ConfigParser.SafeConfigParser):
def isListType(self,key):
return key in self.listTypeEntries or self.hasConfig("include_in_"+key)
def isValidMetaEntry(self, key):
return key in self.getValidMetaList()
@@ -476,7 +493,7 @@ class Configuration(ConfigParser.SafeConfigParser):
# used by adapters & writers, non-convention naming style
def getConfig(self, key, default=""):
return self.get_config(self.sectionslist,key,default)
def get_config(self, sections, key, default=""):
val = default
for section in sections:
@@ -496,7 +513,7 @@ class Configuration(ConfigParser.SafeConfigParser):
#print "getConfig(add_to_%s)=[%s]%s" % (key,section,val)
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError), e:
pass
return val
# split and strip each.
@@ -508,7 +525,7 @@ class Configuration(ConfigParser.SafeConfigParser):
return default
else:
return vlist
# used by adapters & writers, non-convention naming style
def getConfigList(self, key, default=[]):
return self.get_config_list(self.sectionslist, key, default)
@@ -522,7 +539,7 @@ class Configuration(ConfigParser.SafeConfigParser):
return self.linenos.get(section+','+key,None)
else:
return self.linenos.get(section,None)
## Copied from Python 2.7 library so as to make read utf8.
def read(self, filenames):
"""Read and parse a filename or a list of filenames.
@@ -546,7 +563,7 @@ class Configuration(ConfigParser.SafeConfigParser):
fp.close()
read_ok.append(filename)
return read_ok
## Copied from Python 2.7 library so as to make it save linenos too.
#
# Regular expressions for parsing section headers and options.
@@ -626,7 +643,7 @@ class Configuration(ConfigParser.SafeConfigParser):
optval = ''
optname = self.optionxform(optname.rstrip())
cursect[optname] = optval
self.linenos[cursect['__name__']+','+optname]=lineno
self.linenos[cursect['__name__']+','+optname]=lineno
else:
# a non-fatal parsing error occurred. set up the
# exception but keep going. the exception will be
@@ -654,11 +671,11 @@ class Configuration(ConfigParser.SafeConfigParser):
from story import set_in_ex_clude, make_replacements
custom_columns_settings_re = re.compile(r'(add_to_)?custom_columns_settings')
generate_cover_settings_re = re.compile(r'(add_to_)?generate_cover_settings')
valdict = get_valid_set_options()
for section in self.sections():
allow_all_section = allow_all_sections_re.match(section)
if section not in allowedsections and not allow_all_section:
@@ -674,17 +691,17 @@ class Configuration(ConfigParser.SafeConfigParser):
elif sitename in othersections:
formatname = None
sitename = None
## check each keyword in section. Due to precedence
## order of sections, it's possible for bad lines to
## never be used.
for keyword,value in self.items(section):
try:
## check regex bearing keywords first. Each
## will raise exceptions if flawed.
if clude_metadata_re.match(keyword):
set_in_ex_clude(value)
set_in_ex_clude(value)
if replace_metadata_re.match(keyword):
make_replacements(value)
@@ -716,7 +733,7 @@ class Configuration(ConfigParser.SafeConfigParser):
## used with CLI/web yet.
except Exception as e:
errors.append((self.get_lineno(section,keyword),"Error:%s in (%s:%s)"%(e,keyword,value)))
errors.append((self.get_lineno(section,keyword),"Error:%s in (%s:%s)"%(e,keyword,value)))
return errors
@@ -731,7 +748,7 @@ class Configurable(object):
def addUrlConfigSection(self,url):
self.configuration.addUrlConfigSection(url)
def isListType(self,key):
return self.configuration.isListType(key)
@@ -740,10 +757,10 @@ class Configurable(object):
def getValidMetaList(self):
return self.configuration.getValidMetaList()
def hasConfig(self, key):
return self.configuration.hasConfig(key)
return self.configuration.hasConfig(key)
def has_config(self, sections, key):
return self.configuration.has_config(sections, key)
+30 -6
View File
@@ -180,7 +180,7 @@ extratags: FanFiction
## Can also be used for other metadata values
#default_value_category:FanFiction
## number of seconds to sleep between calls to the story site. May by
## number of seconds to sleep between calls to the story site. May be
## useful if pulling large numbers of stories or if the site is slow.
#slow_down_sleep_time:0.5
@@ -189,11 +189,17 @@ extratags: FanFiction
## prevent excessive wait when your network or the site is down.
connect_timeout:60.0
## For use only with stand-alone CLI version--run a command on the
## generated file after it's produced. All of the titlepage_entries
## values are available, plus output_filename.
## For use only with CLI version--run a command on the generated file
## after it's produced. All of the titlepage_entries values are
## available, plus output_filename.
#post_process_cmd: addbook -f "${output_filename}" -t "${title}"
## Some operating systems and command shells have problems with some
## characters. When true, the output_filename_safepattern will be
## applied to each metadata item passed to post_process_cmd before
## it's called.
#post_process_apply_filename_safepattern:false
## Use regular expressions to find and replace (or remove) metadata.
## For example, you could change Sci-Fi=>SF, remove *-Centered tags,
## etc. See http://docs.python.org/library/re.html (look for re.sub)
@@ -760,6 +766,18 @@ remove_transparency: true
## true--replace_br_with_p also fixes the problem.
nook_img_fix:true
## Apply adapter's normalize_chapterurl() to all links in chapter
## texts, if they match chapter URLs. Currently only implemented by
## base_xenforoforum adapters.
#normalize_text_links:false
## Search all links in chapter texts and, if they match any included
## chapter URLs, replace them with links to the chapter in the
## download. Only works with epub and html output formats.
## base_xenforoforum adapters should also use normalize_text_links
## with this.
#internalize_text_links:false
[mobi]
## mobi TOC cannot be turned off right now.
#include_tocpage: true
@@ -1576,6 +1594,11 @@ comments_label:Comments
include_in_category:category,searchtags
[royalroadl.com]
extra_valid_entries:stars
#add_to_extra_titlepage_entries:,stars
[samandjack.net]
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
@@ -2341,8 +2364,9 @@ extracharacters:Wolverine,Rogue
## Site dedicated to these categories/characters/ships
extracategories:Stargate: Atlantis
extra_valid_entries:reviews
reviews_label:Reviews
##site stopped showing reviews ~ Oct 2016
#extra_valid_entries:reviews
#reviews_label:Reviews
[buffygiles.velocitygrass.com]
## Some sites do not require a login, but do require the user to
+4 -2
View File
@@ -110,12 +110,14 @@ def get_update_data(inputio,
if( item.getAttribute("media-type") == "application/xhtml+xml" ):
href=relpath+item.getAttribute("href")
#print("---- item href:%s path part: %s"%(href,get_path_part(href)))
if re.match(r'.*/log_page\.x?html',href):
if re.match(r'.*/log_page(_u\d+)?\.x?html',href):
try:
logfile = epub.read(href).decode("utf-8")
except:
pass # corner case I bumped into while testing.
if re.match(r'.*/(file|chapter)\d+\.x?html',href):
if re.match(r'.*/(file|chapter)\d+(_u\d+)?\.x?html',href):
# (_u\d+)? is from calibre convert naming files
# 3/OEBPS/file0005_u3.xhtml etc.
if getsoups:
soup = bs.BeautifulSoup(epub.read(href).decode("utf-8"),"html5lib")
for img in soup.findAll('img'):
+17 -9
View File
@@ -997,20 +997,28 @@ class Story(Configurable):
return retval
def get_filename_safe_metadata(self):
origvalues = self.getAllMetadata()
values={}
pattern = re_compile(self.getConfig("output_filename_safepattern",
r"(^\.|/\.|[^a-zA-Z0-9_\. \[\]\(\)&'-]+)"),
"output_filename_safepattern")
for k in origvalues.keys():
if k == 'formatext': # don't do file extension--we set it anyway.
values[k]=self.getMetadata(k)
else:
values[k]=re.sub(pattern,'_', removeAllEntities(self.getMetadata(k)))
return values
def formatFileName(self,template,allowunsafefilename=True):
values = origvalues = self.getAllMetadata()
# fall back default:
if not template:
template="${title}-${siteabbrev}_${storyId}${formatext}"
if not allowunsafefilename:
values={}
pattern = re_compile(self.getConfig("output_filename_safepattern",r"(^\.|/\.|[^a-zA-Z0-9_\. \[\]\(\)&'-]+)"),"output_filename_safepattern")
for k in origvalues.keys():
if k == 'formatext': # don't do file extension--we set it anyway.
values[k]=self.getMetadata(k)
else:
values[k]=re.sub(pattern,'_', removeAllEntities(self.getMetadata(k)))
if allowunsafefilename:
values = self.getAllMetadata()
else:
values = self.get_filename_safe_metadata()
return string.Template(template).substitute(values).encode('utf8')
+33 -3
View File
@@ -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.
@@ -27,8 +27,11 @@ import re
## use DOM to generate the XML files.
from xml.dom.minidom import parse, parseString, getDOMImplementation
import bs4
from base_writer import *
from ..htmlcleanup import stripHTML,removeEntities
from ..story import commaGroups
logger = logging.getLogger(__name__)
@@ -206,7 +209,7 @@ div { margin: 0pt; padding: 0pt; }
# to add to.
if self.story.logfile:
logger.debug("existing logfile found, appending")
logger.debug("existing data:%s"%self._getLastLogData(self.story.logfile))
# logger.debug("existing data:%s"%self._getLastLogData(self.story.logfile))
replace_string = "</body>" # "</h3>"
self._write(out,self.story.logfile.replace(replace_string,self._makeLogEntry(self._getLastLogData(self.story.logfile))+replace_string))
else:
@@ -253,6 +256,14 @@ div { margin: 0pt; padding: 0pt; }
retval = START.substitute(self.story.getAllMetadata())
## words_added is only used in logpage because it's the only
## place we know the previous version's word count.
if 'words_added' in (self.getConfigList("logpage_entries") + self.getConfigList("extra_logpage_entries")):
new_words = self.story.getMetadata('numWords')
old_words = oldvalues.get('numWords',None)
if new_words and old_words:
self.story.setMetadata('words_added',commaGroups(unicode(int(new_words.replace(',',''))-int(old_words.replace(',','')))))
for entry in self.getConfigList("logpage_entries") + self.getConfigList("extra_logpage_entries"):
if self.isValidMetaEntry(entry):
val = self.story.getMetadata(entry)
@@ -502,6 +513,8 @@ div { margin: 0pt; padding: 0pt; }
(self.story.logfile or self.story.getMetadataRaw("status") == "In-Progress") ) \
or self.getConfig("include_logpage") == "true"
## collect chapter urls and file names for internalize_text_links option.
chapurlmap = {}
for index, chap in enumerate(self.story.getChapters(fortoc=True)):
if chap.html:
i=index+1
@@ -510,6 +523,7 @@ div { margin: 0pt; padding: 0pt; }
"application/xhtml+xml",
chap.title))
itemrefs.append("file%04d"%i)
chapurlmap[chap.url]="file%04d.xhtml"%i # url -> relative epub file name.
if dologpage:
if self.getConfig("logpage_at_end") == "true":
@@ -659,6 +673,20 @@ div { margin: 0pt; padding: 0pt; }
for index, chap in enumerate(self.story.getChapters()): # (url,title,html)
if chap.html:
chap_data = chap.html
if self.getConfig('internalize_text_links'):
soup = bs4.BeautifulSoup(chap.html,'html5lib')
changed=False
for alink in soup.find_all('a'):
if alink.has_attr('href') and alink['href'] in chapurlmap:
alink['href']=chapurlmap[alink['href']]
changed=True
if changed:
chap_data = unicode(soup)
# Don't want html, head or body tags in
# chapter html--bs4 insists on adding them.
chap_data = re.sub(r"</?(html|head|body)[^>]*>\r?\n?","",chap_data)
#logger.debug('Writing chapter text for: %s' % chap.title)
vals={'url':removeEntities(chap.url),
'chapter':removeEntities(chap.title),
@@ -670,7 +698,9 @@ div { margin: 0pt; padding: 0pt; }
for k,v in vals.items():
if isinstance(v,basestring): vals[k]=v.replace('"','&quot;')
fullhtml = CHAPTER_START.substitute(vals) + \
chap.html + CHAPTER_END.substitute(vals)
chap_data.strip() + \
CHAPTER_END.substitute(vals)
# strip to avoid ever growning numbers of newlines.
# 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
+38 -11
View File
@@ -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.
@@ -18,6 +18,8 @@
import logging
import string
import bs4
from base_writer import *
class HTMLWriter(BaseStoryWriter):
@@ -32,7 +34,7 @@ class HTMLWriter(BaseStoryWriter):
def __init__(self, config, story):
BaseStoryWriter.__init__(self, config, story)
self.HTML_FILE_START = string.Template('''<!DOCTYPE html>
<html>
<head>
@@ -48,7 +50,7 @@ ${output_css}
self.HTML_COVER = string.Template('''
<img src="${coverimg}" alt="cover" />
''')
self.HTML_TITLE_PAGE_START = string.Template('''
<table class="full">
''')
@@ -62,14 +64,14 @@ ${output_css}
''')
self.HTML_TOC_PAGE_START = string.Template('''
<a name="TOCTOP"><h2>Table of Contents</h2>
<a name="TOCTOP"><h2>Table of Contents</h2></a>
<p>
''')
self.HTML_TOC_ENTRY = string.Template('''
<a href="#section${index}">${chapter}</a><br />
''')
self.HTML_TOC_PAGE_END = string.Template('''
</p>
''')
@@ -100,12 +102,12 @@ ${output_css}
FILE_END = string.Template(self.getConfig("file_end"))
else:
FILE_END = self.HTML_FILE_END
self._write(out,FILE_START.substitute(self.story.getAllMetadata()))
if self.getConfig('include_images') and self.story.cover:
self._write(out,COVER.substitute(dict(self.story.getAllMetadata().items()+{'coverimg':self.story.cover}.items())))
self.writeTitlePage(out,
self.HTML_TITLE_PAGE_START,
self.HTML_TITLE_ENTRY,
@@ -120,18 +122,43 @@ ${output_css}
CHAPTER_START = string.Template(self.getConfig("chapter_start"))
else:
CHAPTER_START = self.HTML_CHAPTER_START
if self.hasConfig('chapter_end'):
CHAPTER_END = string.Template(self.getConfig("chapter_end"))
else:
CHAPTER_END = self.HTML_CHAPTER_END
## collect chapter urls and file names for internalize_text_links option.
chapurlmap = {}
for index, chap in enumerate(self.story.getChapters()):
if chap.html:
## HTML_CHAPTER_START needs to have matching <a>
## anchor to work. Which it does by default. This
## could also be made configurable if some user
## changed it.
chapurlmap[chap.url]="#section%04d"%(index+1) # url -> index
for index, chap in enumerate(self.story.getChapters()):
if chap.html:
chap_data = chap.html
if self.getConfig('internalize_text_links'):
soup = bs4.BeautifulSoup(chap.html,'html5lib')
changed=False
for alink in soup.find_all('a'):
if alink.has_attr('href') and alink['href'] in chapurlmap:
alink['href']=chapurlmap[alink['href']]
changed=True
if changed:
chap_data = unicode(soup)
# Don't want html, head or body tags in
# chapter html--bs4 insists on adding them.
chap_data = re.sub(r"</?(html|head|body)[^>]*>\r?\n?","",chap_data)
logging.debug('Writing chapter text for: %s' % chap.title)
vals={'url':chap.url, 'chapter':chap.title, 'index':"%04d"%(index+1), 'number':index+1}
self._write(out,CHAPTER_START.substitute(vals))
self._write(out,chap.html)
self._write(out,chap_data)
self._write(out,CHAPTER_END.substitute(vals))
self._write(out,FILE_END.substitute(self.story.getAllMetadata()))
@@ -139,4 +166,4 @@ ${output_css}
if self.getConfig('include_images'):
for imgmap in self.story.getImgUrls():
self.writeFile(imgmap['newsrc'],imgmap['data'])
+1 -1
View File
@@ -21,7 +21,7 @@ setup(
name="FanFicFare",
# Versions should comply with PEP440.
version="2.4.3",
version="2.5.1",
description='A tool for downloading fanfiction to eBook formats',
long_description=long_description,
+1 -1
View File
@@ -1,6 +1,6 @@
# ffd-retief-hrd fanficfare
application: fanficfare
version: 2-4-0
version: 2-5-0
runtime: python27
api_version: 1
threadsafe: true
+1 -1
View File
@@ -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-3-06.fanficfare.appspot.com">previous version
<a href="http://2-4-0.fanficfare.appspot.com">previous version
</a> is also available for you to use if necessary.
</p>
<div id='error'>