Merge branch 'master' into masseffect2in

This commit is contained in:
Dmitry Kozliuk
2015-07-22 17:29:33 +03:00
33 changed files with 1708 additions and 735 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division,
print_function)
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import,
print_function)
+10 -4
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import,
print_function)
@@ -89,7 +89,7 @@ from calibre_plugins.fanficfare_plugin.dialogs \
EditTextDialog, IniTextDialog, RejectUrlEntry)
from calibre_plugins.fanficfare_plugin.fanficfare.adapters \
import getConfigSections
import getSiteSections
from calibre_plugins.fanficfare_plugin.common_utils \
import ( KeyboardConfigDialog, PrefsViewerDialog )
@@ -272,6 +272,7 @@ class ConfigWidget(QWidget):
prefs['addtolists'] = self.readinglist_tab.addtolists.isChecked()
prefs['addtoreadlists'] = self.readinglist_tab.addtoreadlists.isChecked()
prefs['addtolistsonread'] = self.readinglist_tab.addtolistsonread.isChecked()
prefs['autounnew'] = self.readinglist_tab.autounnew.isChecked()
# personal.ini
ini = self.personalini_tab.personalini
@@ -781,6 +782,11 @@ class ReadingListTab(QWidget):
self.addtolistsonread.setChecked(prefs['addtolistsonread'])
self.l.addWidget(self.addtolistsonread)
self.autounnew = QCheckBox(_('Automatically run Remove "New" Chapter Marks when marking books "Read".'),self)
self.autounnew.setToolTip(_('Menu option to remove from "To Read" lists will also remove "(new)" chapter marks created by personal.ini <i>mark_new_chapters</i> setting.'))
self.autounnew.setChecked(prefs['autounnew'])
self.l.addWidget(self.autounnew)
self.l.insertStretch(-1)
class CalibreCoverTab(QWidget):
@@ -928,7 +934,7 @@ class CalibreCoverTab(QWidget):
self.gc_dropdowns = {}
sitelist = getConfigSections()
sitelist = getSiteSections()
sitelist.sort()
sitelist.insert(0,_("Default"))
for site in sitelist:
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division,
print_function)
+76 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import,
print_function)
@@ -66,7 +66,8 @@ from calibre_plugins.fanficfare_plugin.fanficfare import (
adapters, exceptions)
from calibre_plugins.fanficfare_plugin.fanficfare.epubutils import (
get_dcsource, get_dcsource_chaptercount, get_story_url_from_html)
get_dcsource, get_dcsource_chaptercount, get_story_url_from_html,
reset_orig_chapters_epub)
from calibre_plugins.fanficfare_plugin.fanficfare.geturls import (
get_urls_from_page, get_urls_from_html,get_urls_from_text,
@@ -335,6 +336,12 @@ class FanFicFarePlugin(InterfaceAction):
image='minusminus.png',
triggered=partial(self.update_lists,add=False))
self.menu.addSeparator()
self.get_list_action = self.create_menu_item_ex(self.menu, _('Remove "New" Chapter Marks from Selected books'),
unique_name='Remove "(new)" chapter marks created by personal.ini <i>mark_new_chapters</i> setting.',
image='edit-undo.png',
triggered=self.unnew_books)
self.menu.addSeparator()
self.get_list_action = self.create_menu_item_ex(self.menu, _('Get Story URLs from Selected Books'),
unique_name='Get URLs from Selected Books',
@@ -415,6 +422,8 @@ class FanFicFarePlugin(InterfaceAction):
return
self.update_reading_lists(self.gui.library_view.get_selected_ids(),add)
if not add and prefs['autounnew']:
self.unnew_books()
def get_urls_from_imap_menu(self):
@@ -555,6 +564,70 @@ class FanFicFarePlugin(InterfaceAction):
show=True,
show_copy_button=False)
def unnew_books(self):
'''Get list of URLs from existing books.'''
if not self.is_library_view():
self.gui.status_bar.show_message(_('Can only UnNew books in library'),
3000)
return
if not self.gui.current_view().selectionModel().selectedRows() :
self.gui.status_bar.show_message(_('No Selected Books to Get URLs From'),
3000)
return
book_list = map( partial(self.make_book_id_only),
self.gui.library_view.get_selected_ids() )
tdir = PersistentTemporaryDirectory(prefix='fanficfare_')
LoopProgressDialog(self.gui,
book_list,
partial(self.get_unnew_books_loop, db=self.gui.current_db, tdir=tdir),
partial(self.get_unnew_books_finish, tdir=tdir),
init_label=_("UnNewing books..."),
win_title=_("UnNew Books"),
status_prefix=_("Books UnNewed"))
def get_unnew_books_loop(self,book,db=None,tdir=None):
if book['calibre_id'] and db.has_format(book['calibre_id'],'EPUB',index_is_id=True):
tmp = PersistentTemporaryFile(prefix='%s-'%book['calibre_id'],
suffix='.epub',
dir=tdir)
db.copy_format_to(book['calibre_id'],'EPUB',tmp,index_is_id=True)
unnewtmp = PersistentTemporaryFile(prefix='unnew-%s-'%book['calibre_id'],
suffix='.epub',
dir=tdir)
book['changed']=reset_orig_chapters_epub(tmp,unnewtmp)
if book['changed']:
db.add_format_with_hooks(book['calibre_id'],
'EPUB',
unnewtmp,
index_is_id=True)
if prefs['deleteotherforms']:
fmts = db.formats(book['calibre_id'], index_is_id=True).split(',')
for fmt in fmts:
if fmt.lower() != formmapping['epub'].lower():
logger.debug("deleteotherforms remove f:"+fmt)
db.remove_format(book['calibre_id'], fmt, index_is_id=True)#, notify=False
elif prefs['autoconvert']:
## 'Convert Book'.auto_convert_auto_add doesn't convert if
## the format is already there.
fmt = calibre_prefs['output_format']
# delete if there, but not if the format we just made.
if fmt.lower() != 'epub' and db.has_format(book['calibre_id'],fmt,index_is_id=True):
logger.debug("autoconvert remove f:"+fmt)
db.remove_format(book['calibre_id'], fmt, index_is_id=True)#, notify=False
def get_unnew_books_finish(self, book_list, tdir=None):
remove_dir(tdir)
if prefs['autoconvert']:
changed_ids = [ x['calibre_id'] for x in book_list if x['changed'] ]
if changed_ids:
self.gui.status_bar.show_message(_('Starting auto conversion of %d books.')%(len(changed_ids)), 3000)
self.gui.iactions['Convert Books'].auto_convert_auto_add(changed_ids)
def reject_list_urls(self):
if self.is_library_view():
book_list = map( partial(self.make_book_id_only),
+6 -6
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import,
print_function)
@@ -23,12 +23,12 @@ def get_fff_personalini():
def get_fff_config(url,fileform="epub",personalini=None):
if not personalini:
personalini = get_fff_personalini()
site='unknown'
sections=['unknown']
try:
site = adapters.getConfigSectionFor(url)
sections = adapters.getConfigSectionsFor(url)
except Exception as e:
logger.debug("Failed trying to get ini config for url(%s): %s, using section [%s] instead"%(url,e,site))
configuration = Configuration(site,fileform)
logger.debug("Failed trying to get ini config for url(%s): %s, using section %s instead"%(url,e,sections))
configuration = Configuration(sections,fileform)
configuration.readfp(StringIO(get_resources("plugin-defaults.ini")))
configuration.readfp(StringIO(personalini))
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division,
print_function)
+5 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import,
print_function)
@@ -192,7 +192,9 @@ def do_download_for_worker(book,options,notification=lambda x,y:x):
adapter.oldimgs,
adapter.oldcover,
adapter.calibrebookmark,
adapter.logfile) = get_update_data(book['epub_for_update'])[0:7]
adapter.logfile,
adapter.oldchaptersmap,
adapter.oldchaptersdata) = get_update_data(book['epub_for_update'])[0:9]
# dup handling from fff_plugin needed for anthology updates.
if options['collision'] == UPDATE:
+103 -7
View File
@@ -293,12 +293,31 @@ chapter_title_strip_pattern:^[0-9]+[\.: -]+
## etc
#chapter_title_strip_pattern:^([0-9]+[\.: -]+)?(Chapter *[0-9]+[\.:, -]*)?
## Uses a python template substitution. The ${index} is the 'chapter'
## number and ${title} is the chapter title, after applying
## chapter_title_strip_pattern. Those are the only variables available.
## "The Beginning" => "1. The Beginning"
## If true, when updating an epub that already has old chapters, new
## chapters will be marked in the TOC and chapter header by using
## chapter_title_new_pattern and chapter_title_addnew_pattern to set the chapter.
mark_new_chapters:false
## chapter title patterns use python template substitution. The
## ${index} is the 'chapter' number and ${title} is the chapter title,
## after applying chapter_title_strip_pattern. Those are the only
## variables available.
## The basic pattern used when not using add_chapter_numbers or
## mark_new_chapters
chapter_title_def_pattern:${title}
## Pattern used with add_chapter_numbers, but not mark_new_chapters
chapter_title_add_pattern:${index}. ${title}
## Pattern used with mark_new_chapters, but not add_chapter_numbers
## (new) is just text and can be changed.
chapter_title_new_pattern:(new) ${title}
## Pattern used with add_chapter_numbers and mark_new_chapters
## (new) is just text and can be changed.
chapter_title_addnew_pattern:${index}. (new) ${title}
## Uses a python template substitution. The ${title} is the default
## title of a new anthology, <series name> in the case of a series, or
## the first book title otherwise. This is only applied to new
@@ -330,11 +349,74 @@ sort_ships:false
## User-agent
user_agent:FFF/2.X
## Virtually all eFiction Base adapters allow downloading the whole story in
## bulk using the 'Print' feature. If 'bulk_load' is set to 'true', both
## metadata and chapters can be loaded in one step
## Added for [base_xenforoforum], but can be used with other sites,
## too. Limit the 'description' to the first X *characters*
## collected. Character count includes HTML tags, so it can be
## non-intuitive.
#description_limit:1000
[base_efiction]
## At the time of writing, eFiction Base adapters allow downloading
## the whole story in bulk using the 'Print' feature. If 'bulk_load'
## is set to 'true', both metadata and chapters can be loaded in one
## step
bulk_load:true
[base_xenforoforum]
## Currently only forums.spacebattles.com and forums.sufficientvelocity.com
cover_exclusion_regexp:/clear.png
## I saw lots of chapters name simply '1.1' etc during testing.
strip_chapter_numbers:false
## Copy title to tagsfromtitle for parsing tags.
add_to_extra_valid_entries:,tagsfromtitle
## '.NOREPL' tells the system to *not* apply title's
## in/exclude/replace_metadata -- Only works on include_in_ lines.
include_in_tagsfromtitle:title.NOREPL
tagsfromtitle_label:Tags from Title
## might want to do this, maybe not. Will often include category, but
## also often include non-category stuff.
# include_in_category:tagsfromtitle
add_to_include_metadata_pre:
# only keep tagsfromtitle with ( or [ in.
tagsfromtitle=~[\[\(]
add_to_replace_metadata:
# remove anything outside () or []
tagsfromtitle=>^.*?([\(\[]([^\]\)]+)[\)\]]).*?$=>\1
# remove () []
tagsfromtitle=>[\(\)\[\]]=>
# change (spaces)slash(spaces) to comma
tagsfromtitle=> */ *=>,
tagsfromtitle=> x =>,
# remove [] or () blocks and leading/trailing spaces/dashes/colons
title=>[-: ]*[\(\[]([^\]\)]+)[\)\]][-: ]*=>
# remove 'Thread' and the next word, usually "Thread 2", "Thread
# four", "Thread iv", etc
title=>[-: ]*[Tt]hread [^ ]+[-: ]*=>
add_to_extra_titlepage_entries:,tagsfromtitle
## '.SPLIT' tells the system to split by ','
add_to_include_subject_tags:,tagsfromtitle.SPLIT
## base_xenforoforum reads Published and Updated datetimes from
## Threadmarks if used, or from the posted & updated times of the
## 'first' post if no threadmarks.
datePublished_format:%%Y-%%m-%%d %%H:%%M:%%S
dateUpdated_format:%%Y-%%m-%%d %%H:%%M:%%S
## Only take the first X characters of the 'first' post to use as
## the description.
description_limit:500
## Each output format has a section that overrides [defaults]
[html]
@@ -583,6 +665,8 @@ extratags: FanFiction,Testing,HTML
## doesn't like that. If do_update_hook is uncommented and set true,
## the adapter will discard all existing chapters from the newest one
## on when updating to enforce accurate chapters.
## Starting July 2015, FFF stores chapter URLs in the chapter files.
## Stories downloaded after that shouldn't need this setting anymore.
#do_update_hook:false
## AO3 adapter defines a few extra metadata entries.
@@ -1040,6 +1124,12 @@ extra_valid_entries:size
# don't show twitter icon.
cover_exclusion_regexp:/res/css/bir.png
[forums.spacebattles.com]
## see [base_xenforoforum]
[forums.sufficientvelocity.com]
## see [base_xenforoforum]
[grangerenchanted.com]
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
@@ -1181,6 +1271,12 @@ extracategories:NCIS
extracategories:Buffy: The Vampire Slayer
extracharacters:Willow
[ninelives.dark-solace.org]
## Site dedicated to these categories/characters/ships
extracategories:The Walking Dead
extracharacters:Carol,Daryl
extraships:Carol/Daryl
[nocturnal-light.net]
## Extra metadata that this adapter knows about. See [dramione.org]
## for examples of how to use them.
+3 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import,
print_function)
@@ -85,6 +85,7 @@ default_prefs['read_lists'] = ''
default_prefs['addtolists'] = False
default_prefs['addtoreadlists'] = False
default_prefs['addtolistsonread'] = False
default_prefs['autounnew'] = False
default_prefs['updatecalcover'] = None
default_prefs['gencalcover'] = SAVE_YES
File diff suppressed because it is too large Load Diff
+19 -6
View File
@@ -135,6 +135,9 @@ import adapter_fanfictionjunkiesde
import adapter_devianthearts
import adapter_tgstorytimecom
import adapter_itcouldhappennet
import adapter_forumsspacebattlescom
import adapter_forumssufficientvelocitycom
import adapter_ninelivesdarksolaceorg
import adapter_masseffect2in
## This bit of complexity allows adapters to be added by just adding
@@ -195,14 +198,24 @@ def getAdapter(config,url,anyurl=False):
# No adapter found.
raise exceptions.UnknownSite( url, [cls.getSiteDomain() for cls in __class_list] )
def getConfigSections():
def getSiteSections():
# doesn't include base sections. Sections rather than site DNS because of squidge/peja
return [cls.getConfigSection() for cls in __class_list]
def getConfigSections():
# does include base sections.
sections = set()
for cls in __class_list:
sections.update(cls.getConfigSections())
return sections
def get_bulk_load_sites():
# for now, all eFiction Base adapters are assumed to allow bulk_load.
return [cls.getConfigSection().replace('www.','') for cls in
filter( lambda x : issubclass(x,base_efiction_adapter.BaseEfictionAdapter),
__class_list)]
sections = set()
for cls in filter( lambda x : issubclass(x,base_efiction_adapter.BaseEfictionAdapter),
__class_list):
sections.update( [ x.replace('www.','') for x in cls.getConfigSections() ] )
return sections
def getSiteExamples():
l=[]
@@ -210,10 +223,10 @@ def getSiteExamples():
l.append((cls.getConfigSection(),cls.getSiteExampleURLs().split()))
return l
def getConfigSectionFor(url):
def getConfigSectionsFor(url):
(cls,fixedurl) = getClassFor(url)
if cls:
return cls.getConfigSection()
return cls.getConfigSections()
# No adapter found.
raise exceptions.UnknownSite( url, [cls.getSiteDomain() for cls in __class_list] )
@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
# Copyright 2015 FanFicFare team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import time
import logging
logger = logging.getLogger(__name__)
import re
import urllib2
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_xenforoforum_adapter import BaseXenForoForumAdapter
def getClass():
return ForumsSpacebattlesComAdapter
class ForumsSpacebattlesComAdapter(BaseXenForoForumAdapter):
def __init__(self, config, url):
BaseXenForoForumAdapter.__init__(self, config, url)
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','fsb')
@staticmethod # must be @staticmethod, don't remove it.
def getSiteDomain():
# The site domain. Does have www here, if it uses it.
return 'forums.spacebattles.com'
@classmethod
def getURLPrefix(cls):
return 'https://' + cls.getSiteDomain()
@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Copyright 2015 FanFicFare team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from base_xenforoforum_adapter import BaseXenForoForumAdapter
def getClass():
return ForumsSufficientVelocityComAdapter
class ForumsSufficientVelocityComAdapter(BaseXenForoForumAdapter):
def __init__(self, config, url):
BaseXenForoForumAdapter.__init__(self, config, url)
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','fsv')
@staticmethod # must be @staticmethod, don't remove it.
def getSiteDomain():
# The site domain. Does have www here, if it uses it.
return 'forums.sufficientvelocity.com'
@classmethod
def getURLPrefix(cls):
return 'http://' + cls.getSiteDomain()
@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Fanficdownloader team, 2015 FanFicFare team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Software: eFiction
import re
from base_efiction_adapter import BaseEfictionAdapter
class NineLivesDarkSolaceAdapter(BaseEfictionAdapter):
@staticmethod
def getSiteDomain():
return 'ninelives.dark-solace.org'
@classmethod
def getSiteAbbrev(self):
return '9lvs'
@classmethod
def getDateFormat(self):
return "%B %d, %Y"
def getClass():
return NineLivesDarkSolaceAdapter
+7 -3
View File
@@ -106,7 +106,9 @@ class SpikeluverComAdapter(BaseSiteAdapter):
listbox_tag = soup.find('div', {'class': 'listbox'})
for span_tag in listbox_tag('span'):
key = span_tag.string.strip(' :')
key = span_tag.string
if key:
key = key.strip(' :')
try:
value = stripHTML(span_tag.nextSibling)
# This can happen with some fancy markup in the summary. Just
@@ -135,8 +137,10 @@ class SpikeluverComAdapter(BaseSiteAdapter):
contents.append(sibling)
# Remove the preceding break line tag and other crud
contents.pop()
contents.pop()
if contents:
contents.pop()
if contents:
contents.pop()
self.story.setMetadata('description', ''.join(contents))
elif key == 'Rated':
+86 -39
View File
@@ -104,6 +104,8 @@ class BaseSiteAdapter(Configurable):
self.chapterFirst = None
self.chapterLast = None
self.oldchapters = None
self.oldchaptersmap = None
self.oldchaptersdata = None
self.oldimgs = None
self.oldcover = None # (data of existing cover html, data of existing cover image)
self.calibrebookmark = None
@@ -140,28 +142,6 @@ class BaseSiteAdapter(Configurable):
'''
self.get_cookiejar().load(filename, ignore_discard=True, ignore_expires=True)
# def save_cookiejar(self,filename):
# '''
# Assumed to be a FileCookieJar if self.cookiejar set.
# Takes file *name*.
# '''
# self.get_cookiejar().save(filename, ignore_discard=True, ignore_expires=True)
# def save_pagecache(self,filename):
# '''
# Writes pickle of pagecache to file *name*
# '''
# with open(filename, 'wb') as f:
# pickle.dump(self.get_pagecache(),
# f,protocol=pickle.HIGHEST_PROTOCOL)
# def load_pagecache(self,filename):
# '''
# Reads pickle of pagecache from file *name*
# '''
# with open(filename, 'rb') as f:
# self.set_pagecache(pickle.load(f))
def get_pagecache(self):
return self.pagecache
@@ -185,9 +165,9 @@ class BaseSiteAdapter(Configurable):
else:
return None
def _set_to_pagecache(self,cachekey,data):
def _set_to_pagecache(self,cachekey,data,redirectedurl):
if self.use_pagecache():
self.get_pagecache()[cachekey] = data
self.get_pagecache()[cachekey] = (data,redirectedurl)
def use_pagecache(self):
'''
@@ -257,7 +237,8 @@ class BaseSiteAdapter(Configurable):
cachekey=self._get_cachekey(url, parameters, headers)
if usecache and self._has_cachekey(cachekey):
logger.debug("#####################################\npagecache HIT: %s"%cachekey)
return self._get_from_pagecache(cachekey)
data,redirecturl = self._get_from_pagecache(cachekey)
return data
logger.debug("#####################################\npagecache MISS: %s"%cachekey)
self.do_sleep(extrasleep)
@@ -272,13 +253,23 @@ class BaseSiteAdapter(Configurable):
data=urllib.urlencode(parameters),
headers=headers)
data = self._decode(self.opener.open(req,None,float(self.getConfig('connect_timeout',30.0))).read())
self._set_to_pagecache(cachekey,data)
self._set_to_pagecache(cachekey,data,url)
return data
def _fetchUrlRaw(self, url,
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...
@@ -289,16 +280,25 @@ class BaseSiteAdapter(Configurable):
cachekey=self._get_cachekey(url, parameters)
if usecache and self._has_cachekey(cachekey):
logger.debug("#####################################\npagecache HIT: %s"%cachekey)
return self._get_from_pagecache(cachekey)
data,redirecturl = self._get_from_pagecache(cachekey)
class FakeOpened:
def __init__(self,data,url):
self.data=data
self.url=url
def geturl(self): return self.url
def read(self): return self.data
return (data,FakeOpened(data,redirecturl))
logger.debug("#####################################\npagecache MISS: %s"%cachekey)
self.do_sleep(extrasleep)
if parameters != None:
data = self.opener.open(url.replace(' ','%20'),urllib.urlencode(parameters),float(self.getConfig('connect_timeout',30.0))).read()
opened = self.opener.open(url.replace(' ','%20'),urllib.urlencode(parameters),float(self.getConfig('connect_timeout',30.0)))
else:
data = self.opener.open(url.replace(' ','%20'),None,float(self.getConfig('connect_timeout',30.0))).read()
self._set_to_pagecache(cachekey,data)
return data
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)
@@ -312,20 +312,30 @@ class BaseSiteAdapter(Configurable):
elif self.getConfig('slow_down_sleep_time'):
time.sleep(float(self.getConfig('slow_down_sleep_time')))
# parameters is a dict()
def _fetchUrl(self, url,
parameters=None,
usecache=True,
extrasleep=None):
return self._fetchUrlOpened(url,
parameters,
usecache,
extrasleep)[0]
# parameters is a dict()
def _fetchUrlOpened(self, url,
parameters=None,
usecache=True,
extrasleep=None):
excpt=None
for sleeptime in [0, 0.5, 4, 9]:
time.sleep(sleeptime)
try:
return self._decode(self._fetchUrlRaw(url,
(data,opened)=self._fetchUrlRawOpened(url,
parameters=parameters,
usecache=usecache,
extrasleep=extrasleep))
extrasleep=extrasleep)
return (self._decode(data),opened)
except u2.HTTPError, he:
excpt=he
if he.code == 404:
@@ -352,21 +362,40 @@ class BaseSiteAdapter(Configurable):
self.getStoryMetadataOnly(get_cover=True)
for index, (title,url) in enumerate(self.chapterUrls):
newchap = False
if (self.chapterFirst!=None and index < self.chapterFirst) or \
(self.chapterLast!=None and index > self.chapterLast):
self.story.addChapter(url,
removeEntities(title),
None)
else:
if self.oldchapters and index < len(self.oldchapters):
data = None
if self.oldchaptersmap:
if url in self.oldchaptersmap:
data = self.utf8FromSoup(None,
self.oldchaptersmap[url],
partial(cachedfetch,self._fetchUrlRaw,self.oldimgs))
elif self.oldchapters and index < len(self.oldchapters):
data = self.utf8FromSoup(None,
self.oldchapters[index],
partial(cachedfetch,self._fetchUrlRaw,self.oldimgs))
else:
# if already marked new -- ie, origtitle and title don't match
# logger.debug("self.oldchaptersdata[url]:%s"%(self.oldchaptersdata[url]))
newchap = (self.oldchaptersdata is not None and
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
newchap = bool(self.oldchapters or self.oldchaptersmap)
self.story.addChapter(url,
removeEntities(title),
removeEntities(data))
removeEntities(data),
newchap)
self.storyDone = True
# include image, but no cover from story, add default_cover_image cover.
@@ -399,7 +428,10 @@ class BaseSiteAdapter(Configurable):
self.doExtractChapterUrlsAndMetadata(get_cover=get_cover)
if not self.story.getMetadataRaw('dateUpdated'):
self.story.setMetadata('dateUpdated',self.story.getMetadataRaw('datePublished'))
if self.story.getMetadataRaw('datePublished'):
self.story.setMetadata('dateUpdated',self.story.getMetadataRaw('datePublished'))
else:
self.story.setMetadata('dateUpdated',self.story.getMetadataRaw('dateCreated'))
self.metadataDone = True
return self.story
@@ -409,7 +441,10 @@ class BaseSiteAdapter(Configurable):
self.story.load_html_metadata(metahtml)
self.metadataDone = True
if not self.story.getMetadataRaw('dateUpdated'):
self.story.setMetadata('dateUpdated',self.story.getMetadataRaw('datePublished'))
if self.story.getMetadataRaw('datePublished'):
self.story.setMetadata('dateUpdated',self.story.getMetadataRaw('datePublished'))
else:
self.story.setMetadata('dateUpdated',self.story.getMetadataRaw('dateCreated'))
def hookForUpdates(self,chaptercount):
"Usually not needed."
@@ -427,6 +462,11 @@ class BaseSiteAdapter(Configurable):
"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"
@@ -476,6 +516,13 @@ class BaseSiteAdapter(Configurable):
def setDescription(self,url,svalue):
#print("\n\nsvalue:\n%s\n"%svalue)
strval = u"%s"%svalue # works for either soup or string
if self.hasConfig('description_limit'):
limit = int(self.getConfig('description_limit'))
if limit and len(strval) > limit:
svalue = strval[:limit]
#print(u"[[[[[\n\n%s\n\n]]]]]]]]"%svalue) # works for either soup or string
if self.getConfig('keep_summary_html'):
if isinstance(svalue,basestring):
# bs4/html5lib add html, header and body tags, which
@@ -70,6 +70,11 @@ class BaseEfictionAdapter(BaseSiteAdapter):
self.triedAcceptWarnings = False
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
@classmethod
def getConfigSections(cls):
"Only needs to be overriden if has additional ini sections."
return ['base_efiction',cls.getConfigSection()]
@classmethod
def getAcceptDomains(cls):
return [cls.getSiteDomain(),'www.' + cls.getSiteDomain()]
@@ -0,0 +1,229 @@
# -*- coding: utf-8 -*-
# Copyright 2015 FanFicFare team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import time
import logging
logger = logging.getLogger(__name__)
import re
import urllib2
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, makeDate
logger = logging.getLogger(__name__)
class BaseXenForoForumAdapter(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.
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
# get storyId from url--url validation guarantees query correct
m = re.match(self.getSiteURLPattern(),url)
if m:
self.story.setMetadata('storyId',m.group('id'))
# normalized story URL.
self._setURL(self.getURLPrefix() + '/'+m.group('tp')+'/'+self.story.getMetadata('storyId')+'/')
else:
raise exceptions.InvalidStoryURL(url,
self.getSiteDomain(),
self.getSiteExampleURLs())
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','fsb')
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%b %d, %Y at %I:%M %p"
@classmethod
def getConfigSections(cls):
"Only needs to be overriden if has additional ini sections."
return ['base_xenforoforum',cls.getConfigSection()]
@classmethod
def getURLPrefix(cls):
# The site domain. Does have www here, if it uses it.
return 'https://' + cls.getSiteDomain()
@classmethod
def getSiteExampleURLs(cls):
return cls.getURLPrefix()+"/threads/some-story-name.123456/"
def getSiteURLPattern(self):
return r"https?://"+re.escape(self.getSiteDomain())+r"/(?P<tp>threads|posts)/(.+\.)?(?P<id>\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):
useurl = self.url
logger.info("url: "+useurl)
try:
(data,opened) = self._fetchUrlOpened(useurl)
useurl = opened.geturl()
logger.info("use useurl: "+useurl)
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)
a = soup.find('h3',{'class':'userText'}).find('a')
self.story.addToList('authorId',a['href'].split('/')[1])
self.story.addToList('authorUrl',self.getURLPrefix()+'/'+a['href'])
self.story.addToList('author',a.text)
h1 = soup.find('div',{'class':'titleBar'}).h1
self.story.setMetadata('title',stripHTML(h1))
if '#' in useurl:
anchorid = useurl.split('#')[1]
soup = soup.find('li',id=anchorid)
else:
# try threadmarks if no '#' in , require at least 2.
threadmarksa = soup.find('a',{'class':'threadmarksTrigger'})
if threadmarksa:
soupmarks = self.make_soup(self._fetchUrl(self.getURLPrefix()+'/'+threadmarksa['href']))
markas = soupmarks.find('ol',{'class':'overlayScroll'}).find_all('a')
if len(markas) > 1:
for (atag,url,name) in [ (x,x['href'],stripHTML(x)) for x in markas ]:
date = self.make_date(atag.find_next_sibling('div',{'class':'extra'}))
if not self.story.getMetadataRaw('datePublished') or date < self.story.getMetadataRaw('datePublished'):
self.story.setMetadata('datePublished', date)
if not self.story.getMetadataRaw('dateUpdated') or date > self.story.getMetadataRaw('dateUpdated'):
self.story.setMetadata('dateUpdated', date)
self.chapterUrls.append((name,self.getURLPrefix()+'/'+url))
soup = soup.find('li',{'class':'message'}) # limit first post for date stuff below. ('#' posts above)
# Now go hunting for the 'chapter list'.
bq = soup.find('blockquote') # assume first posting contains TOC urls.
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>
self.setDescription(useurl,bq)
# otherwise, use first post links--include first post since
# that's often also the first chapter.
if not self.chapterUrls:
self.chapterUrls.append(("First Post",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):
# brute force way to deal with SB's http->https change when hardcoded http urls.
url = url.replace('http://'+self.getSiteDomain(),self.getURLPrefix())
logger.debug("used chapurl:%s"%(url))
self.chapterUrls.append((name,url))
if url == useurl and 'First Post' == self.chapterUrls[0][0]:
# remove "First Post" if included in list.
logger.debug("delete dup 'First Post' chapter: %s %s"%self.chapterUrls[0])
del self.chapterUrls[0]
# Didn't use threadmarks, so take created/updated dates
# from the 'first' posting created and updated.
date = self.make_date(soup.find('a',{'class':'datePermalink'}))
if date:
self.story.setMetadata('datePublished', date)
self.story.setMetadata('dateUpdated', date) # updated overwritten below if found.
date = self.make_date(soup.find('div',{'class':'editDate'}))
if date:
self.story.setMetadata('dateUpdated', date)
self.story.setMetadata('numChapters',len(self.chapterUrls))
def make_date(self,parenttag): # forums use a BS thing where dates
# can appear different if recent.
datestr=None
try:
datetag = parenttag.find('span',{'class':'DateTime'})
if datetag:
datestr = datetag['title']
else:
datetag = parenttag.find('abbr',{'class':'DateTime'})
if datetag:
datestr="%s at %s"%(datetag['data-datestring'],datetag['data-timestring'])
# Apr 24, 2015 at 4:39 AM
# May 1, 2015 at 5:47 AM
datestr = re.sub(r' (\d[^\d])',r' 0\1',datestr) # add leading 0 for single digit day & hours.
return makeDate(datestr, self.dateformat)
except:
logger.debug('No date found in %s'%parenttag)
return None
# grab the text for an individual chapter.
def getChapterText(self, url):
logger.debug('Getting chapter text from: %s' % url)
origurl = url
(data,opened) = self._fetchUrlOpened(url)
url = opened.geturl()
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>
return self.utf8FromSoup(url,bq)
+20 -4
View File
@@ -41,12 +41,14 @@ try:
# running under calibre
from calibre_plugins.fanfictiondownloader_plugin.fanficfare import adapters, writers, exceptions
from calibre_plugins.fanfictiondownloader_plugin.fanficfare.configurable import Configuration
from calibre_plugins.fanfictiondownloader_plugin.fanficfare.epubutils import get_dcsource_chaptercount, get_update_data
from calibre_plugins.fanfictiondownloader_plugin.fanficfare.epubutils import (
get_dcsource_chaptercount, get_update_data, reset_orig_chapters_epub)
from calibre_plugins.fanfictiondownloader_plugin.fanficfare.geturls import get_urls_from_page
except ImportError:
from fanficfare import adapters, writers, exceptions
from fanficfare.configurable import Configuration
from fanficfare.epubutils import get_dcsource_chaptercount, get_update_data
from fanficfare.epubutils import (
get_dcsource_chaptercount, get_update_data, reset_orig_chapters_epub)
from fanficfare.geturls import get_urls_from_page
@@ -87,6 +89,9 @@ def main(argv=None, parser=None, passed_defaultsini=None, passed_personalini=Non
parser.add_option('-u', '--update-epub',
action='store_true', dest='update',
help='Update an existing epub with new chapters, give epub filename instead of storyurl.', )
parser.add_option('--unnew',
action='store_true', dest='unnew',
help='Remove (new) chapter marks left by mark_new_chapters setting.', )
parser.add_option('--update-cover',
action='store_true', dest='updatecover',
help='Update cover in an existing epub, otherwise existing cover (if any) is used on update. Only valid with --update-epub.', )
@@ -129,6 +134,9 @@ def main(argv=None, parser=None, passed_defaultsini=None, passed_personalini=Non
if options.update and options.format != 'epub':
parser.error('-u/--update-epub only works with epub')
if options.unnew and options.format != 'epub':
parser.error('--unnew only works with epub')
# for passing in a file list
if options.infile:
urls=[]
@@ -168,6 +176,12 @@ def do_download(arg,
# Attempt to update an existing epub.
chaptercount = None
output_filename = None
if options.unnew:
# remove mark_new_chapters marks
reset_orig_chapters_epub(arg,arg)
return
if options.update:
try:
url, chaptercount = get_dcsource_chaptercount(arg)
@@ -184,7 +198,7 @@ def do_download(arg,
url = arg
try:
configuration = Configuration(adapters.getConfigSectionFor(url), options.format)
configuration = Configuration(adapters.getConfigSectionsFor(url), options.format)
except exceptions.UnknownSite, e:
if options.list or options.normalize:
# list for page doesn't have to be a supported site.
@@ -324,7 +338,9 @@ def do_download(arg,
adapter.oldimgs,
adapter.oldcover,
adapter.calibrebookmark,
adapter.logfile) = (get_update_data(output_filename))[0:7]
adapter.logfile,
adapter.oldchaptersmap,
adapter.oldchaptersdata) = (get_update_data(output_filename))[0:9]
print 'Do update - epub(%d) vs url(%d)' % (chaptercount, urlchaptercount)
+22 -3
View File
@@ -77,10 +77,12 @@ 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)
# also allows [www.base_efiction] and [www.base_forum]. Not
# likely to matter.
if section.startswith('www.'):
# add w/o www if has www
sitesections.append(section[4:])
@@ -130,6 +132,7 @@ def get_valid_set_options():
'replace_hr':(None,None,boollist),
'sort_ships':(None,None,boollist),
'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),
@@ -210,8 +213,12 @@ def get_valid_keywords():
'bulk_load',
'chapter_end',
'chapter_start',
'chapter_title_add_pattern',
'chapter_title_strip_pattern',
'chapter_title_def_pattern',
'chapter_title_add_pattern',
'chapter_title_new_pattern',
'chapter_title_addnew_pattern',
'mark_new_chapters',
'check_next_chapter',
'skip_author_cover',
'collect_series',
@@ -224,6 +231,7 @@ def get_valid_keywords():
'datePublished_format',
'dateUpdated_format',
'default_cover_image',
'description_limit',
'do_update_hook',
'exclude_notes',
'extra_logpage_entries',
@@ -331,7 +339,8 @@ def make_generate_cover_settings(param):
class Configuration(ConfigParser.SafeConfigParser):
def __init__(self, site, fileform):
def __init__(self, sections, fileform):
site = sections[-1] # first section is site DN.
ConfigParser.SafeConfigParser.__init__(self)
self.linenos=dict() # key by section or section,key -> lineno
@@ -339,6 +348,11 @@ class Configuration(ConfigParser.SafeConfigParser):
## [injected] section has even less priority than [defaults]
self.sectionslist = ['defaults','injected']
## add other sections (not including site DN) after defaults,
## but before site-specific.
for section in sections[:-1]:
self.addConfigSection(section)
if site.startswith("www."):
sitewith = site
sitewithout = site.replace("www.","")
@@ -348,8 +362,13 @@ class Configuration(ConfigParser.SafeConfigParser):
self.addConfigSection(sitewith)
self.addConfigSection(sitewithout)
if fileform:
self.addConfigSection(fileform)
## add other sections:fileform (not including site DN)
## after fileform, but before site-specific:fileform.
for section in sections[:-1]:
self.addConfigSection(section+":"+fileform)
self.addConfigSection(sitewith+":"+fileform)
self.addConfigSection(sitewithout+":"+fileform)
self.addConfigSection("overrides")
+103 -7
View File
@@ -300,12 +300,31 @@ chapter_title_strip_pattern:^[0-9]+[\.: -]+
## etc
#chapter_title_strip_pattern:^([0-9]+[\.: -]+)?(Chapter *[0-9]+[\.:, -]*)?
## Uses a python template substitution. The ${index} is the 'chapter'
## number and ${title} is the chapter title, after applying
## chapter_title_strip_pattern. Those are the only variables available.
## "The Beginning" => "1. The Beginning"
## If true, when updating an epub that already has old chapters, new
## chapters will be marked in the TOC and chapter header by using
## chapter_title_new_pattern and chapter_title_addnew_pattern to set the chapter.
mark_new_chapters:false
## chapter title patterns use python template substitution. The
## ${index} is the 'chapter' number and ${title} is the chapter title,
## after applying chapter_title_strip_pattern. Those are the only
## variables available.
## The basic pattern used when not using add_chapter_numbers or
## mark_new_chapters
chapter_title_def_pattern:${title}
## Pattern used with add_chapter_numbers, but not mark_new_chapters
chapter_title_add_pattern:${index}. ${title}
## Pattern used with mark_new_chapters, but not add_chapter_numbers
## (new) is just text and can be changed.
chapter_title_new_pattern:(new) ${title}
## Pattern used with add_chapter_numbers and mark_new_chapters
## (new) is just text and can be changed.
chapter_title_addnew_pattern:${index}. (new) ${title}
## Reorder ships so b/a and c/b/a become a/b and a/b/c. Only separates
## on '/', so use replace_metadata to change separator first if
## needed. Something like: ships=>[ ]*(/|&amp;|&)[ ]*=>/ You can use
@@ -327,11 +346,74 @@ sort_ships:false
## User-agent
user_agent:FFF/2.X
## Virtually all eFiction Base adapters allow downloading the whole story in
## bulk using the 'Print' feature. If 'bulk_load' is set to 'true', both
## metadata and chapters can be loaded in one step
## Added for [base_xenforoforum], but can be used with other sites,
## too. Limit the 'description' to the first X *characters*
## collected. Character count includes HTML tags, so it can be
## non-intuitive.
#description_limit:1000
[base_efiction]
## At the time of writing, eFiction Base adapters allow downloading
## the whole story in bulk using the 'Print' feature. If 'bulk_load'
## is set to 'true', both metadata and chapters can be loaded in one
## step
bulk_load:true
[base_xenforoforum]
## Currently only forums.spacebattles.com and forums.sufficientvelocity.com
cover_exclusion_regexp:/clear.png
## I saw lots of chapters name simply '1.1' etc during testing.
strip_chapter_numbers:false
## Copy title to tagsfromtitle for parsing tags.
add_to_extra_valid_entries:,tagsfromtitle
## '.NOREPL' tells the system to *not* apply title's
## in/exclude/replace_metadata -- Only works on include_in_ lines.
include_in_tagsfromtitle:title.NOREPL
tagsfromtitle_label:Tags from Title
## might want to do this, maybe not. Will often include category, but
## also often include non-category stuff.
# include_in_category:tagsfromtitle
add_to_include_metadata_pre:
# only keep tagsfromtitle with ( or [ in.
tagsfromtitle=~[\[\(]
add_to_replace_metadata:
# remove anything outside () or []
tagsfromtitle=>^.*?([\(\[]([^\]\)]+)[\)\]]).*?$=>\1
# remove () []
tagsfromtitle=>[\(\)\[\]]=>
# change (spaces)slash(spaces) to comma
tagsfromtitle=> */ *=>,
tagsfromtitle=> x =>,
# remove [] or () blocks and leading/trailing spaces/dashes/colons
title=>[-: ]*[\(\[]([^\]\)]+)[\)\]][-: ]*=>
# remove 'Thread' and the next word, usually "Thread 2", "Thread
# four", "Thread iv", etc
title=>[-: ]*[Tt]hread [^ ]+[-: ]*=>
add_to_extra_titlepage_entries:,tagsfromtitle
## '.SPLIT' tells the system to split by ','
add_to_include_subject_tags:,tagsfromtitle.SPLIT
## base_xenforoforum reads Published and Updated datetimes from
## Threadmarks if used, or from the posted & updated times of the
## 'first' post if no threadmarks.
datePublished_format:%%Y-%%m-%%d %%H:%%M:%%S
dateUpdated_format:%%Y-%%m-%%d %%H:%%M:%%S
## Only take the first X characters of the 'first' post to use as
## the description.
description_limit:500
## Each output format has a section that overrides [defaults]
[html]
@@ -587,6 +669,8 @@ extratags: FanFiction,Testing,HTML
## doesn't like that. If do_update_hook is uncommented and set true,
## the adapter will discard all existing chapters from the newest one
## on when updating to enforce accurate chapters.
## Starting July 2015, FFF stores chapter URLs in the chapter files.
## Stories downloaded after that shouldn't need this setting anymore.
#do_update_hook:false
## AO3 adapter defines a few extra metadata entries.
@@ -1026,6 +1110,12 @@ extra_valid_entries:size
# don't show twitter icon.
cover_exclusion_regexp:/res/css/bir.png
[forums.spacebattles.com]
## see [base_xenforoforum]
[forums.sufficientvelocity.com]
## see [base_xenforoforum]
[grangerenchanted.com]
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
@@ -1156,6 +1246,12 @@ extracategories:NCIS
extracategories:Buffy: The Vampire Slayer
extracharacters:Willow
[ninelives.dark-solace.org]
## Site dedicated to these categories/characters/ships
extracategories:The Walking Dead
extracharacters:Carol,Daryl
extraships:Carol/Daryl
[nocturnal-light.net]
## Extra metadata that this adapter knows about. See [dramione.org]
## for examples of how to use them.
+141 -12
View File
@@ -1,18 +1,17 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
# -*- coding: utf-8 -*-
__license__ = 'GPL v3'
__copyright__ = '2014, Jim Miller'
__copyright__ = '2015, Jim Miller'
__docformat__ = 'restructuredtext en'
import logging
logger = logging.getLogger(__name__)
import re, os, traceback
from zipfile import ZipFile
from collections import defaultdict
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
from xml.dom.minidom import parseString
from StringIO import StringIO
import bs4 as bs
@@ -90,7 +89,9 @@ def get_update_data(inputio,
filecount = 0
soups = [] # list of xhmtl blocks
urlsoups = {} # map of xhtml blocks by url
images = {} # dict() longdesc->data
datamaps = defaultdict(dict) # map of data maps by url
if getfilecount:
# spin through the manifest--only place there are item tags.
for item in contentdom.getElementsByTagName("item"):
@@ -124,20 +125,45 @@ def get_update_data(inputio,
logger.warn("Image %s not found!\n(originally:%s)"%(newsrc,longdesc))
logger.warn("Exception: %s"%(unicode(e)))
traceback.print_exc()
soup = soup.find('body')
bodysoup = soup.find('body')
# ffdl epubs have chapter title h3
h3 = soup.find('h3')
h3 = bodysoup.find('h3')
if h3:
h3.extract()
# TtH epubs have chapter title h2
h2 = soup.find('h2')
h2 = bodysoup.find('h2')
if h2:
h2.extract()
for skip in soup.findAll(attrs={'class':'skip_on_ffdl_update'}):
for skip in bodysoup.findAll(attrs={'class':'skip_on_ffdl_update'}):
skip.extract()
## <meta name="chapterurl" content="${url}"></meta>
#print("look for meta chapurl")
currenturl = None
chapurl = soup.find('meta',{'name':'chapterurl'})
if chapurl:
if chapurl['content'] not in urlsoups: # keep first found if more than one.
#print("Found chapurl['content']:%s"%chapurl['content'])
currenturl = chapurl['content']
urlsoups[chapurl['content']] = bodysoup
else:
# for older pre-meta. Only temp.
chapa = bodysoup.find('a',{'class':'chapterurl'})
if chapa and chapa['href'] not in urlsoups: # keep first found if more than one.
urlsoups[chapa['href']] = bodysoup
currenturl = chapa['href']
chapa.extract()
chapterorigtitle = soup.find('meta',{'name':'chapterorigtitle'})
if chapterorigtitle:
datamaps[currenturl]['chapterorigtitle'] = chapterorigtitle['content']
soups.append(soup)
chaptertitle = soup.find('meta',{'name':'chaptertitle'})
if chaptertitle:
datamaps[currenturl]['chaptertitle'] = chaptertitle['content']
soups.append(bodysoup)
filecount+=1
@@ -148,7 +174,8 @@ def get_update_data(inputio,
#for k in images.keys():
#print("\tlongdesc:%s\n\tData len:%s\n"%(k,len(images[k])))
return (source,filecount,soups,images,oldcover,calibrebookmark,logfile)
# print("datamaps:%s"%datamaps)
return (source,filecount,soups,images,oldcover,calibrebookmark,logfile,urlsoups,datamaps)
def get_path_part(n):
relpath = os.path.dirname(n)
@@ -192,3 +219,105 @@ def get_story_url_from_html(inputio,_is_good_url=None):
if _is_good_url == None or _is_good_url(ahref):
return ahref
return None
def reset_orig_chapters_epub(inputio,outfile):
inputepub = ZipFile(inputio, 'r') # works equally well with a path or a blob
## build zip in memory in case updating in place(CLI).
zipio = StringIO()
## Write mimetype file, must be first and uncompressed.
## Older versions of python(2.4/5) don't allow you to specify
## compression by individual file.
## Overwrite if existing output file.
outputepub = ZipFile(zipio, 'w', compression=ZIP_STORED)
outputepub.debug = 3
outputepub.writestr("mimetype", "application/epub+zip")
outputepub.close()
## Re-open file for content.
outputepub = ZipFile(zipio, "a", compression=ZIP_DEFLATED)
outputepub.debug = 3
changed = False
tocncxdom = parseString(inputepub.read('toc.ncx'))
## spin through file contents.
for zf in inputepub.namelist():
if zf not in ['mimetype','toc.ncx'] :
entrychanged = False
data = inputepub.read(zf)
# if isinstance(data,unicode):
# logger.debug("\n\n\ndata is unicode\n\n\n")
if re.match(r'.*/file\d+\.xhtml',zf):
data = data.decode('utf-8')
soup = bs.BeautifulSoup(data,"html5lib")
chapterorigtitle = None
tag = soup.find('meta',{'name':'chapterorigtitle'})
if tag:
chapterorigtitle = tag['content']
# toctitle is separate for add_chapter_numbers:toconly users.
chaptertoctitle = None
tag = soup.find('meta',{'name':'chaptertoctitle'})
if tag:
chaptertoctitle = tag['content']
elif chapterorigtitle:
chaptertoctitle = chapterorigtitle
chaptertitle = None
tag = soup.find('meta',{'name':'chaptertitle'})
if tag:
chaptertitle = tag['content']
if chaptertitle and chapterorigtitle and chapterorigtitle != chaptertitle:
origdata = data
# print("\n%s\n%s\n"%(chapterorigtitle,chaptertitle))
data = data.replace(u'<meta name="chaptertitle" content="'+chaptertitle+u'"></meta>',
u'<meta name="chaptertitle" content="'+chapterorigtitle+u'"></meta>')
data = data.replace(u'<title>'+chaptertitle+u'</title>',u'<title>'+chapterorigtitle+u'</title>')
data = data.replace(u'<h3>'+chaptertitle+u'</h3>',u'<h3>'+chapterorigtitle+u'</h3>')
entrychanged = ( origdata != data )
changed = changed or entrychanged
if entrychanged:
## go after the TOC entry, too.
# <navPoint id="file0005" playOrder="6">
# <navLabel>
# <text>5. (new) Chapter 4</text>
# </navLabel>
# <content src="OEBPS/file0005.xhtml"/>
# </navPoint>
for contenttag in tocncxdom.getElementsByTagName("content"):
if contenttag.getAttribute('src') == zf:
texttag = contenttag.parentNode.getElementsByTagName('navLabel')[0].getElementsByTagName('text')[0]
texttag.childNodes[0].replaceWholeText(chaptertoctitle)
# logger.debug("text label:%s"%texttag.toxml())
continue
outputepub.writestr(zf,data.encode('utf-8'))
else:
# possibly binary data, thus no .encode().
outputepub.writestr(zf,data)
outputepub.writestr('toc.ncx',tocncxdom.toxml(encoding='utf-8'))
outputepub.close()
# declares all the files created by Windows. otherwise, when
# it runs in appengine, windows unzips the files as 000 perms.
for zf in outputepub.filelist:
zf.create_system = 0
# only *actually* write if changed.
if changed:
if isinstance(outfile,basestring):
with open(outfile,"wb") as outputio:
outputio.write(zipio.getvalue())
else:
outfile.write(zipio.getvalue())
inputepub.close()
zipio.close()
return changed
+73 -17
View File
@@ -16,6 +16,7 @@
#
import os, re
from collections import namedtuple
import urlparse
import string
import json
@@ -32,6 +33,8 @@ import exceptions
from htmlcleanup import conditionalRemoveEntities, removeAllEntities
from configurable import Configurable, re_compile
Chapter = namedtuple('Chapter', 'url title html origtitle toctitle new')
SPACE_REPLACE=u'\s'
SPLIT_META=u'\,'
@@ -412,7 +415,7 @@ class Story(Configurable):
except:
self.metadata = {'version':'4.4'}
self.in_ex_cludes = {}
self.chapters = [] # chapters will be tuples of (title,html)
self.chapters = [] # chapters will be namedtuple of Chapter(url,title,html,etc)
self.imgurls = []
self.imgtuples = []
@@ -442,7 +445,7 @@ class Story(Configurable):
self.in_ex_cludes[ie] = set_in_ex_clude(ies)
def join_list(self, key, vallist):
return self.getConfig("join_string_"+key,u", ").replace(SPACE_REPLACE,' ').join(map(unicode, vallist))
return self.getConfig("join_string_"+key,u", ").replace(SPACE_REPLACE,' ').join(map(unicode, [ x for x in vallist if x is not None ]))
def setMetadata(self, key, value, condremoveentities=True):
@@ -465,7 +468,7 @@ class Story(Configurable):
if key == 'dateUpdated' and value:
# Last Update tags for Bill.
self.addToList('lastupdate',value.strftime("Last Update Year/Month: %Y/%m"))
self.addToList('lastupdate',value.strftime("Last Update Year/Month: %Y/%m"),clear=True)
self.addToList('lastupdate',value.strftime("Last Update: %Y/%m/%d"))
@@ -757,8 +760,12 @@ class Story(Configurable):
# includelist prevents infinite recursion of include_in_'s
if self.hasConfig("include_in_"+listname) and listname not in includelist:
for k in self.getConfigList("include_in_"+listname):
ldorepl = doreplacements
if k.endswith('.NOREPL'):
k = k[:-len('.NOREPL')]
ldorepl = False
retlist.extend(self.getList(k,removeallentities=False,
doreplacements=doreplacements,includelist=includelist+[listname]))
doreplacements=ldorepl,includelist=includelist+[listname]))
else:
if not self.isList(listname):
@@ -813,7 +820,16 @@ class Story(Configurable):
# metadata all go into dc:subject tags, but only if they are configured.
for (name,value) in self.getAllMetadata(removeallentities=removeallentities,keeplists=True).iteritems():
if name in tags_list:
if name+'.SPLIT' in tags_list:
flist=[]
if isinstance(value,list):
for tag in value:
flist.extend(tag.split(','))
else:
flist.extend(value)
for tag in flist:
subjectset.add(tag)
elif name in tags_list:
if isinstance(value,list):
for tag in value:
subjectset.add(tag)
@@ -827,24 +843,63 @@ class Story(Configurable):
return list(subjectset | set(self.getConfigList("extratags")))
def addChapter(self, url, title, html):
def addChapter(self, url, title, html, newchap=False):
# logger.debug("addChapter(%s,%s)"%(url,newchap))
if self.getConfig('strip_chapter_numbers') and \
self.getConfig('chapter_title_strip_pattern'):
title = re.sub(self.getConfig('chapter_title_strip_pattern'),"",title)
self.chapters.append( (url,title,html) )
self.chapters.append( Chapter(url,title,html,title,title,newchap) )
def getChapters(self,fortoc=False):
"Chapters will be tuples of (title,html)"
"Chapters will be Chapter namedtuples"
retval = []
## only add numbers if more than one chapter.
if len(self.chapters) > 1 and \
(self.getConfig('add_chapter_numbers') == "true" \
or (self.getConfig('add_chapter_numbers') == "toconly" and fortoc)) \
and self.getConfig('chapter_title_add_pattern'):
for index, (url,title,html) in enumerate(self.chapters):
retval.append( (url,
string.Template(self.getConfig('chapter_title_add_pattern')).substitute({'index':index+1,'title':title}),
html) )
## only add numbers if more than one chapter. Ditto (new) marks.
if len(self.chapters) > 1:
addnums = ( self.getConfig('add_chapter_numbers') == "true"
or (self.getConfig('add_chapter_numbers') == "toconly" and fortoc) )
marknew = self.getConfig('mark_new_chapters')=='true'
defpattern = self.getConfig('chapter_title_def_pattern','${title}') # default val in case of missing defaults.ini
if addnums and marknew:
pattern = self.getConfig('chapter_title_add_pattern')
newpattern = self.getConfig('chapter_title_addnew_pattern')
elif addnums:
pattern = self.getConfig('chapter_title_add_pattern')
newpattern = pattern
elif marknew:
pattern = defpattern
newpattern = self.getConfig('chapter_title_new_pattern')
else:
pattern = defpattern
newpattern = pattern
if self.getConfig('add_chapter_numbers') in ["true","toconly"]:
tocpattern = self.getConfig('chapter_title_add_pattern')
else:
tocpattern = defpattern
# logger.debug("Patterns: (%s)(%s)"%(pattern,newpattern))
templ = string.Template(pattern)
newtempl = string.Template(newpattern)
toctempl = string.Template(tocpattern)
for index, chap in enumerate(self.chapters):
if chap.new:
usetempl = newtempl
else:
usetempl = templ
# logger.debug("chap.url, chap.new: (%s)(%s)"%(chap.url,chap.new))
retval.append( Chapter(chap.url,
# 'new'
usetempl.substitute({'index':index+1,'title':chap.title}),
chap.html,
# 'orig'
templ.substitute({'index':index+1,'title':chap.title}),
# 'toc'
toctempl.substitute({'index':index+1,'title':chap.title}),
chap.new) )
else:
retval = self.chapters
@@ -911,6 +966,7 @@ class Story(Configurable):
#print("\n===========\nparsedUrl.path:%s\ntoppath:%s\nimgurl:%s\n\n"%(parsedUrl.path,toppath,imgurl))
# apply coverexclusion to explicit covers, too. Primarily for ffnet imageu.
#print("[[[[[\n\n %s %s \n\n]]]]]]]"%(imgurl,coverexclusion))
if cover and coverexclusion and re.search(coverexclusion,imgurl):
return (None,None)
+4 -4
View File
@@ -148,12 +148,12 @@ class BaseStoryWriter(Configurable):
self._write(out,START.substitute(self.story.getAllMetadata()))
for index, (url,title,html) in enumerate(self.story.getChapters(fortoc=True)):
if html:
self._write(out,ENTRY.substitute({'chapter':title,
for index, chap in enumerate(self.story.getChapters(fortoc=True)):
if chap.html:
self._write(out,ENTRY.substitute({'chapter':chap.title,
'number':index+1,
'index':"%04d"%(index+1),
'url':url}))
'url':chap.url}))
self._write(out,END.substitute(self.story.getAllMetadata()))
+19 -13
View File
@@ -28,7 +28,7 @@ import re
from xml.dom.minidom import parse, parseString, getDOMImplementation
from base_writer import *
from ..htmlcleanup import stripHTML
from ..htmlcleanup import stripHTML,removeEntities
logger = logging.getLogger(__name__)
@@ -133,6 +133,10 @@ ${value}<br />
<head>
<title>${chapter}</title>
<link href="stylesheet.css" type="text/css" charset="UTF-8" rel="stylesheet"/>
<meta name="chapterurl" content="${url}"></meta>
<meta name="chapterorigtitle" content="${origchapter}"></meta>
<meta name="chaptertoctitle" content="${tocchapter}"></meta>
<meta name="chaptertitle" content="${chapter}"></meta>
</head>
<body>
<h3>${chapter}</h3>
@@ -502,13 +506,13 @@ div { margin: 0pt; padding: 0pt; }
items.append(("log_page","OEBPS/log_page.xhtml","application/xhtml+xml","Update Log"))
itemrefs.append("log_page")
for index, (url,title,html) in enumerate(self.story.getChapters(fortoc=True)):
if html:
for index, chap in enumerate(self.story.getChapters(fortoc=True)):
if chap.html:
i=index+1
items.append(("file%04d"%i,
"OEBPS/file%04d.xhtml"%i,
"application/xhtml+xml",
title))
chap.title))
itemrefs.append("file%04d"%i)
manifest = contentdom.createElement("manifest")
@@ -650,19 +654,21 @@ div { margin: 0pt; padding: 0pt; }
else:
CHAPTER_END = self.EPUB_CHAPTER_END
for index, (url,title,html) in enumerate(self.story.getChapters()):
if html:
logger.debug('Writing chapter text for: %s' % title)
vals={'url':url, 'chapter':title, 'index':"%04d"%(index+1), 'number':index+1}
fullhtml = CHAPTER_START.substitute(vals) + html + CHAPTER_END.substitute(vals)
for index, chap in enumerate(self.story.getChapters()): # (url,title,html)
if chap.html:
logger.debug('Writing chapter text for: %s' % chap.title)
vals={'url':removeEntities(chap.url),
'chapter':chap.title,
'origchapter':chap.origtitle,
'tocchapter':chap.toctitle,
'index':"%04d"%(index+1),
'number':index+1}
fullhtml = CHAPTER_START.substitute(vals) + \
chap.html + CHAPTER_END.substitute(vals)
# 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
# (200k+)
#fullhtml = fullhtml.replace('</p>','</p>\n').replace('<br />','<br />\n')
# The replaces above added tons of extra newlines
# during *each* epub update. The regexp version adds
# only one and removes any extra.
fullhtml = re.sub(r'(</p>|<br ?/>)\n*',r'\1\n',fullhtml)
outputepub.writestr("OEBPS/file%04d.xhtml"%(index+1),fullhtml.encode('utf-8'))
+5 -5
View File
@@ -128,12 +128,12 @@ ${output_css}
else:
CHAPTER_END = self.HTML_CHAPTER_END
for index, (url,title,html) in enumerate(self.story.getChapters()):
if html:
logging.debug('Writing chapter text for: %s' % title)
vals={'url':url, 'chapter':title, 'index':"%04d"%(index+1), 'number':index+1}
for index, chap in enumerate(self.story.getChapters()):
if chap.html:
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,html)
self._write(out,chap.html)
self._write(out,CHAPTER_END.substitute(vals))
self._write(out,FILE_END.substitute(self.story.getAllMetadata()))
+5 -5
View File
@@ -161,11 +161,11 @@ ${value}<br />
else:
CHAPTER_END = self.MOBI_CHAPTER_END
for index, (url,title,html) in enumerate(self.story.getChapters()):
if html:
logger.debug('Writing chapter text for: %s' % title)
vals={'url':url, 'chapter':title, 'index':"%04d"%(index+1), 'number':index+1}
fullhtml = CHAPTER_START.substitute(vals) + html + CHAPTER_END.substitute(vals)
for index, chap in enumerate(self.story.getChapters()):
if chap.html:
logger.debug('Writing chapter text for: %s' % chap.title)
vals={'url':chap.url, 'chapter':chap.title, 'index':"%04d"%(index+1), 'number':index+1}
fullhtml = CHAPTER_START.substitute(vals) + chap.html + CHAPTER_END.substitute(vals)
# 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
+5 -5
View File
@@ -154,12 +154,12 @@ End file.
else:
CHAPTER_END = self.TEXT_CHAPTER_END
for index, (url, title,html) in enumerate(self.story.getChapters()):
if html:
logging.debug('Writing chapter text for: %s' % title)
vals={'url':url, 'chapter':title, 'index':"%04d"%(index+1), 'number':index+1}
for index, chap in enumerate(self.story.getChapters()):
if chap.html:
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,self.lineends(self.wraplines(removeAllEntities(CHAPTER_START.substitute(vals)))))
self._write(out,self.lineends(html2text(html,wrap_width=self.wrap_width)))
self._write(out,self.lineends(html2text(chap.html,wrap_width=self.wrap_width)))
self._write(out,self.lineends(self.wraplines(removeAllEntities(CHAPTER_END.substitute(vals)))))
self._write(out,self.lineends(self.wraplines(FILE_END.substitute(self.story.getAllMetadata()))))
+2 -2
View File
@@ -1,6 +1,6 @@
# ffd-retief-hrd fanfictiondownloader fanficfare
# ffd-retief-hrd fanficfare
application: fanficfare
version: 2-2-10
version: 2-2-10a
runtime: python27
api_version: 1
threadsafe: true
+2
View File
@@ -41,6 +41,8 @@ class DownloadMeta(db.Model):
completed = db.BooleanProperty(default=False)
date = db.DateTimeProperty(auto_now_add=True)
version = db.StringProperty()
ch_begin = db.StringProperty()
ch_end = db.StringProperty()
# data_chunks is implicit from DownloadData def.
class DownloadData(db.Model):
+9 -1
View File
@@ -46,7 +46,15 @@
</p>
<h3>Changes:</h3>
<ul>
<li>Updates for mediaminer.org changes.</li>
<li>Add chapter limits with URL by giving chapter range.<br>
Examples:<br>
<ul>
<li>https://www.fanfiction.net/s/2565609/1/[4] <i>Chapter 4 only</i></li>
<li>https://www.fanfiction.net/s/2565609/1/[6-10] <i>Chapters 6, 7, 8, 9 &amp; 10 only</i></li>
<li>https://www.fanfiction.net/s/2565609/1/[-10] <i>Chapters 1-10 only</i></li>
<li>https://www.fanfiction.net/s/2565609/1/[150-] <i>Chapters 150 and up only</i></li>
</ul>
</li>
</ul>
<p>
Questions? Check out our
+14 -1
View File
@@ -62,7 +62,7 @@ class UserConfigServer(webapp2.RequestHandler):
def getUserConfig(self,user,url,fileformat):
configuration = Configuration(adapters.getConfigSectionFor(url),fileformat)
configuration = Configuration(adapters.getConfigSectionsFor(url),fileformat)
logging.debug('reading defaults.ini config file')
configuration.read('fanficfare/defaults.ini')
@@ -366,6 +366,16 @@ class FanfictionDownloader(UserConfigServer):
self.redirect('/')
return
# Allow chapter range with URL.
# test1.com?sid=5[4-6]
mc = re.match(r"^(?P<url>.*?)(?:\[(?P<begin>\d+)?(?P<comma>[,-])?(?P<end>\d+)?\])?$",url)
#print("url:(%s) begin:(%s) end:(%s)"%(mc.group('url'),mc.group('begin'),mc.group('end')))
url = mc.group('url')
ch_begin = mc.group('begin')
ch_end = mc.group('end')
if ch_begin and not mc.group('comma'):
ch_end = ch_begin
logging.info("Queuing Download: %s" % url)
login = self.request.get('login')
password = self.request.get('password')
@@ -408,6 +418,8 @@ class FanfictionDownloader(UserConfigServer):
download.title = story.getMetadata('title')
download.author = story.getMetadata('author')
download.url = story.getMetadata('storyUrl')
download.ch_begin = ch_begin
download.ch_end = ch_end
download.put()
taskqueue.add(url='/fdowntask',
@@ -490,6 +502,7 @@ class FanfictionDownloaderTask(UserConfigServer):
try:
configuration = self.getUserConfig(user,url,format)
adapter = adapters.getAdapter(configuration,url)
adapter.setChaptersRange(download.ch_begin,download.ch_end)
logging.info('Created an adapter: %s' % adapter)