mirror of
https://github.com/wassname/FanFicFare.git
synced 2026-09-09 11:14:08 +08:00
Merge branch 'master' into masseffect2in
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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=>[ ]*(/|&|&)[ ]*=>/ 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
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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()))
|
||||
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 & 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
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user