mirror of
https://github.com/wassname/FanFicFare.git
synced 2026-09-13 12:11:20 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0b4332da8 | ||
|
|
a1bd9c8379 | ||
|
|
e02c969371 | ||
|
|
b18b2177b0 | ||
|
|
5ca33837d4 | ||
|
|
eade66f513 | ||
|
|
28c4557d22 | ||
|
|
d6eda82767 | ||
|
|
28eff8ac12 | ||
|
|
7ae40e539d | ||
|
|
ab6436ca0b | ||
|
|
b5a04b0f97 | ||
|
|
c7bbb765b2 | ||
|
|
8a84043d29 | ||
|
|
f115d68e52 | ||
|
|
e2707f5459 | ||
|
|
aa85efd6d9 | ||
|
|
9e2c0a3563 | ||
|
|
d4f3fee053 | ||
|
|
e87c7b7009 | ||
|
|
5b6228166c | ||
|
|
f91092d9d8 | ||
|
|
a40383bada | ||
|
|
c9205dd6bc | ||
|
|
a0acbb8893 | ||
|
|
7d66d93b70 | ||
|
|
277b1ef92d | ||
|
|
72217423c7 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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+$")):
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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')
|
||||
|
||||
|
||||
@@ -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('"','"')
|
||||
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
|
||||
|
||||
@@ -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'])
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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'>
|
||||
|
||||
Reference in New Issue
Block a user