Compare commits

...
11 Commits
16 changed files with 1598 additions and 1409 deletions
+1 -1
View File
@@ -42,7 +42,7 @@ class FanFicFareBase(InterfaceActionBase):
description = _('UI plugin to download FanFiction stories from various sites.')
supported_platforms = ['windows', 'osx', 'linux']
author = 'Jim Miller'
version = (2, 2, 9)
version = (2, 2, 10)
minimum_calibre_version = (1, 48, 0)
#: This field defines the GUI plugin class that contains all the code
+23 -4
View File
@@ -259,6 +259,7 @@ class ConfigWidget(QWidget):
prefs['checkforseriesurlid'] = self.basic_tab.checkforseriesurlid.isChecked()
prefs['checkforurlchange'] = self.basic_tab.checkforurlchange.isChecked()
prefs['injectseries'] = self.basic_tab.injectseries.isChecked()
prefs['matchtitleauth'] = self.basic_tab.matchtitleauth.isChecked()
prefs['smarten_punctuation'] = self.basic_tab.smarten_punctuation.isChecked()
prefs['reject_always'] = self.basic_tab.reject_always.isChecked()
@@ -323,6 +324,8 @@ class ConfigWidget(QWidget):
colsnewonly[col] = checkbox.isChecked()
prefs['std_cols_newonly'] = colsnewonly
prefs['set_author_url'] =self.std_columns_tab.set_author_url.isChecked()
# Custom Columns tab
# error column
prefs['errorcol'] = unicode(convert_qvariant(self.cust_columns_tab.errorcol.itemData(self.cust_columns_tab.errorcol.currentIndex())))
@@ -346,10 +349,10 @@ class ConfigWidget(QWidget):
prefs['allow_custcol_from_ini'] = self.cust_columns_tab.allow_custcol_from_ini.isChecked()
prefs['imapserver'] = unicode(self.imap_tab.imapserver.text())
prefs['imapuser'] = unicode(self.imap_tab.imapuser.text())
prefs['imappass'] = unicode(self.imap_tab.imappass.text())
prefs['imapfolder'] = unicode(self.imap_tab.imapfolder.text())
prefs['imapserver'] = unicode(self.imap_tab.imapserver.text()).strip()
prefs['imapuser'] = unicode(self.imap_tab.imapuser.text()).strip()
prefs['imappass'] = unicode(self.imap_tab.imappass.text()).strip()
prefs['imapfolder'] = unicode(self.imap_tab.imapfolder.text()).strip()
prefs['imapmarkread'] = self.imap_tab.imapmarkread.isChecked()
prefs['imapsessionpass'] = self.imap_tab.imapsessionpass.isChecked()
prefs['auto_reject_from_email'] = self.imap_tab.auto_reject_from_email.isChecked()
@@ -519,6 +522,11 @@ class BasicTab(QWidget):
self.injectseries.setChecked(prefs['injectseries'])
self.l.addWidget(self.injectseries)
self.matchtitleauth = QCheckBox(_("Search by Title/Author(s) for If Story Already Exists?"),self)
self.matchtitleauth.setToolTip(_("When checking <i>If Story Already Exists</i> FanFicFare will first match by URL Identifier. But if not found, it can also search existing books by Title and Author(s)."))
self.matchtitleauth.setChecked(prefs['matchtitleauth'])
self.l.addWidget(self.matchtitleauth)
rej_gb = groupbox = QGroupBox(_("Reject List"))
self.l = QVBoxLayout()
groupbox.setLayout(self.l)
@@ -1311,6 +1319,17 @@ class StandardColumnsTab(QWidget):
horz.addWidget(newonlycheck)
self.l.addLayout(horz)
self.l.addSpacing(5)
label = QLabel(_("Other Standard Column Options"))
label.setWordWrap(True)
self.l.addWidget(label)
self.l.addSpacing(5)
self.set_author_url = QCheckBox(_('Set Calibre Author URL'),self)
self.set_author_url.setToolTip(_("Set Calibre Author URL to Author's URL on story site."))
self.set_author_url.setChecked(prefs['set_author_url'])
self.l.addWidget(self.set_author_url)
self.l.insertStretch(-1)
+2 -2
View File
@@ -1074,7 +1074,7 @@ class FanFicFarePlugin(InterfaceAction):
# try to find by identifier url or uri first.
identicalbooks = self.do_id_search(url)
# print("identicalbooks:%s"%identicalbooks)
if len(identicalbooks) < 1:
if len(identicalbooks) < 1 and prefs['matchtitleauth']:
# find dups
authlist = story.getList("author", removeallentities=True)
mi = MetaInformation(story.getMetadata("title", removeallentities=True),
@@ -1843,7 +1843,7 @@ class FanFicFarePlugin(InterfaceAction):
# set author link if found. All current adapters have authorUrl, except anonymous on AO3.
# Moved down so author's already in the DB.
if 'authorUrl' in book['all_metadata']:
if 'authorUrl' in book['all_metadata'] and prefs['set_author_url']:
authurls = book['all_metadata']['authorUrl'].split(", ")
authorlist = [ a.replace('&',';') for a in book['author'] ]
authorids = db.new_api.get_item_ids('authors',authorlist)
+2
View File
@@ -76,6 +76,7 @@ default_prefs['lookforurlinhtml'] = False
default_prefs['checkforseriesurlid'] = True
default_prefs['checkforurlchange'] = True
default_prefs['injectseries'] = False
default_prefs['matchtitleauth'] = True
default_prefs['smarten_punctuation'] = False
default_prefs['show_est_time'] = False
@@ -105,6 +106,7 @@ default_prefs['custom_cols_newonly'] = {}
default_prefs['allow_custcol_from_ini'] = True
default_prefs['std_cols_newonly'] = {}
default_prefs['set_author_url'] = True
default_prefs['imapserver'] = ''
default_prefs['imapuser'] = ''
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+34 -26
View File
@@ -22,7 +22,6 @@ import re
import urllib
import urllib2
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
@@ -42,7 +41,12 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query correct
m = re.match(self.getSiteURLPattern(),url)
if m:
self.story.setMetadata('storyId',m.group('id'))
if m.group('id'):
self.story.setMetadata('storyId',m.group('id'))
elif m.group('id2'):
self.story.setMetadata('storyId',m.group('id2'))
elif m.group('id3'):
self.story.setMetadata('storyId',m.group('id2'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/fanfic/view_st.php/'+self.story.getMetadata('storyId'))
@@ -62,8 +66,17 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
def getSiteURLPattern(self):
## http://www.mediaminer.org/fanfic/view_st.php/76882
## http://www.mediaminer.org/fanfic/view_ch.php/167618/594087#fic_c
## http://www.mediaminer.org/fanfic/view_ch.php?submit=View+Chapter&id=105816&cid=357151
## http://www.mediaminer.org/fanfic/view_ch.php?cid=612153&submit=View+Chapter&id=171668
return re.escape("http://"+self.getSiteDomain())+\
"/fanfic/view_(st|ch)\.php/"+r"(?P<id>\d+)(/\d+(#fic_c)?)?$"
r"/fanfic/view_(st|ch)\.php"+\
r"(/(?P<id>\d+)(/\d+(#fic_c)?)?/?|"+\
r"\?((submit=View(\+| )Chapter|id=(?P<id2>\d+)|cid=\d+)&?)+)"
# Override stripURLParameters so the id parameter won't get stripped
@classmethod
def stripURLParameters(cls, url):
return url
def extractChapterUrlsAndMetadata(self):
@@ -71,7 +84,7 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
logger.debug("URL: "+url)
try:
data = self._fetchUrl(url)
data = self._fetchUrl(url+'/') # trailing / gets 'chapter list' page even for one-shots.
except urllib2.HTTPError, e:
if e.code == 404:
raise exceptions.StoryDoesNotExist(self.url)
@@ -79,7 +92,7 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
raise e
# use BeautifulSoup HTML parser to make everything easier to find.
soup = bs.BeautifulSoup(data)
soup = self.make_soup(data)
# [ A - All Readers ], strip '[' ']'
## Above title because we remove the smtxt font to get title.
@@ -106,18 +119,12 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
title = soup.find('td',{'class':'ffh'})
for font in title.findAll('font'):
font.extract() # removes 'font' tags from inside the td.
if title.has_key('colspan'):
if title.has_attr('colspan'):
titlet = stripHTML(title)
else:
## No colspan, it's part chapter title--even if it's a one-shot.
titlet = ':'.join(stripHTML(title).split(':')[:-1]) # strip trailing 'Chapter X' or chapter title
self.story.setMetadata('title',titlet)
## The story title is difficult to reliably parse from the
## story pages. Getting it from the author page is, but costs
## another fetch.
# authsoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')))
# titlea = authsoup.find('a',{'href':'/fanfic/view_st.php/'+self.story.getMetadata('storyId')})
# self.story.setMetadata('title',titlea.text)
# save date from first for later.
firstdate=None
@@ -137,7 +144,9 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
# save date from first for later.
if not firstdate:
firstdate = m.group(3)
self.chapterUrls.append((chapter,'http://'+self.host+'/fanfic/view_ch.php/'+self.story.getMetadata('storyId')+'/'+option['value']))
# http://www.mediaminer.org/fanfic/view_ch.php?cid=376587&submit=View+Chapter&id=105816
# self.chapterUrls.append((chapter,'http://'+self.host+'/fanfic/view_ch.php/'+self.story.getMetadata('storyId')+'/'+option['value']))
self.chapterUrls.append((chapter,'http://'+self.host+'/fanfic/view_ch.php?submit=View Chapter&id='+self.story.getMetadata('storyId')+'&cid='+option['value']))
self.story.setMetadata('numChapters',len(self.chapterUrls))
# category
@@ -193,38 +202,37 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
logger.debug('Getting chapter text from: %s' % url)
data=self._fetchUrl(url)
soup = bs.BeautifulStoneSoup(data,
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
soup = self.make_soup(data)
anchor = soup.find('a',{'name':'fic_c'})
header = soup.find('div',{'class':'post-meta clearfix '})
# print("data:%s"%data)
if None == anchor:
chapter=self.make_soup('<div class="story"></div>').find('div')
if None == header:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
## find divs with align=left, those are paragraphs in newer stories.
divlist = anchor.findAllNext('div',{'align':'left'})
divlist = header.findAllNext('div',{'align':'left'})
if divlist:
for div in divlist:
div.name='p' # convert to <p> mediaminer uses div with
# a margin for paragraphs.
anchor.append(div) # cheat! stuff all the content
# divs into anchor just as a
# holder.
chapter.append(div)
del div['style']
del div['align']
anchor.name='div'
return self.utf8FromSoup(url,anchor)
return self.utf8FromSoup(url,chapter)
else:
logger.debug('Using kludgey text find for older mediaminer story.')
## Some older mediaminer stories are unparsable with BeautifulSoup.
## Really nasty formatting. Sooo... Cheat! Parse it ourselves a bit first.
## Story stuff falls between:
data = "<div id='HERE'>" + data[data.find('<a name="fic_c">'):] +"</div>"
soup = bs.BeautifulStoneSoup(data,
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
data = "<div id='HERE'>" + data[data.find('<div class="adWrap">'):data.find('<div class="addthis_sharing_toolbox">')] +"</div>"
soup = self.make_soup(data)
for tag in soup.findAll('td',{'class':'ffh'}) + \
soup.findAll('div',{'class':'acl'}) + \
soup.findAll('div',{'class':'adWrap'}) + \
soup.findAll('div',{'class':'footer smtxt'}) + \
soup.findAll('table',{'class':'tbbrdr'}):
tag.extract() # remove tag from soup.
+24 -20
View File
@@ -22,6 +22,9 @@ import re
import urllib2 as u2
import urlparse
import logging
logger = logging.getLogger(__name__)
from BeautifulSoup import BeautifulSoup
from gziphttp import GZipProcessor
@@ -82,17 +85,17 @@ def get_urls_from_html(data,url=None,configuration=None,normalize=False,restrict
soup = BeautifulSoup(data)
if restrictsearch:
soup = soup.find(*restrictsearch)
#print("restrict search:%s"%soup)
#logger.debug("restrict search:%s"%soup)
for a in soup.findAll('a'):
if a.has_key('href'):
#print("a['href']:%s"%a['href'])
#logger.debug("a['href']:%s"%a['href'])
href = form_url(url,a['href'])
#print("1 urlhref:%s"%href)
#logger.debug("1 urlhref:%s"%href)
# this (should) catch normal story links, some javascript
# 'are you old enough' links, and 'Report This' links.
if 'story.php' in a['href']:
#print("trying:%s"%a['href'])
#logger.debug("trying:%s"%a['href'])
m = re.search(r"(?P<sid>(view)?story\.php\?(sid|psid|no|story|stid)=\d+)",a['href'])
if m != None:
href = form_url(a['href'] if '//' in a['href'] else url,
@@ -100,15 +103,15 @@ def get_urls_from_html(data,url=None,configuration=None,normalize=False,restrict
try:
href = href.replace('&index=1','')
#print("2 urlhref:%s"%href)
#logger.debug("2 urlhref:%s"%href)
adapter = adapters.getAdapter(configuration,href)
#print("found adapter")
#logger.debug("found adapter")
if adapter.story.getMetadata('storyUrl') not in urls:
urls[adapter.story.getMetadata('storyUrl')] = [href]
else:
urls[adapter.story.getMetadata('storyUrl')].append(href)
except Exception, e:
#print e
#logger.debug e
pass
# Simply return the longest URL with the assumption that it contains the
@@ -173,7 +176,8 @@ def form_url(parenturl,url):
return returl
def get_urls_from_imap(srv,user,passwd,folder,markread=True):
logger.debug("get_urls_from_imap srv:(%s)"%srv)
mail = imaplib.IMAP4_SSL(srv)
mail.login(user, passwd)
mail.list()
@@ -182,8 +186,8 @@ def get_urls_from_imap(srv,user,passwd,folder,markread=True):
result, data = mail.uid('search', None, "UNSEEN")
#print("result:%s"%result)
#print("data:%s"%data)
#logger.debug("result:%s"%result)
#logger.debug("data:%s"%data)
urls=set()
#latest_email_uid = data[0].split()[-1]
@@ -191,8 +195,8 @@ def get_urls_from_imap(srv,user,passwd,folder,markread=True):
result, data = mail.uid('fetch', email_uid, '(BODY.PEEK[])') #RFC822
#print("result:%s"%result)
#print("data:%s"%data)
#logger.debug("result:%s"%result)
#logger.debug("data:%s"%data)
raw_email = data[0][1]
@@ -201,28 +205,28 @@ def get_urls_from_imap(srv,user,passwd,folder,markread=True):
email_message = email.message_from_string(raw_email)
#print "To:%s"%email_message['To']
#print "From:%s"%email_message['From']
#print "Subject:%s"%email_message['Subject']
#logger.debug "To:%s"%email_message['To']
#logger.debug "From:%s"%email_message['From']
#logger.debug "Subject:%s"%email_message['Subject']
# print("payload:%s"%email_message.get_payload())
# logger.debug("payload:%s"%email_message.get_payload())
urllist=[]
for part in email_message.walk():
try:
#print("part mime:%s"%part.get_content_type())
#logger.debug("part mime:%s"%part.get_content_type())
if part.get_content_type() == 'text/plain':
urllist.extend(get_urls_from_text(part.get_payload(decode=True)))
if part.get_content_type() == 'text/html':
urllist.extend(get_urls_from_html(part.get_payload(decode=True)))
except Exception as e:
print("Failed to read email content: %s"%e)
#print "urls:%s"%get_urls_from_text(get_first_text_block(email_message))
logger.error("Failed to read email content: %s"%e)
#logger.debug "urls:%s"%get_urls_from_text(get_first_text_block(email_message))
if urllist and markread:
#obj.store(data[0].replace(' ',','),'+FLAGS','\Seen')
r,d = mail.uid('store',email_uid,'+FLAGS','(\\SEEN)')
#print("seen result:%s->%s"%(email_uid,r))
#logger.debug("seen result:%s->%s"%(email_uid,r))
[ urls.add(x) for x in urllist ]
+1 -1
View File
@@ -25,7 +25,7 @@ setup(
# Versions should comply with PEP440. For a discussion on single-sourcing
# the version across setup.py and the project code, see
# https://packaging.python.org/en/latest/single_source_version.html
version="2.2.9",
version="2.2.10",
description='A tool for downloading fanfiction to eBook formats',
long_description=long_description,
+1 -1
View File
@@ -1,6 +1,6 @@
# ffd-retief-hrd fanfictiondownloader fanficfare
application: fanficfare
version: 2-2-9
version: 2-2-10
runtime: python27
api_version: 1
threadsafe: true
+2 -4
View File
@@ -46,9 +46,7 @@
</p>
<h3>Changes:</h3>
<ul>
<li>Update adapter_nhamagicalworldsus, make a Base eFiction adapter.</li>
<li>Default bulk_load true for all (eFiction Base) adapters.</li>
<li>Exclude doReplacements on add_genre_when_multi_category call to getList('category'). Prevents a possible infinite recursion.</li>
<li>Updates for mediaminer.org changes.</li>
</ul>
<p>
Questions? Check out our
@@ -58,7 +56,7 @@
If you have any problems with this application, please
report them in
the <a href="http://groups.google.com/group/fanfic-downloader">FanFicFare Google Group</a>. The
<a href="http://2-2-8.fanficfare.appspot.com">previous version
<a href="http://2-2-9.fanficfare.appspot.com">previous version
</a> is also available for you to use if necessary.
</p>
<div id='error'>