mirror of
https://github.com/wassname/FanFicFare.git
synced 2026-09-14 11:14:10 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cae4a74578 | ||
|
|
3839bba182 | ||
|
|
4338f4e550 | ||
|
|
883f9e22cc | ||
|
|
9b468da598 | ||
|
|
fa90a3f23c | ||
|
|
4f7fd93b64 | ||
|
|
d37dadf972 | ||
|
|
5dbbc2efe5 | ||
|
|
c442feeb26 | ||
|
|
17b0800242 |
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'] = ''
|
||||
|
||||
+214
-191
File diff suppressed because it is too large
Load Diff
+226
-203
File diff suppressed because it is too large
Load Diff
+217
-194
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+214
-191
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+214
-191
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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 ]
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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'>
|
||||
|
||||
Reference in New Issue
Block a user