Compare commits

...
22 Commits
Author SHA1 Message Date
Jim Miller 6dcfad0832 Fix fictionhunt.com example URLs. 2016-09-30 11:31:22 -05:00
Jim Miller b11e06e697 Bump versions--start bumping minor per version instead of micro. 2016-09-30 11:17:20 -05:00
Jim Miller 63515d1366 New version updating tool. 2016-09-30 11:16:54 -05:00
Jim Miller cfcba87c32 Update translations. 2016-09-30 11:15:04 -05:00
Jim Miller 709da65ea2 New site: fictionhunt.com 2016-09-21 19:59:08 -05:00
Jim Miller a21b555255 New site: fictionhunt.com 2016-09-21 19:07:50 -05:00
Jim Miller f25b46d268 Clean up file a little. 2016-09-21 19:04:22 -05:00
Jim Miller 6e2635a110 Need to provide 'more info' on failure with individual URL in CLI. 2016-09-21 17:50:17 -05:00
Jim Miller ab44185cfa Fix for webservice over-long error msgs causing problems, specific errmsg for unknown site. 2016-09-21 16:55:38 -05:00
Jim Miller c21ad04a9c Fix for calibre series w/injectseries always being applied when doing BG metadata. 2016-09-20 11:02:47 -05:00
Jim Miller 314baf038f Fix for poor HTML in some nfacommunity.com descs. 2016-09-20 09:31:08 -05:00
Jim Miller 860da1208e Improvements to CLI/defaults.ini suggested by MrTyton. 2016-09-19 17:41:08 -05:00
Jim Miller fc477034e5 Back out single-source version number changes. Not worth the head aches. 2016-09-18 21:16:38 -05:00
Jim Miller 51542d4e31 Add 'Reject w/o Confirm' option for Series Anthology Check. 2016-09-18 19:33:01 -05:00
Jim Miller fbabf92861 Remove prints. 2016-09-18 17:34:41 -05:00
Jim Miller a00becc3f8 Changes to CLI options for --imap and download from imap and list. 2016-09-18 14:52:15 -05:00
Jim Miller d503d4914b Adding CLI options --imap-list and --download-list 2016-09-15 16:27:58 -05:00
Jim Miller b6bd88c4b2 Adding CLI options --imap-list and --download-list 2016-09-15 12:11:20 -05:00
Jim Miller c4153ac59b Add support for Pillow (vs PIL) image lib in CLI. 2016-09-10 12:29:41 -05:00
Jim Miller dce0b5a335 Adding Update Existing Only option to Email URL Fetch. 2016-09-08 10:50:33 -05:00
Jim Miller d4291f2cd6 Adding Update Existing Only option to Email URL Fetch. 2016-09-08 10:50:01 -05:00
Jim Miller 43637fa67b Single sourcing version number in fanficfare/__init__.py 2016-08-30 21:50:35 -05:00
29 changed files with 5300 additions and 4682 deletions
+9 -8
View File
@@ -48,7 +48,7 @@ class FanFicFareBase(InterfaceActionBase):
description = _('UI plugin to download FanFiction stories from various sites.')
supported_platforms = ['windows', 'osx', 'linux']
author = 'Jim Miller'
version = (2, 3, 6)
version = (2, 4, 0)
minimum_calibre_version = (1, 48, 0)
#: This field defines the GUI plugin class that contains all the code
@@ -116,22 +116,23 @@ class FanFicFareBase(InterfaceActionBase):
from calibre_plugins.fanficfare_plugin.fanficfare.cli import main as fff_main
from calibre_plugins.fanficfare_plugin.prefs import PrefsFacade
from calibre.utils.config import prefs as calibre_prefs
from optparse import OptionParser
from optparse import OptionParser
parser = OptionParser('%prog --run-plugin '+self.name+' -- [options] <storyurl>')
parser.add_option('--library-path', '--with-library', default=None, help=_('Path to the calibre library. Default is to use the path stored in the settings.'))
# parser.add_option('--dont-notify-gui', default=False, action='store_true',
# help=_('Do not notify the running calibre GUI (if any) that the database has'
# ' changed. Use with care, as it can lead to database corruption!'))
pargs = [x for x in argv if x.startswith('--with-library') or x.startswith('--library-path')
or not x.startswith('-')]
opts, args = parser.parse_args(pargs)
fff_prefs = PrefsFacade(db(path=opts.library_path,
read_only=True))
fff_main(argv[1:],
parser=parser,
passed_defaultsini=StringIO(get_resources("fanficfare/defaults.ini")),
passed_personalini=StringIO(fff_prefs["personal.ini"]))
parser=parser,
passed_defaultsini=StringIO(get_resources("fanficfare/defaults.ini")),
passed_personalini=StringIO(fff_prefs["personal.ini"]),
)
+137 -119
View File
@@ -14,11 +14,13 @@ import traceback, copy, threading, re
from collections import OrderedDict
try:
from PyQt5 import QtWidgets as QtGui
from PyQt5.Qt import (QDialog, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
QLabel, QLineEdit, QFont, QWidget, QTextEdit, QComboBox,
QCheckBox, QPushButton, QTabWidget, QScrollArea,
QDialogButtonBox, QGroupBox, QButtonGroup, QRadioButton, Qt)
except ImportError as e:
from PyQt4 import QtGui
from PyQt4.Qt import (QDialog, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
QLabel, QLineEdit, QFont, QWidget, QTextEdit, QComboBox,
QCheckBox, QPushButton, QTabWidget, QScrollArea,
@@ -87,7 +89,7 @@ from calibre_plugins.fanficfare_plugin.prefs \
from calibre_plugins.fanficfare_plugin.dialogs \
import (UPDATE, UPDATEALWAYS, collision_order, save_collisions, RejectListDialog,
EditTextDialog, IniTextDialog, RejectUrlEntry)
from calibre_plugins.fanficfare_plugin.fanficfare.adapters \
import getSiteSections
@@ -112,7 +114,7 @@ class RejectURLList:
#print("rue.url:%s"%rue.url)
if rue.valid:
cache[rue.url] = rue
return cache
return cache
def _get_listcache(self):
if self.listcache == None:
@@ -124,7 +126,7 @@ class RejectURLList:
self.prefs['rejecturls'] = '\n'.join([x.to_line() for x in listcache.values()])
self.prefs.save_to_db()
self.listcache = None
def clear_cache(self):
self.listcache = None
@@ -133,7 +135,7 @@ class RejectURLList:
with self.sync_lock:
listcache = self._get_listcache()
return url in listcache
def get_note(self,url):
with self.sync_lock:
listcache = self._get_listcache()
@@ -159,7 +161,7 @@ class RejectURLList:
def add_text(self,rejecttext,addreasontext):
self.add(self._read_list_from_text(rejecttext,addreasontext).values())
def add(self,rejectlist,clear=False):
with self.sync_lock:
if clear:
@@ -172,7 +174,7 @@ class RejectURLList:
def get_list(self):
return self._get_listcache().values()
def get_reject_reasons(self):
return self.prefs['rejectreasons'].splitlines()
@@ -183,7 +185,7 @@ class ConfigWidget(QWidget):
def __init__(self, plugin_action):
QWidget.__init__(self)
self.plugin_action = plugin_action
self.l = QVBoxLayout()
self.setLayout(self.l)
@@ -192,7 +194,7 @@ class ConfigWidget(QWidget):
+_('List of Supported Sites')+'</a> -- <a href="'\
+'https://github.com/JimmXinu/FanFicFare/wiki/FAQs">'\
+_('FAQs')+'</a>')
label.setOpenExternalLinks(True)
self.l.addWidget(label)
@@ -204,13 +206,13 @@ class ConfigWidget(QWidget):
tab_widget = QTabWidget(self)
self.scroll_area.setWidget(tab_widget)
self.basic_tab = BasicTab(self, plugin_action)
tab_widget.addTab(self.basic_tab, _('Basic'))
self.personalini_tab = PersonalIniTab(self, plugin_action)
tab_widget.addTab(self.personalini_tab, 'personal.ini')
self.readinglist_tab = ReadingListTab(self, plugin_action)
tab_widget.addTab(self.readinglist_tab, 'Reading Lists')
if 'Reading List' not in plugin_action.gui.iactions:
@@ -258,6 +260,7 @@ class ConfigWidget(QWidget):
prefs['adddialogstaysontop'] = self.basic_tab.adddialogstaysontop.isChecked()
prefs['lookforurlinhtml'] = self.basic_tab.lookforurlinhtml.isChecked()
prefs['checkforseriesurlid'] = self.basic_tab.checkforseriesurlid.isChecked()
prefs['auto_reject_seriesurlid'] = self.basic_tab.auto_reject_seriesurlid.isChecked()
prefs['checkforurlchange'] = self.basic_tab.checkforurlchange.isChecked()
prefs['injectseries'] = self.basic_tab.injectseries.isChecked()
prefs['matchtitleauth'] = self.basic_tab.matchtitleauth.isChecked()
@@ -285,7 +288,7 @@ class ConfigWidget(QWidget):
prefs['personal.ini'] = get_resources('plugin-example.ini')
prefs['cal_cols_pass_in'] = self.personalini_tab.cal_cols_pass_in.isChecked()
# Covers tab
prefs['updatecalcover'] = prefs_save_options[unicode(self.calibrecover_tab.updatecalcover.currentText())]
# for backward compatibility:
@@ -306,7 +309,7 @@ class ConfigWidget(QWidget):
# Count Pages tab
countpagesstats = []
if self.countpages_tab.pagecount.isChecked():
countpagesstats.append('PageCount')
if self.countpages_tab.wordcount.isChecked():
@@ -317,10 +320,10 @@ class ConfigWidget(QWidget):
countpagesstats.append('FleschGrade')
if self.countpages_tab.gunningfog.isChecked():
countpagesstats.append('GunningFog')
prefs['countpagesstats'] = countpagesstats
prefs['wordcountmissing'] = self.countpages_tab.wordcount.isChecked() and self.countpages_tab.wordcountmissing.isChecked()
# Standard Columns tab
colsnewonly = {}
for (col,checkbox) in self.std_columns_tab.stdcol_newonlycheck.iteritems():
@@ -352,7 +355,7 @@ class ConfigWidget(QWidget):
for (col,checkbox) in self.cust_columns_tab.custcol_newonlycheck.iteritems():
colsnewonly[col] = checkbox.isChecked()
prefs['custom_cols_newonly'] = colsnewonly
prefs['allow_custcol_from_ini'] = self.cust_columns_tab.allow_custcol_from_ini.isChecked()
prefs['imapserver'] = unicode(self.imap_tab.imapserver.text()).strip()
@@ -362,8 +365,9 @@ class ConfigWidget(QWidget):
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()
prefs['update_existing_only_from_email'] = self.imap_tab.update_existing_only_from_email.isChecked()
prefs['download_from_email_immediately'] = self.imap_tab.download_from_email_immediately.isChecked()
prefs.save_to_db()
def edit_shortcuts(self):
@@ -380,7 +384,7 @@ class BasicTab(QWidget):
self.parent_dialog = parent_dialog
self.plugin_action = plugin_action
QWidget.__init__(self)
topl = QVBoxLayout()
self.setLayout(topl)
@@ -435,7 +439,7 @@ class BasicTab(QWidget):
self.updateepubcover.setToolTip(_("On each download, FanFicFare offers an option to update the book cover image <i>inside</i> the EPUB from the web site when the EPUB is updated.<br />This sets whether that will default to on or off."))
self.updateepubcover.setChecked(prefs['updateepubcover'])
horz.addWidget(self.updateepubcover)
self.bgmeta = QCheckBox(_('Default Background Metadata?'),self)
self.bgmeta.setToolTip(_("On each download, FanFicFare offers an option to Collect Metadata from sites in a Background process.<br />This returns control to you quicker while updating, but you won't be asked for username/passwords or if you are an adult--stories that need those will just fail.<br />Only available for Update/Overwrite of existing books in case URL given isn't canonical or matches to existing book by Title/Author."))
self.bgmeta.setChecked(prefs['bgmeta'])
@@ -451,7 +455,7 @@ class BasicTab(QWidget):
self.deleteotherforms.setToolTip(_('Check this to automatically delete all other ebook formats when updating an existing book.\nHandy if you have both a Nook(epub) and Kindle(mobi), for example.'))
self.deleteotherforms.setChecked(prefs['deleteotherforms'])
self.l.addWidget(self.deleteotherforms)
self.keeptags = QCheckBox(_('Keep Existing Tags when Updating Metadata?'),self)
self.keeptags.setToolTip(_("Existing tags will be kept and any new tags added.\n%(cmplt)s and %(inprog)s tags will be still be updated, if known.\n%(lul)s tags will be updated if %(lus)s in %(is)s.\n(If Tags is set to 'New Only' in the Standard Columns tab, this has no effect.)")%no_trans)
self.keeptags.setChecked(prefs['keeptags'])
@@ -468,10 +472,18 @@ class BasicTab(QWidget):
self.l.addWidget(self.suppresstitlesort)
self.checkforseriesurlid = QCheckBox(_("Check for existing Series Anthology books?"),self)
self.checkforseriesurlid.setToolTip(_("Check for existings Series Anthology books using each new story's series URL before downloading.\nOffer to skip downloading if a Series Anthology is found.\nDoesn't work when Collect Metadata in Background is selected."))
self.checkforseriesurlid.setToolTip(_("Check for existing Series Anthology books using each new story's series URL before downloading.\nOffer to skip downloading if a Series Anthology is found.\nDoesn't work when Collect Metadata in Background is selected."))
self.checkforseriesurlid.setChecked(prefs['checkforseriesurlid'])
self.l.addWidget(self.checkforseriesurlid)
self.auto_reject_seriesurlid = QCheckBox(_("Reject Without Confirmation?"),self)
self.auto_reject_seriesurlid.setToolTip(_("Automatically reject storys with existing Series Anthology books.\nOnly works if 'Check for existing Series Anthology books' is on.\nDoesn't work when Collect Metadata in Background is selected."))
self.auto_reject_seriesurlid.setChecked(prefs['auto_reject_seriesurlid'])
horz = QHBoxLayout()
horz.addItem(QtGui.QSpacerItem(20, 1))
horz.addWidget(self.auto_reject_seriesurlid)
self.l.addLayout(horz)
self.checkforurlchange = QCheckBox(_("Check for changed Story URL?"),self)
self.checkforurlchange.setToolTip(_("Warn you if an update will change the URL of an existing book.\nfanfiction.net URLs will change from http to https silently."))
self.checkforurlchange.setChecked(prefs['checkforurlchange'])
@@ -501,7 +513,7 @@ class BasicTab(QWidget):
self.smarten_punctuation.setChecked(prefs['smarten_punctuation'])
self.l.addWidget(self.smarten_punctuation)
tooltip = _("Calculate Word Counts using Calibre internal methods.\n"
"Many sites include Word Count, but many do not.\n"
"This will count the words in each book and include it as if it came from the site.")
@@ -518,7 +530,7 @@ class BasicTab(QWidget):
horz.addWidget(self.do_wordcount)
self.l.addLayout(horz)
self.autoconvert = QCheckBox(_("Automatically Convert new/update books?"),self)
self.autoconvert.setToolTip(_("Automatically call calibre's Convert for new/update books.\nConverts to the current output format as chosen in calibre's\nPreferences->Behavior settings."))
self.autoconvert.setChecked(prefs['autoconvert'])
@@ -570,12 +582,12 @@ class BasicTab(QWidget):
self.rejectlist.setToolTip(_("Edit list of URLs FanFicFare will automatically Reject."))
self.rejectlist.clicked.connect(self.show_rejectlist)
self.l.addWidget(self.rejectlist)
self.reject_urls = QPushButton(_('Add Reject URLs'), self)
self.reject_urls.setToolTip(_("Add additional URLs to Reject as text."))
self.reject_urls.clicked.connect(self.add_reject_urls)
self.l.addWidget(self.reject_urls)
self.reject_reasons = QPushButton(_('Edit Reject Reasons List'), self)
self.reject_reasons.setToolTip(_("Customize the Reasons presented when Rejecting URLs"))
self.reject_reasons.clicked.connect(self.show_reject_reasons)
@@ -598,13 +610,13 @@ class BasicTab(QWidget):
vertright.addWidget(gui_gb)
vertright.addWidget(misc_gb)
vertright.addWidget(rej_gb)
horz.addLayout(vertleft)
horz.addLayout(vertright)
topl.addLayout(horz)
topl.insertStretch(-1)
def set_collisions(self):
prev=self.collision.currentText()
self.collision.clear()
@@ -614,7 +626,7 @@ class BasicTab(QWidget):
i = self.collision.findText(prev)
if i > -1:
self.collision.setCurrentIndex(i)
def show_rejectlist(self):
d = RejectListDialog(self,
rejecturllist.get_list(),
@@ -623,12 +635,12 @@ class BasicTab(QWidget):
show_delete=False,
show_all_reasons=False)
d.exec_()
if d.result() != d.Accepted:
return
rejecturllist.add(d.get_reject_list(),clear=True)
def show_reject_reasons(self):
d = EditTextDialog(self,
prefs['rejectreasons'],
@@ -640,7 +652,7 @@ class BasicTab(QWidget):
d.exec_()
if d.result() == d.Accepted:
prefs['rejectreasons'] = d.get_plain_text()
def add_reject_urls(self):
d = EditTextDialog(self,
"http://example.com/story.php?sid=5,"+_("Reason why I rejected it")+"\nhttp://example.com/story.php?sid=6,"+_("Title by Author")+" - "+_("Reason why I rejected it"),
@@ -654,14 +666,14 @@ class BasicTab(QWidget):
d.exec_()
if d.result() == d.Accepted:
rejecturllist.add_text(d.get_plain_text(),d.get_reason_text())
class PersonalIniTab(QWidget):
def __init__(self, parent_dialog, plugin_action):
self.parent_dialog = parent_dialog
self.plugin_action = plugin_action
QWidget.__init__(self)
self.l = QVBoxLayout()
self.setLayout(self.l)
@@ -678,16 +690,16 @@ class PersonalIniTab(QWidget):
self.l.addWidget(groupbox)
horz = QHBoxLayout()
vert.addLayout(horz)
vert.addLayout(horz)
self.ini_button = QPushButton(_('Edit personal.ini'), self)
#self.ini_button.setToolTip(_("Edit personal.ini file."))
self.ini_button.clicked.connect(self.add_ini_button)
horz.addWidget(self.ini_button)
label = QLabel(_("FanFicFare now includes find, color coding, and error checking for personal.ini editing. Red generally indicates errors."))
label.setWordWrap(True)
horz.addWidget(label)
vert.addSpacing(5)
horz = QHBoxLayout()
@@ -696,18 +708,18 @@ class PersonalIniTab(QWidget):
#self.ini_button.setToolTip(_("Edit personal.ini file."))
self.ini_button.clicked.connect(self.safe_ini_button)
horz.addWidget(self.ini_button)
label = QLabel(_("View your personal.ini with usernames and passwords removed. For safely sharing your personal.ini settings with others."))
label.setWordWrap(True)
horz.addWidget(label)
self.l.addSpacing(5)
groupbox = QGroupBox(_("defaults.ini"))
horz = QHBoxLayout()
groupbox.setLayout(horz)
self.l.addWidget(groupbox)
view_label = _("View all of the plugin's configurable settings\nand their default settings.")
self.defaults = QPushButton(_('View Defaults')+' (plugin-defaults.ini)', self)
self.defaults.setToolTip(view_label)
@@ -717,7 +729,7 @@ class PersonalIniTab(QWidget):
label = QLabel(view_label)
label.setWordWrap(True)
horz.addWidget(label)
self.l.addSpacing(5)
groupbox = QGroupBox(_("Calibre Columns"))
@@ -732,13 +744,13 @@ class PersonalIniTab(QWidget):
self.cal_cols_pass_in.setToolTip(pass_label)
self.cal_cols_pass_in.setChecked(prefs['cal_cols_pass_in'])
horz.addWidget(self.cal_cols_pass_in)
label = QLabel(pass_label)
label.setWordWrap(True)
horz.addWidget(label)
vert.addSpacing(5)
horz = QHBoxLayout()
vert.addLayout(horz)
col_label = _("FanFicFare can pass the Calibre Columns into the download/update process.<br>This will show you the columns available by name.")
@@ -756,7 +768,7 @@ class PersonalIniTab(QWidget):
self.l.addWidget(label)
self.l.insertStretch(-1)
def show_defaults(self):
IniTextDialog(self,
get_resources('plugin-defaults.ini'),
@@ -766,10 +778,10 @@ class PersonalIniTab(QWidget):
use_find=True,
read_only=True,
save_size_name='fff:defaults.ini').exec_()
def safe_ini_button(self):
personalini = re.sub(r'((username|password) *[=:]).*$',r'\1XXXXXXXX',self.personalini,flags=re.MULTILINE)
d = EditTextDialog(self,
personalini,
icon=self.windowIcon(),
@@ -778,7 +790,7 @@ class PersonalIniTab(QWidget):
save_size_name='fff:safe personal.ini',
read_only=True)
d.exec_()
def add_ini_button(self):
d = IniTextDialog(self,
self.personalini,
@@ -790,13 +802,13 @@ class PersonalIniTab(QWidget):
d.exec_()
if d.result() == d.Accepted:
self.personalini = d.get_plain_text()
def show_showcalcols(self):
lines=[]#[('calibre_std_user_categories',_('User Categories'))]
for k,f in field_metadata.iteritems():
if f['name'] and k not in STD_COLS_SKIP: # only if it has a human readable name.
lines.append(('calibre_std_'+k,f['name']))
for k, column in self.plugin_action.gui.library_view.model().custom_columns.iteritems():
if k != prefs['savemetacol']:
# custom always have name.
@@ -811,14 +823,14 @@ class PersonalIniTab(QWidget):
label=_('Label (entry_name)'),
read_only=True,
save_size_name='fff:showcalcols').exec_()
class ReadingListTab(QWidget):
def __init__(self, parent_dialog, plugin_action):
self.parent_dialog = parent_dialog
self.plugin_action = plugin_action
QWidget.__init__(self)
self.l = QVBoxLayout()
self.setLayout(self.l)
@@ -827,21 +839,21 @@ class ReadingListTab(QWidget):
reading_lists = rl_plugin.get_list_names()
except KeyError:
reading_lists= []
label = QLabel(_('These settings provide integration with the %(rl)s Plugin. %(rl)s can automatically send to devices and change custom columns. You have to create and configure the lists in %(rl)s to be useful.')%no_trans)
label.setWordWrap(True)
self.l.addWidget(label)
self.l.addSpacing(5)
self.addtolists = QCheckBox(_('Add new/updated stories to "Send to Device" Reading List(s).'),self)
self.addtolists.setToolTip(_('Automatically add new/updated stories to these lists in the %(rl)s plugin.')%no_trans)
self.addtolists.setChecked(prefs['addtolists'])
self.l.addWidget(self.addtolists)
horz = QHBoxLayout()
label = QLabel(_('"Send to Device" Reading Lists'))
label.setToolTip(_("When enabled, new/updated stories will be automatically added to these lists."))
horz.addWidget(label)
horz.addWidget(label)
self.send_lists_box = EditWithComplete(self)
self.send_lists_box.setToolTip(_("When enabled, new/updated stories will be automatically added to these lists."))
self.send_lists_box.update_items_cache(reading_lists)
@@ -849,16 +861,16 @@ class ReadingListTab(QWidget):
horz.addWidget(self.send_lists_box)
self.send_lists_box.setCursorPosition(0)
self.l.addLayout(horz)
self.addtoreadlists = QCheckBox(_('Add new/updated stories to "To Read" Reading List(s).'),self)
self.addtoreadlists.setToolTip(_('Automatically add new/updated stories to these lists in the %(rl)s plugin.\nAlso offers menu option to remove stories from the "To Read" lists.')%no_trans)
self.addtoreadlists.setChecked(prefs['addtoreadlists'])
self.l.addWidget(self.addtoreadlists)
horz = QHBoxLayout()
label = QLabel(_('"To Read" Reading Lists'))
label.setToolTip(_("When enabled, new/updated stories will be automatically added to these lists."))
horz.addWidget(label)
horz.addWidget(label)
self.read_lists_box = EditWithComplete(self)
self.read_lists_box.setToolTip(_("When enabled, new/updated stories will be automatically added to these lists."))
self.read_lists_box.update_items_cache(reading_lists)
@@ -866,31 +878,31 @@ class ReadingListTab(QWidget):
horz.addWidget(self.read_lists_box)
self.read_lists_box.setCursorPosition(0)
self.l.addLayout(horz)
self.addtolistsonread = QCheckBox(_('Add stories back to "Send to Device" Reading List(s) when marked "Read".'),self)
self.addtolistsonread.setToolTip(_('Menu option to remove from "To Read" lists will also add stories back to "Send to Device" Reading List(s)'))
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):
def __init__(self, parent_dialog, plugin_action):
self.parent_dialog = parent_dialog
self.plugin_action = plugin_action
QWidget.__init__(self)
self.gencov_elements=[] ## used to disable/enable when gen
## cover is off/on. This is more
## about being a visual que than real
## necessary function.
topl = self.l = QVBoxLayout()
self.setLayout(self.l)
@@ -900,7 +912,7 @@ class CalibreCoverTab(QWidget):
except KeyError:
gc_settings= []
label = QLabel(_("The Calibre cover image for a downloaded book can come"
" from the story site(if EPUB and images are enabled), or"
" from either Calibre's built-in random cover generator or"
@@ -908,7 +920,7 @@ class CalibreCoverTab(QWidget):
label.setWordWrap(True)
self.l.addWidget(label)
self.l.addSpacing(5)
tooltip = _("Update Calibre book cover image from EPUB when Calibre metadata is updated.\n"
"Doesn't go looking for new images on 'Update Calibre Metadata Only'.\n"
"Cover in EPUB could be from site or previously injected into the EPUB.\n"
@@ -949,7 +961,7 @@ class CalibreCoverTab(QWidget):
# self.gencalcover.setCurrentIndex(self.gencalcover.findText(prefs_save_options[SAVE_YES]))
# else: # doesn't have own value, old value not set, NO.
# self.gencalcover.setCurrentIndex(self.gencalcover.findText(prefs_save_options[SAVE_NO]))
self.gencalcover.setToolTip(tooltip)
label.setBuddy(self.gencalcover)
horz.addWidget(self.gencalcover)
@@ -962,7 +974,7 @@ class CalibreCoverTab(QWidget):
self.gencov_gb = QGroupBox()
horz = QHBoxLayout()
self.gencov_gb.setLayout(horz)
self.plugin_gen_cover = QRadioButton(_('Plugin %(gc)s')%no_trans,self)
self.plugin_gen_cover.setToolTip(_("Use plugin to create covers. Additional settings are below."))
self.gencov_rdgrp.addButton(self.plugin_gen_cover)
@@ -1007,7 +1019,7 @@ class CalibreCoverTab(QWidget):
self.gencov_elements.append(self.gcp_gb)
self.gencov_rdgrp.buttonClicked.connect(self.endisable_elements)
label = QLabel(_('The %(gc)s plugin can create cover images for books using various metadata (including existing cover image). If you have %(gc)s installed, FanFicFare can run %(gc)s on new downloads and metadata updates. Pick a %(gc)s setting by site and/or one to use by Default.')%no_trans)
label.setWordWrap(True)
self.l.addWidget(label)
@@ -1021,7 +1033,7 @@ class CalibreCoverTab(QWidget):
self.sl = QVBoxLayout()
scrollcontent.setLayout(self.sl)
self.gc_dropdowns = {}
sitelist = getSiteSections()
@@ -1054,9 +1066,9 @@ class CalibreCoverTab(QWidget):
horz.addWidget(dropdown)
self.sl.addLayout(horz)
self.sl.insertStretch(-1)
self.allow_gc_from_ini = QCheckBox(_('Allow %(gcset)s from %(pini)s to override')%no_trans,self)
self.allow_gc_from_ini.setToolTip(_("The %(pini)s parameter %(gcset)s allows you to choose a %(gc)s setting based on metadata"
" rather than site, but it's much more complex.<br \>%(gcset)s is ignored when this is off.")%no_trans)
@@ -1070,7 +1082,7 @@ class CalibreCoverTab(QWidget):
"Clearing house function for setting elements of Calibre"
"Cover tab enabled/disabled depending on all factors."
## First, cover gen on/off
## First, cover gen on/off
for e in self.gencov_elements:
e.setEnabled(prefs_save_options[unicode(self.gencalcover.currentText())] != SAVE_NO)
@@ -1084,15 +1096,15 @@ class CalibreCoverTab(QWidget):
if not 'Generate Cover' in self.plugin_action.gui.iactions:
self.plugin_gen_cover.setEnabled(False)
self.gcp_gb.setEnabled(False)
class CountPagesTab(QWidget):
def __init__(self, parent_dialog, plugin_action):
self.parent_dialog = parent_dialog
self.plugin_action = plugin_action
QWidget.__init__(self)
self.l = QVBoxLayout()
self.setLayout(self.l)
@@ -1115,7 +1127,7 @@ class CountPagesTab(QWidget):
self.l.addWidget(self.pagecount)
horz = QHBoxLayout()
self.wordcount = QCheckBox('Word Count',self)
self.wordcount.setToolTip(tooltip+"\n"+_('Will overwrite word count from FanFicFare metadata if set to update the same custom column.'))
self.wordcount.setChecked('WordCount' in prefs['countpagesstats'])
@@ -1128,33 +1140,33 @@ class CountPagesTab(QWidget):
horz.addWidget(self.wordcountmissing)
self.wordcount.stateChanged.connect(lambda x : self.wordcountmissing.setEnabled(self.wordcount.isChecked()))
self.l.addLayout(horz)
self.fleschreading = QCheckBox('Flesch Reading Ease',self)
self.fleschreading.setToolTip(tooltip)
self.fleschreading.setChecked('FleschReading' in prefs['countpagesstats'])
self.l.addWidget(self.fleschreading)
self.fleschgrade = QCheckBox('Flesch-Kincaid Grade Level',self)
self.fleschgrade.setToolTip(tooltip)
self.fleschgrade.setChecked('FleschGrade' in prefs['countpagesstats'])
self.l.addWidget(self.fleschgrade)
self.gunningfog = QCheckBox('Gunning Fog Index',self)
self.gunningfog.setToolTip(tooltip)
self.gunningfog.setChecked('GunningFog' in prefs['countpagesstats'])
self.l.addWidget(self.gunningfog)
self.l.insertStretch(-1)
class OtherTab(QWidget):
def __init__(self, parent_dialog, plugin_action):
self.parent_dialog = parent_dialog
self.plugin_action = plugin_action
QWidget.__init__(self)
self.l = QVBoxLayout()
self.setLayout(self.l)
@@ -1162,7 +1174,7 @@ class OtherTab(QWidget):
label.setWordWrap(True)
self.l.addWidget(label)
self.l.addSpacing(5)
keyboard_shortcuts_button = QPushButton(_('Keyboard shortcuts...'), self)
keyboard_shortcuts_button.setToolTip(_('Edit the keyboard shortcuts associated with this plugin'))
keyboard_shortcuts_button.clicked.connect(parent_dialog.edit_shortcuts)
@@ -1172,14 +1184,14 @@ class OtherTab(QWidget):
reset_confirmation_button.setToolTip(_('Reset all show me again dialogs for the FanFicFare plugin'))
reset_confirmation_button.clicked.connect(self.reset_dialogs)
self.l.addWidget(reset_confirmation_button)
view_prefs_button = QPushButton(_('&View library preferences...'), self)
view_prefs_button.setToolTip(_('View data stored in the library database for this plugin'))
view_prefs_button.clicked.connect(self.view_prefs)
self.l.addWidget(view_prefs_button)
self.l.insertStretch(-1)
def reset_dialogs(self):
for key in dynamic.keys():
if key.startswith('fanfictiondownloader_') and key.endswith('_again') \
@@ -1189,7 +1201,7 @@ class OtherTab(QWidget):
_('Confirmation dialogs have all been reset'),
show=True,
show_copy_button=False)
def view_prefs(self):
d = PrefsViewerDialog(self.plugin_action.gui, PREFS_NAMESPACE)
d.exec_()
@@ -1271,7 +1283,7 @@ class CustomColumnsTab(QWidget):
self.parent_dialog = parent_dialog
self.plugin_action = plugin_action
QWidget.__init__(self)
custom_columns = self.plugin_action.gui.library_view.model().custom_columns
self.l = QVBoxLayout()
@@ -1281,7 +1293,7 @@ class CustomColumnsTab(QWidget):
label.setWordWrap(True)
self.l.addWidget(label)
self.l.addSpacing(5)
self.custcol_dropdowns = {}
self.custcol_newonlycheck = {}
@@ -1293,7 +1305,7 @@ class CustomColumnsTab(QWidget):
self.sl = QVBoxLayout()
scrollcontent.setLayout(self.sl)
for key, column in custom_columns.iteritems():
if column['datatype'] in permitted_values:
@@ -1323,9 +1335,9 @@ class CustomColumnsTab(QWidget):
if key in prefs['custom_cols_newonly']:
newonlycheck.setChecked(prefs['custom_cols_newonly'][key])
horz.addWidget(newonlycheck)
self.sl.addLayout(horz)
self.sl.insertStretch(-1)
self.l.addSpacing(5)
@@ -1333,11 +1345,11 @@ class CustomColumnsTab(QWidget):
self.allow_custcol_from_ini.setToolTip(_("The %(pini)s parameter %(ccset)s allows you to set custom columns to site specific values that aren't common to all sites.<br />%(ccset)s is ignored when this is off.")%no_trans)
self.allow_custcol_from_ini.setChecked(prefs['allow_custcol_from_ini'])
self.l.addWidget(self.allow_custcol_from_ini)
label = QLabel(_("Special column:"))
label.setWordWrap(True)
self.l.addWidget(label)
horz = QHBoxLayout()
label = QLabel(_("Update/Overwrite Error Column:"))
tooltip=_("When an update or overwrite of an existing story fails, record the reason in this column.\n(Text and Long Text columns only.)")
@@ -1352,7 +1364,7 @@ class CustomColumnsTab(QWidget):
self.errorcol.addItem(column['name'],key)
self.errorcol.setCurrentIndex(self.errorcol.findData(prefs['errorcol']))
horz.addWidget(self.errorcol)
self.save_all_errors = QCheckBox(_('Save All Errors'),self)
self.save_all_errors.setToolTip(_('If unchecked, these errors will not be saved:%s')%(
'\n'+
@@ -1360,9 +1372,9 @@ class CustomColumnsTab(QWidget):
_("Already contains %d chapters.").replace('%d','X')))))
self.save_all_errors.setChecked(prefs['save_all_errors'])
horz.addWidget(self.save_all_errors)
self.l.addLayout(horz)
horz = QHBoxLayout()
label = QLabel(_("Saved Metadata Column:"))
tooltip=_("If set, FanFicFare will save a copy of all its metadata in this column when the book is downloaded or updated.<br/>The metadata from this column can later be used to update custom columns without having to request the metadata from the server again.<br/>(Long Text columns only.)")
@@ -1379,7 +1391,7 @@ class CustomColumnsTab(QWidget):
label = QLabel('')
horz.addWidget(label) # empty spacer for alignment with error column line.
self.l.addLayout(horz)
#print("prefs['custom_cols'] %s"%prefs['custom_cols'])
@@ -1393,7 +1405,7 @@ class StandardColumnsTab(QWidget):
QWidget.__init__(self)
columns=OrderedDict()
columns["title"]=_("Title")
columns["authors"]=_("Author(s)")
columns["publisher"]=_("Publisher")
@@ -1412,7 +1424,7 @@ class StandardColumnsTab(QWidget):
label.setWordWrap(True)
self.l.addWidget(label)
self.l.addSpacing(5)
self.stdcol_newonlycheck = {}
for key, column in columns.iteritems():
@@ -1427,9 +1439,9 @@ class StandardColumnsTab(QWidget):
if key in prefs['std_cols_newonly']:
newonlycheck.setChecked(prefs['std_cols_newonly'][key])
horz.addWidget(newonlycheck)
self.l.addLayout(horz)
self.l.addSpacing(5)
label = QLabel(_("Other Standard Column Options"))
label.setWordWrap(True)
@@ -1451,7 +1463,7 @@ Default is a list of included titles only.'''))
self.anth_comments_newonly.setToolTip(_("Comments will only be set for New Anthologies, not updates.\nThat way comments you set manually are retained."))
self.anth_comments_newonly.setChecked(prefs['anth_comments_newonly'])
self.l.addWidget(self.anth_comments_newonly)
self.l.insertStretch(-1)
class ImapTab(QWidget):
@@ -1460,11 +1472,11 @@ class ImapTab(QWidget):
self.parent_dialog = parent_dialog
self.plugin_action = plugin_action
QWidget.__init__(self)
self.l = QGridLayout()
self.setLayout(self.l)
row=0
label = QLabel(_('These settings will allow FanFicFare to fetch story URLs from your email account. It will only look for story URLs in unread emails in the folder specified below.'))
label.setWordWrap(True)
self.l.addWidget(label,row,0,1,-1)
@@ -1479,21 +1491,21 @@ class ImapTab(QWidget):
self.imapserver.setText(prefs['imapserver'])
self.l.addWidget(self.imapserver,row,1)
row+=1
label = QLabel(_('IMAP User Name'))
tooltip = _("Name of IMAP user. Eg: yourname@gmail.com\nNote that Gmail accounts need to have IMAP enabled in Gmail Settings first.")
label.setToolTip(tooltip)
self.l.addWidget(label,row,0)
self.l.addWidget(label,row,0)
self.imapuser = QLineEdit(self)
self.imapuser.setToolTip(tooltip)
self.imapuser.setText(prefs['imapuser'])
self.l.addWidget(self.imapuser,row,1)
row+=1
label = QLabel(_('IMAP User Password'))
tooltip = _("IMAP password. If left empty, FanFicFare will ask you for your password when you use the feature.")
label.setToolTip(tooltip)
self.l.addWidget(label,row,0)
self.l.addWidget(label,row,0)
self.imappass = QLineEdit(self)
self.imappass.setToolTip(tooltip)
self.imappass.setEchoMode(QLineEdit.Password)
@@ -1506,35 +1518,41 @@ class ImapTab(QWidget):
self.imapsessionpass.setChecked(prefs['imapsessionpass'])
self.l.addWidget(self.imapsessionpass,row,0,1,-1)
row+=1
label = QLabel(_('IMAP Folder Name'))
tooltip = _("Name of IMAP folder to search for new emails. The folder (or label) has to already exist. Use INBOX for your default inbox.")
label.setToolTip(tooltip)
self.l.addWidget(label,row,0)
self.l.addWidget(label,row,0)
self.imapfolder = QLineEdit(self)
self.imapfolder.setToolTip(tooltip)
self.imapfolder.setText(prefs['imapfolder'])
self.l.addWidget(self.imapfolder,row,1)
row+=1
self.imapmarkread = QCheckBox(_('Mark Emails Read'),self)
self.imapmarkread.setToolTip(_('If checked, emails will be marked as having been read if they contain any story URLs.'))
self.imapmarkread.setChecked(prefs['imapmarkread'])
self.l.addWidget(self.imapmarkread,row,0,1,-1)
row+=1
self.auto_reject_from_email = QCheckBox(_('Discard URLs on Reject List'),self)
self.auto_reject_from_email.setToolTip(_('If checked, FanFicFare will silently discard story URLs from emails that are on your Reject URL List.<br>Otherwise they will appear and you will see the normal Reject URL dialog.<br>The Emails will still be marked Read if configured to.'))
self.auto_reject_from_email.setChecked(prefs['auto_reject_from_email'])
self.l.addWidget(self.auto_reject_from_email,row,0,1,-1)
row+=1
self.update_existing_only_from_email = QCheckBox(_('Update Existing Books Only'),self)
self.update_existing_only_from_email.setToolTip(_('If checked, FanFicFare will silently discard story URLs from emails that are not already in your library.<br>Otherwise all story URLs, new and existing, will be used.<br>The Emails will still be marked Read if configured to.'))
self.update_existing_only_from_email.setChecked(prefs['update_existing_only_from_email'])
self.l.addWidget(self.update_existing_only_from_email,row,0,1,-1)
row+=1
self.download_from_email_immediately = QCheckBox(_('Download from Email Immediately'),self)
self.download_from_email_immediately.setToolTip(_('If checked, FanFicFare will start downloading story URLs from emails immediately.<br>Otherwise the usual Download from URLs dialog will appear.'))
self.download_from_email_immediately.setChecked(prefs['download_from_email_immediately'])
self.l.addWidget(self.download_from_email_immediately,row,0,1,-1)
row+=1
label = QLabel(_("<b>It's safest if you create a separate email account that you use only "
"for your story update notices. FanFicFare and calibre cannot guarantee that "
"malicious code cannot get your email password once you've entered it. "
@@ -1543,5 +1561,5 @@ class ImapTab(QWidget):
self.l.addWidget(label,row,0,1,-1,Qt.AlignTop)
self.l.setRowStretch(row,1)
row+=1
+13 -3
View File
@@ -484,9 +484,16 @@ class FanFicFarePlugin(InterfaceAction):
reject_list=set()
if prefs['auto_reject_from_email']:
# need to normalize for reject list.
reject_list = set([x for x in url_list if rejecturllist.check(adapters.getNormalStoryURLSite(x)[0])])
reject_list = set([x for x in url_list if rejecturllist.check(adapters.getNormalStoryURL(x))])
url_list = url_list - reject_list
## feature for update-only - check url_list with
## self.do_id_search(url)
notupdate_list = set()
if prefs['update_existing_only_from_email']:
notupdate_list = set([x for x in url_list if not self.do_id_search(adapters.getNormalStoryURL(x))])
url_list = url_list - notupdate_list
self.gui.status_bar.show_message(_('No Valid Story URLs Found in Unread Emails.'),3000)
self.restore_cursor()
@@ -512,6 +519,8 @@ class FanFicFarePlugin(InterfaceAction):
msg = _('No Valid Story URLs Found in Unread Emails.')
if reject_list:
msg = msg + '<p>'+(_('(%d Story URLs Skipped, on Rejected URL List)')%len(reject_list))+'</p>'
if notupdate_list:
msg = msg + '<p>'+(_("(%d Story URLs Skipped, no Existing Book in Library)")%len(notupdate_list))+'</p>'
info_dialog(self.gui, _('Get Story URLs from Email'),
msg,
show=True,
@@ -1155,7 +1164,8 @@ class FanFicFarePlugin(InterfaceAction):
# try to find *series anthology* by *seriesUrl* identifier url or uri first.
identicalbooks = self.do_id_search(story.getMetadata('seriesUrl'))
# print("identicalbooks:%s"%identicalbooks)
if len(identicalbooks) > 0 and question_dialog(self.gui, _('Skip Story?'),'''
if len(identicalbooks) > 0 and (prefs['auto_reject_seriesurlid'] or \
question_dialog(self.gui, _('Skip Story?'),'''
<h3>%s</h3>
<p>%s</p>
<p>%s</p>
@@ -1165,7 +1175,7 @@ class FanFicFarePlugin(InterfaceAction):
_('"<b>%s</b>" is in series "<b><a href="%s">%s</a></b>" that you have an anthology book for.')%(story.getMetadata('title'),story.getMetadata('seriesUrl'),series[:series.index(' [')]),
_("Click '<b>Yes</b>' to Skip."),
_("Click '<b>No</b>' to download anyway.")),
show_copy_button=False):
show_copy_button=False)):
book['comment'] = _("Story in Series Anthology(%s).")%series
book['title'] = story.getMetadata('title')
book['author'] = [story.getMetadata('author')]
+1 -1
View File
@@ -161,7 +161,7 @@ def do_download_for_worker(book,options,merge,notification=lambda x,y:x):
adapter.set_pagecache(options['pagecache'])
story = adapter.getStoryMetadataOnly()
if 'calibre_series' in book:
if not story.getMetadata("series") and 'calibre_series' in book:
adapter.setSeries(book['calibre_series'][0],book['calibre_series'][1])
# set PI version instead of default.
+15
View File
@@ -1177,6 +1177,21 @@ romance_label: Romance
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[fictionhunt.com]
## Archive only site for ffnet HP stories.
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
extra_valid_entries: origin,originUrl,originHTML,reviews
originHTML_label:Original Story URL
## Assume entryUrl, apply to "<a class='%slink' href='%s'>%s</a>" to
## make entryHTML.
make_linkhtml_entries:origin
add_to_extra_titlepage_entries:originHTML
[fictionmania.tv]
## website encoding(s) In theory, each website reports the character
## encoding they use for each page. In practice, some sites report it
+2
View File
@@ -130,6 +130,7 @@ default_prefs['deleteotherforms'] = False
default_prefs['adddialogstaysontop'] = False
default_prefs['lookforurlinhtml'] = False
default_prefs['checkforseriesurlid'] = True
default_prefs['auto_reject_seriesurlid'] = False
default_prefs['checkforurlchange'] = True
default_prefs['injectseries'] = False
default_prefs['matchtitleauth'] = True
@@ -176,6 +177,7 @@ default_prefs['imapsessionpass'] = False
default_prefs['imapfolder'] = 'INBOX'
default_prefs['imapmarkread'] = True
default_prefs['auto_reject_from_email'] = False
default_prefs['update_existing_only_from_email'] = False
default_prefs['download_from_email_immediately'] = False
def set_library_config(library_config,db):
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
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
+2 -3
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Fanficdownloader team, 2015 FanFicFare team
# Copyright 2015 Fanficdownloader team, 2016 FanFicFare team
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
@@ -17,7 +17,7 @@
try:
# just a way to switch between web service and CLI/PI
import google.appengine.api
import google.appengine.api
except:
try: # just a way to switch between CLI and PI
import calibre.constants
@@ -31,4 +31,3 @@ except:
logger.addHandler(loghandler)
loghandler.setLevel(logging.DEBUG)
logger.setLevel(logging.DEBUG)
+1
View File
@@ -143,6 +143,7 @@ import adapter_haremlucifaelcom
import adapter_kiarepositorymujajinet
import adapter_fanfictionlucifaelcom
import adapter_adultfanfictionorg
import adapter_fictionhuntcom
## This bit of complexity allows adapters to be added by just adding
## importing. It eliminates the long if/else clauses we used to need
+8 -29
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2011 Fanficdownloader team, 2015 FanFicFare team
# Copyright 2011 Fanficdownloader team, 2016 FanFicFare team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,14 +15,12 @@
# limitations under the License.
#
import time
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
import re
import urllib2
from urllib import unquote_plus
import time
from .. import exceptions as exceptions
from ..htmlcleanup import stripHTML
@@ -206,7 +204,7 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
# b.extract()
metatext = stripHTML(grayspan).replace('Hurt/Comfort','Hurt-Comfort')
#logger.debug("metatext:(%s)"%metatext)
if 'Status: Complete' in metatext:
self.story.setMetadata('status', 'Completed')
else:
@@ -282,8 +280,8 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
l = chars_ships_text
while '[' in l:
self.story.addToList('ships',l[l.index('[')+1:l.index(']')].replace(', ','/'))
l = l[l.index(']')+1:]
l = l[l.index(']')+1:]
if get_cover:
# Try the larger image first.
cover_url = ""
@@ -331,7 +329,7 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
# Find the chapter selector
select = soup.find('select', { 'name' : 'chapter' } )
if select is None:
# no selector found, so it's a one-chapter story.
self.chapterUrls.append((self.story.getMetadata('title'),url))
@@ -351,36 +349,17 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
return
def getChapterText(self, url):
# time.sleep(4.0) ## ffnet(and, I assume, fpcom) tends to fail
# ## more if hit too fast. This is in
# ## additional to what ever the
# ## slow_down_sleep_time setting is.
logger.debug('Getting chapter text from: %s' % url)
## ffnet(and, I assume, fpcom) tends to fail more if hit too
## fast. This is in additional to what ever the
## slow_down_sleep_time setting is.
data = self._fetchUrl(url,extrasleep=4.0)
if "Please email this error message in full to <a href='mailto:support@fanfiction.com'>support@fanfiction.com</a>" in data:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! FanFiction.net Site Error!" % url)
# some ancient stories have body tags inside them that cause
# soup parsing to discard the content. For story text we
# don't care about anything before "<div role='main'" and
# this kills any body tags.
# XXX needed with new BS? -- No, doesn't look like it
# divstr = "<div role='main'"
# if divstr not in data:
# raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
# else:
# data = data[data.index(divstr):]
# data = data.replace("<body","<notbody").replace("<BODY","<NOTBODY")
soup = self.make_soup(data)
## Remove the 'share' button.
## No longer appears in the story text.
# sharediv = soup.find('div', {'class' : 'a2a_kit a2a_default_style'})
# if sharediv:
# sharediv.extract()
div = soup.find('div', {'id' : 'storytextp'})
if None == div:
@@ -0,0 +1,147 @@
# -*- coding: utf-8 -*-
# Copyright 2016 FanFicFare team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# 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 logging
logger = logging.getLogger(__name__)
import re
import urllib2
from .. import exceptions as exceptions
from ..htmlcleanup import stripHTML
from base_adapter import BaseSiteAdapter, makeDate
class FictionHuntComSiteAdapter(BaseSiteAdapter):
def __init__(self, config, url):
BaseSiteAdapter.__init__(self, config, url)
self.story.setMetadata('siteabbrev','fichunt')
# get storyId from url--url validation guarantees second part is storyId
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
# normalized story URL.
self._setURL("http://"+self.getSiteDomain()\
+"/read/"+self.story.getMetadata('storyId')+"/1")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%d-%m-%Y"
@staticmethod
def getSiteDomain():
return 'fictionhunt.com'
@classmethod
def getSiteExampleURLs(cls):
return "http://fictionhunt.com/read/1234/1"
def getSiteURLPattern(self):
return r"http://(www.)?fictionhunt.com/read/\d+(/\d+)?(/|/[^/]+)?/?$"
def use_pagecache(self):
'''
adapters that will work with the page cache need to implement
this and change it to True.
'''
return True
def doExtractChapterUrlsAndMetadata(self,get_cover=True):
# fetch the chapter. From that we will get almost all the
# metadata and chapter list
url = self.url
try:
data = self._fetchUrl(url)
except urllib2.HTTPError, e:
if e.code == 404:
raise exceptions.StoryDoesNotExist(self.meta)
else:
raise e
# use BeautifulSoup HTML parser to make everything easier to find.
soup = self.make_soup(data)
self.story.setMetadata('title',stripHTML(soup.find('div',{'class':'title'})).strip())
self.setDescription(url,'<i>(Story descriptions not available on fictionhunt.com)</i>')
# Find authorid and URL from... author url.
# fictionhunt doesn't have author pages, use ffnet original author link.
a = soup.find('a', href=re.compile(r"fanfiction.net/u/\d+"))
self.story.setMetadata('authorId',a['href'].split('/')[-1])
self.story.setMetadata('authorUrl','https://www.fanfiction.net/u/'+self.story.getMetadata('authorId'))
self.story.setMetadata('author',a.string)
# Find original ffnet URL
a = soup.find('a', href=re.compile(r"fanfiction.net/s/\d+"))
self.story.setMetadata('origin',stripHTML(a))
self.story.setMetadata('originUrl',a['href'])
# Fleur D. & Harry P. & Hermione G. & Susan B. - Words: 42,848 - Rated: M - English - None - Chapters: 9 - Reviews: 248 - Updated: 21-09-2016 - Published: 16-05-2015 - by Elven Sorcerer (FFN)
# None - Words: 13,087 - Rated: M - English - Romance & Supernatural - Chapters: 3 - Reviews: 5 - Updated: 21-09-2016 - Published: 20-09-2016
# Harry P. & OC - Words: 10,910 - Rated: M - English - None - Chapters: 5 - Reviews: 6 - Updated: 21-09-2016 - Published: 11-09-2016
# Dudley D. & Harry P. & Nagini & Vernon D. - Words: 4,328 - Rated: K+ - English - None - Chapters: 2 - Updated: 21-09-2016 - Published: 20-09-2016 -
details = soup.find('div',{'class':'details'})
detail_re = \
r'(?P<characters>.+) - Words: (?P<numWords>[0-9,]+) - Rated: (?P<rating>[a-zA-Z\\+]+) - (?P<language>.+) - (?P<genre>.+)'+ \
r' - Chapters: (?P<numChapters>[0-9,]+)( - Reviews: (?P<reviews>[0-9,]+))? - Updated: (?P<dateUpdated>[0-9-]+)'+ \
r' - Published: (?P<datePublished>[0-9-]+)(?P<completed> - Complete)?'
details_dict = re.match(detail_re,stripHTML(details)).groupdict()
# lists
for meta in ('characters','genre'):
if details_dict[meta] != 'None':
self.story.extendList(meta,details_dict[meta].split(' & '))
# scalars
for meta in ('numWords','numChapters','rating','language','reviews'):
self.story.setMetadata(meta,details_dict[meta])
# dates
for meta in ('datePublished','dateUpdated'):
self.story.setMetadata(meta, makeDate(details_dict[meta], self.dateformat))
# status
if details_dict['completed']:
self.story.setMetadata('status', 'Completed')
else:
self.story.setMetadata('status', 'In-Progress')
# It's assumed that the number of chapters is correct.
# There's no complete list of chapters, so the only
# alternative is to get the num of chaps from the last
# indiated chapter list instead.
for i in range(1,1+int(self.story.getMetadata('numChapters'))):
self.chapterUrls.append(("Chapter "+unicode(i),"http://"+self.getSiteDomain()\
+"/read/"+self.story.getMetadata('storyId')+"/%s"%i))
def getChapterText(self, url):
logger.debug('Getting chapter text from: %s' % url)
data = self._fetchUrl(url)
soup = self.make_soup(data)
div = soup.find('div', {'class' : 'text'})
return self.utf8FromSoup(url,div)
def getClass():
return FictionHuntComSiteAdapter
@@ -184,7 +184,6 @@ class NfaCommunityComAdapter(BaseSiteAdapter): # XXX
except:
return ""
# <span class="label">Rated:</span> NC-17<br /> etc
labels = soup.findAll('span',{'class':'label'})
for labelspan in labels:
value = labelspan.nextSibling
@@ -195,7 +194,13 @@ class NfaCommunityComAdapter(BaseSiteAdapter): # XXX
svalue = ""
while 'label' not in defaultGetattr(value,'class'):
svalue += unicode(value)
value = value.nextSibling
# poor HTML(unclosed <p> for one) can cause run on
# over the next label.
if '<span class="label">' in svalue:
svalue = svalue[0:svalue.find('<span class="label">')]
break
else:
value = value.nextSibling
self.setDescription(url,svalue)
#self.story.setMetadata('description',stripHTML(svalue))
+191 -109
View File
@@ -26,6 +26,8 @@ import pprint
import string
import sys
version="2.4.0"
if sys.version_info < (2, 5):
print 'This program requires Python 2.5 or newer.'
sys.exit(1)
@@ -43,13 +45,13 @@ try:
from calibre_plugins.fanficfare_plugin.fanficfare.configurable import Configuration
from calibre_plugins.fanficfare_plugin.fanficfare.epubutils import (
get_dcsource_chaptercount, get_update_data, reset_orig_chapters_epub)
from calibre_plugins.fanficfare_plugin.fanficfare.geturls import get_urls_from_page
from calibre_plugins.fanficfare_plugin.fanficfare.geturls import get_urls_from_page, get_urls_from_imap
except ImportError:
from fanficfare import adapters, writers, exceptions
from fanficfare.configurable import Configuration
from fanficfare.epubutils import (
get_dcsource_chaptercount, get_update_data, reset_orig_chapters_epub)
from fanficfare.geturls import get_urls_from_page
from fanficfare.geturls import get_urls_from_page, get_urls_from_imap
def write_story(config, adapter, writeformat, metaonly=False, outstream=None):
@@ -59,13 +61,15 @@ def write_story(config, adapter, writeformat, metaonly=False, outstream=None):
del writer
return output_filename
def main(argv=None, parser=None, passed_defaultsini=None, passed_personalini=None):
def main(argv=None,
parser=None,
passed_defaultsini=None,
passed_personalini=None):
if argv is None:
argv = sys.argv[1:]
# read in args, anything starting with -- will be treated as --<varible>=<value>
if not parser:
parser = OptionParser('usage: %prog [options] storyurl')
parser = OptionParser('usage: %prog [options] [STORYURL]...')
parser.add_option('-f', '--format', dest='format', default='epub',
help='write story as FORMAT, epub(default), mobi, text or html', metavar='FORMAT')
@@ -88,41 +92,68 @@ def main(argv=None, parser=None, passed_defaultsini=None, passed_personalini=Non
help='Retrieve metadata and stop. Or, if --update-epub, update metadata title page only.', )
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.', )
help='Update an existing epub(if present) with new chapters. Give either epub filename or story URL.', )
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.', )
parser.add_option('--unnew',
action='store_true', dest='unnew',
help='Remove (new) chapter marks left by mark_new_chapters setting.', )
parser.add_option('--force',
action='store_true', dest='force',
help='Force overwrite of an existing epub, download and overwrite all chapters.', )
parser.add_option('-i', '--infile',
help='Give a filename to read for URLs (and/or existing EPUB files with -u for updates).',
help='Give a filename to read for URLs (and/or existing EPUB files with --update-epub).',
dest='infile', default=None,
metavar='INFILE')
parser.add_option('-l', '--list',
action='store_true', dest='list',
dest='list', default=None, metavar='URL',
help='Get list of valid story URLs from page given.', )
parser.add_option('-n', '--normalize-list',
action='store_true', dest='normalize', default=False,
dest='normalize', default=None, metavar='URL',
help='Get list of valid story URLs from page given, but normalized to standard forms.', )
parser.add_option('--download-list',
dest='downloadlist', default=None, metavar='URL',
help='Download story URLs retrieved from page given. Update existing EPUBs if used with --update-epub.', )
parser.add_option('--imap',
action='store_true', dest='imaplist',
help='Get list of valid story URLs from unread email from IMAP account configured in ini.', )
parser.add_option('--download-imap',
action='store_true', dest='downloadimap',
help='Download valid story URLs from unread email from IMAP account configured in ini. Update existing EPUBs if used with --update-epub.', )
parser.add_option('-s', '--sites-list',
action='store_true', dest='siteslist', default=False,
help='Get list of valid story URLs examples.', )
parser.add_option('-d', '--debug',
action='store_true', dest='debug',
help='Show debug output while downloading.', )
help='Show debug and notice output.', )
parser.add_option('-v', '--version',
action='store_true', dest='version',
help='Display version and quit.', )
options, args = parser.parse_args(argv)
if options.version:
print("Version: %s" % version)
return
if not options.debug:
logger = logging.getLogger('fanficfare')
logger.setLevel(logging.INFO)
logger.setLevel(logging.WARNING)
if not (options.siteslist or options.infile) and len(args) != 1:
parser.error('incorrect number of arguments')
list_only = any((options.imaplist,
options.siteslist,
options.list,
options.normalize,
))
if list_only and (args or any((options.downloadimap,
options.downloadlist))):
parser.error('Incorrect arguments: Cannot download and list URLs at the same time.')
if options.siteslist:
for site, examples in adapters.getSiteExamples():
@@ -137,43 +168,84 @@ def main(argv=None, parser=None, passed_defaultsini=None, passed_personalini=Non
if options.unnew and options.format != 'epub':
parser.error('--unnew only works with epub')
urls=args
if not list_only and not (args or any((options.downloadimap,
options.downloadlist))):
parser.print_help();
return
if options.list:
configuration = get_configuration(options.list,
passed_defaultsini,
passed_personalini,options)
retlist = get_urls_from_page(options.list, configuration)
print '\n'.join(retlist)
if options.normalize:
configuration = get_configuration(options.normalize,
passed_defaultsini,
passed_personalini,options)
retlist = get_urls_from_page(options.normalize, configuration,normalize=True)
print '\n'.join(retlist)
if options.downloadlist:
configuration = get_configuration(options.downloadlist,
passed_defaultsini,
passed_personalini,options)
retlist = get_urls_from_page(options.downloadlist, configuration)
urls.extend(retlist)
if options.imaplist or options.downloadimap:
# list doesn't have a supported site.
configuration = get_configuration('test1.com',passed_defaultsini,passed_personalini,options)
markread = configuration.getConfig('imap_mark_read') == 'true' or \
(configuration.getConfig('imap_mark_read') == 'downloadonly' and options.downloadimap)
retlist = get_urls_from_imap(configuration.getConfig('imap_server'),
configuration.getConfig('imap_username'),
configuration.getConfig('imap_password'),
configuration.getConfig('imap_folder'),
markread)
if options.downloadimap:
urls.extend(retlist)
else:
print '\n'.join(retlist)
# for passing in a file list
if options.infile:
urls=[]
with open(options.infile,"r") as infile:
#print "File exists and is readable"
#fileurls = [line.strip() for line in infile]
for url in infile:
url = url[:url.find('#')].strip()
if len(url) > 0:
#print "URL: (%s)"%url
urls.append(url)
else:
urls = args
if len(urls) > 1:
for url in urls:
try:
do_download(url,
options,
passed_defaultsini,
passed_personalini)
if not list_only:
if len(urls) < 1:
print "No valid story URLs found"
else:
for url in urls:
try:
do_download(url,
options,
passed_defaultsini,
passed_personalini)
#print("pagecache:%s"%options.pagecache.keys())
except Exception, e:
print "URL(%s) Failed: Exception (%s). Run URL individually for more detail."%(url,e)
else:
do_download(urls[0],
options,
passed_defaultsini,
passed_personalini)
except Exception, e:
if len(urls) == 1:
raise
print "URL(%s) Failed: Exception (%s). Run URL individually for more detail."%(url,e)
# make rest a function and loop on it.
def do_download(arg,
options,
passed_defaultsini,
passed_personalini):
# Attempt to update an existing epub.
chaptercount = None
output_filename = None
@@ -182,7 +254,7 @@ def do_download(arg,
# remove mark_new_chapters marks
reset_orig_chapters_epub(arg,arg)
return
if options.update:
try:
url, chaptercount = get_dcsource_chaptercount(arg)
@@ -197,71 +269,12 @@ def do_download(arg,
url = arg
else:
url = arg
try:
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.
configuration = Configuration('test1.com', options.format)
else:
raise e
conflist = []
homepath = join(expanduser('~'), '.fanficdownloader')
## also look for .fanficfare now, give higher priority than old dir.
homepath2 = join(expanduser('~'), '.fanficfare')
if passed_defaultsini:
configuration.readfp(passed_defaultsini)
# don't need to check existance for our selves.
conflist.append(join(dirname(__file__), 'defaults.ini'))
conflist.append(join(homepath, 'defaults.ini'))
conflist.append(join(homepath2, 'defaults.ini'))
conflist.append('defaults.ini')
if passed_personalini:
configuration.readfp(passed_personalini)
conflist.append(join(homepath, 'personal.ini'))
conflist.append(join(homepath2, 'personal.ini'))
conflist.append('personal.ini')
if options.configfile:
conflist.extend(options.configfile)
logging.debug('reading %s config file(s), if present' % conflist)
configuration.read(conflist)
try:
configuration.add_section('overrides')
except ConfigParser.DuplicateSectionError:
pass
if options.force:
configuration.set('overrides', 'always_overwrite', 'true')
if options.update and chaptercount:
configuration.set('overrides', 'output_filename', output_filename)
if options.update and not options.updatecover:
configuration.set('overrides', 'never_make_cover', 'true')
# images only for epub, even if the user mistakenly turned it
# on else where.
if options.format not in ('epub', 'html'):
configuration.set('overrides', 'include_images', 'false')
if options.options:
for opt in options.options:
(var, val) = opt.split('=')
configuration.set('overrides', var, val)
if options.list or options.normalize:
retlist = get_urls_from_page(arg, configuration, normalize=options.normalize)
print '\n'.join(retlist)
return
configuration = get_configuration(url,
passed_defaultsini,
passed_personalini,
options,
chaptercount)
try:
adapter = adapters.getAdapter(configuration, url)
@@ -269,10 +282,10 @@ def do_download(arg,
if not hasattr(options,'pagecache'):
options.pagecache = adapter.get_empty_pagecache()
options.cookiejar = adapter.get_empty_cookiejar()
adapter.set_pagecache(options.pagecache)
adapter.set_cookiejar(options.cookiejar)
adapter.setChaptersRange(options.begin, options.end)
# check for updating from URL (vs from file)
@@ -290,17 +303,20 @@ def do_download(arg,
if adapter.getConfig('include_images') and not adapter.getConfig('no_image_processing'):
try:
from calibre.utils.magick import Image
logging.debug('Using calibre.utils.magick')
except ImportError:
try:
import Image
logging.debug('Using PIL')
## Pillow is a more current fork of PIL library
from PIL import Image
logging.debug('Using Pillow')
except ImportError:
print "You have include_images enabled, but Python Image Library(PIL) isn't found.\nImages will be included full size in original format.\nContinue? (y/n)?"
if not sys.stdin.readline().strip().lower().startswith('y'):
return
try:
import Image
logging.debug('Using PIL')
except ImportError:
print "You have include_images enabled, but Python Image Library(PIL) isn't found.\nImages will be included full size in original format.\nContinue? (y/n)?"
if not sys.stdin.readline().strip().lower().startswith('y'):
return
# three tries, that's enough if both user/pass & is_adult needed,
# or a couple tries of one or the other
@@ -375,6 +391,72 @@ def do_download(arg,
except exceptions.AccessDenied as ad:
print ad
def get_configuration(url,
passed_defaultsini,
passed_personalini,
options,
chaptercount=None):
try:
configuration = Configuration(adapters.getConfigSectionsFor(url), options.format)
except exceptions.UnknownSite, e:
if options.list or options.normalize or options.downloadlist:
# list for page doesn't have to be a supported site.
configuration = Configuration('test1.com', options.format)
else:
raise e
conflist = []
homepath = join(expanduser('~'), '.fanficdownloader')
## also look for .fanficfare now, give higher priority than old dir.
homepath2 = join(expanduser('~'), '.fanficfare')
if passed_defaultsini:
configuration.readfp(passed_defaultsini)
# don't need to check existance for our selves.
conflist.append(join(dirname(__file__), 'defaults.ini'))
conflist.append(join(homepath, 'defaults.ini'))
conflist.append(join(homepath2, 'defaults.ini'))
conflist.append('defaults.ini')
if passed_personalini:
configuration.readfp(passed_personalini)
conflist.append(join(homepath, 'personal.ini'))
conflist.append(join(homepath2, 'personal.ini'))
conflist.append('personal.ini')
if options.configfile:
conflist.extend(options.configfile)
logging.debug('reading %s config file(s), if present' % conflist)
configuration.read(conflist)
try:
configuration.add_section('overrides')
except ConfigParser.DuplicateSectionError:
pass
if options.force:
configuration.set('overrides', 'always_overwrite', 'true')
if options.update and chaptercount:
configuration.set('overrides', 'output_filename', output_filename)
if options.update and not options.updatecover:
configuration.set('overrides', 'never_make_cover', 'true')
# images only for epub, even if the user mistakenly turned it
# on else where.
if options.format not in ('epub', 'html'):
configuration.set('overrides', 'include_images', 'false')
if options.options:
for opt in options.options:
(var, val) = opt.split('=')
configuration.set('overrides', var, val)
return configuration
if __name__ == '__main__':
main()
+49
View File
@@ -404,6 +404,40 @@ user_agent:FFF/2.X
## non-intuitive.
#description_limit:1000
## The FFF CLI can fetch story URLs from unread emails when configured
## to read from your IMAP mail server. The example shows GMail, but
## other services that support IMAP can be used. GMail requires you
## to turn on an option to enable IMAP access. Only the CLI uses these
## options--the Calibre Plugin stores these separately.
##
## It's safest if you create a separate email account that you use
## only for your story update notices. FanFicFare cannot guarantee
## that malicious code cannot get your email password once you've
## saved it. Use this feature at your own risk.
##
#imap_server:imap.gmail.com
#imap_username:youraddress@gmail.com
#imap_password:XXXXXXXX
#imap_folder:INBOX
## Mark mails with story URLs read:
## imap_mark_read can be 'true', 'false'(default) or 'downloadonly'.
##
## If 'true', unread emails will be marked as read when
## either CLI option --imap to list the story URLs or --download-imap
## to download story URLs from email are used.
##
## If 'downloadonly', unread emails will be marked as read
## only when CLI --download-imap to download story URLs from email are
## used.
##
## If 'false', unread emails will not be marked as read.
##
## Only unread emails will be searched for story URLs, and only emails
## containing valid story URLs will ever be marked read.
##
#imap_mark_read:true
[base_efiction]
## At the time of writing, eFiction Base adapters allow downloading
## the whole story in bulk using the 'Print' feature. If 'bulk_load'
@@ -1156,6 +1190,21 @@ romance_label: Romance
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[fictionhunt.com]
## Archive only site for ffnet HP stories.
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
extra_valid_entries: origin,originUrl,originHTML,reviews
originHTML_label:Original Story URL
## Assume entryUrl, apply to "<a class='%slink' href='%s'>%s</a>" to
## make entryHTML.
make_linkhtml_entries:origin
add_to_extra_titlepage_entries:originHTML
[fictionmania.tv]
## website encoding(s) In theory, each website reports the character
## encoding they use for each page. In practice, some sites report it
+3 -5
View File
@@ -16,14 +16,12 @@ from os import path
# Get the long description from the relevant file
with codecs.open('DESCRIPTION.rst', encoding='utf-8') as f:
long_description = f.read()
setup(
name="FanFicFare",
# 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.3.6",
# Versions should comply with PEP440.
version="2.4.0",
description='A tool for downloading fanfiction to eBook formats',
long_description=long_description,
+79
View File
@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
import codecs, sys, re
from tempfile import mkstemp
from os import rename, close, unlink
#print sys.argv[1:]
## Files that contain version numbers that will need to be updated.
version_files = [
# 'version_test.sml',
# 'version_test.txt',
'setup.py',
'calibre-plugin/__init__.py',
'webservice/app.yaml',
'fanficfare/cli.py',
]
## save version from this file for index.html link.
# save_file='version_test.txt'
save_file='webservice/app.yaml'
saved_version = None
def main(args):
## major.minor.micro
'''
version = (2, 3, 6)
version="2.3.6",
version: 2-3-06a
version="2.3.6"
'''
version_re = \
r'^(?P<prefix>[ ]*)version(?P<infix>[ =:"\\(]+)' \
r'(?P<major>[0-9]+)(?P<dot1>[, \\.-]+)' \
r'(?P<minor>[0-9]+)(?P<dot2>[, \\.-]+)' \
r'(?P<micro>[0-9]+[a-z]?)(?P<suffix>[",\\)]*\r?\n)$'
version_subs = '\g<prefix>version\g<infix>%s\g<dot1>%s\g<dot2>%s\g<suffix>' % tuple(args)
do_loop(version_files, version_re, version_subs)
if saved_version:
# index_files = ['index.html']
index_files = ['webservice/index.html']
index_re = 'http://([0-9-]+[a-z]?)\\.fanficfare\\.appspot\\.com'
index_subs = 'http://%s-%s-%s.fanficfare.appspot.com'%saved_version
do_loop(index_files, index_re, index_subs)
def do_loop(files, pattern, substring):
global saved_version
for source_file_path in files:
print "src:"+source_file_path
fh, target_file_path = mkstemp()
with codecs.open(target_file_path, 'w', 'utf-8') as target_file:
with codecs.open(source_file_path, 'r', 'utf-8') as source_file:
for line in source_file:
repline = re.sub(pattern, substring, line)
if line != repline and source_file_path == save_file:
m = re.match(pattern,line)
saved_version = (m.group('major'),m.group('minor'),m.group('micro'))
print("<-%s->%s"%(line,repline))
target_file.write(repline)
close(fh)
unlink(source_file_path)
rename(target_file_path,source_file_path)
if __name__ == '__main__':
args = sys.argv[1:]
try:
if len(args) != 3:
raise Exception()
[int(x) for x in args]
except:
print "Requires exactly 3 numeric args: major minor micro"
exit()
main(args)
# print saved_version
+1 -1
View File
@@ -1,6 +1,6 @@
# ffd-retief-hrd fanficfare
application: fanficfare
version: 2-3-06
version: 2-4-0
runtime: python27
api_version: 1
threadsafe: true
+1 -1
View File
@@ -35,7 +35,7 @@
If you have any problems with this application, please
report them in
the <a href="http://groups.google.com/group/fanfic-downloader">FanFicFare Google Group</a>. The
<a href="http://2-3-05.fanficfare.appspot.com">previous version
<a href="http://2-3-06.fanficfare.appspot.com">previous version
</a> is also available for you to use if necessary.
</p>
<div id='error'>
+16 -23
View File
@@ -45,11 +45,11 @@ from fanficfare import adapters, writers, exceptions
from fanficfare.configurable import Configuration
class UserConfigServer(webapp2.RequestHandler):
def getUserConfig(self,user,url,fileformat):
configuration = Configuration(adapters.getConfigSectionsFor(url),fileformat)
logging.debug('reading defaults.ini config file')
configuration.read('fanficfare/defaults.ini')
@@ -96,7 +96,7 @@ class MainHandler(webapp2.RequestHandler):
template_values = {'login_url' : url, 'authorized': False}
path = os.path.join(os.path.dirname(__file__), 'index.html')
template_values['supported_sites'] = '<dl>\n'
for (site,examples) in adapters.getSiteExamples():
template_values['supported_sites'] += "<dt>%s</dt>\n<dd>Example Story URLs:<br>"%site
@@ -139,7 +139,7 @@ class EditConfigServer(UserConfigServer):
self.redirect("/?error=configsaved")
except Exception, e:
logging.info("Saved Config Failed:%s"%e)
self.redirect("/?error=custom&errtext=%s"%urlEscape(unicode(e)))
self.redirect("/?error=custom&errtext=%s"%urllib.quote(unicode(e),''))
else: # not update, assume display for edit
if uconfig is not None and uconfig.config:
config = uconfig.config
@@ -240,7 +240,7 @@ class FileStatusServer(webapp2.RequestHandler):
if download:
logging.info("Status url: %s" % download.url)
if download.completed and download.format=='epub':
escaped_url = urlEscape(self.request.host_url+"/file/"+download.name+"."+download.format+"?id="+fileId+"&fake=file."+download.format)
escaped_url = urllib.quote(self.request.host_url+"/file/"+download.name+"."+download.format+"?id="+fileId+"&fake=file."+download.format,'')
else:
download = DownloadMeta()
download.failure = "Download not found"
@@ -295,7 +295,7 @@ class RecentFilesServer(webapp2.RequestHandler):
for fic in fics:
if fic.completed and fic.format == 'epub':
fic.escaped_url = urlEscape(self.request.host_url+"/file/"+fic.name+"."+fic.format+"?id="+unicode(fic.key())+"&fake=file."+fic.format)
fic.escaped_url = urllib.quote(self.request.host_url+"/file/"+fic.name+"."+fic.format+"?id="+unicode(fic.key())+"&fake=file."+fic.format,'')
template_values = dict(fics = fics, nickname = user.nickname())
path = os.path.join(os.path.dirname(__file__), 'recent.html')
@@ -313,7 +313,7 @@ class AllRecentFilesServer(webapp2.RequestHandler):
q.order('-date')
else:
q.order('-count')
fics = q.fetch(200)
logging.info("Recent fetched %d downloads for user %s."%(len(fics),user.nickname()))
@@ -322,7 +322,7 @@ class AllRecentFilesServer(webapp2.RequestHandler):
for fic in fics:
ficslug = FicSlug(fic)
sendslugs.append(ficslug)
template_values = dict(fics = sendslugs, nickname = user.nickname())
path = os.path.join(os.path.dirname(__file__), 'allrecent.html')
self.response.out.write(template.render(path, template_values))
@@ -333,7 +333,7 @@ class FicSlug():
self.count = savedmeta.count
for k, v in savedmeta.meta.iteritems():
setattr(self,k,v)
class FanfictionDownloader(UserConfigServer):
def get(self):
self.post()
@@ -361,7 +361,7 @@ class FanfictionDownloader(UserConfigServer):
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')
@@ -376,8 +376,11 @@ class FanfictionDownloader(UserConfigServer):
try:
try:
configuration = self.getUserConfig(user,url,format)
except exceptions.UnknownSite:
self.redirect("/?error=custom&errtext=%s"%urllib.quote("Unsupported site in URL (%s). See 'Support sites' list below."%url,''))
return
except Exception, e:
self.redirect("/?error=custom&errtext=%s"%urlEscape("There's an error in your User Configuration: "+unicode(e)))
self.redirect("/?error=custom&errtext=%s"%urllib.quote("There's an error in your User Configuration: "+unicode(e),'')[:2048]) # limited due to Locatton header length limit.
return
adapter = adapters.getAdapter(configuration,url)
@@ -537,7 +540,7 @@ class FanfictionDownloaderTask(UserConfigServer):
# delete existing chunks first
for chunk in download.data_chunks:
chunk.delete()
index=0
while( len(data) > 0 ):
# logging.info("len(data): %s" % len(data))
@@ -563,7 +566,7 @@ class FanfictionDownloaderTask(UserConfigServer):
smeta.meta = allmeta
smeta.date = datetime.datetime.now()
smeta.put()
logging.info("Download finished OK")
del data
@@ -616,16 +619,6 @@ def getDownloadMeta(id=None,url=None,user=None,format=None,new=False):
return download
def toPercentDecimal(match):
"Return the %decimal number for the character for url escaping"
s = match.group(1)
return "%%%02x" % ord(s)
def urlEscape(data):
"Escape text, including unicode, for use in URLs"
p = re.compile(r'([^\w])')
return p.sub(toPercentDecimal, data.encode("utf-8"))
logging.getLogger().setLevel(logging.DEBUG)
app = webapp2.WSGIApplication([('/', MainHandler),
('/fdowntask', FanfictionDownloaderTask),