mirror of
https://github.com/wassname/FanFicFare.git
synced 2026-09-14 11:14:10 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f181ff2c03 | ||
|
|
389a04ce81 | ||
|
|
787ebbdb53 | ||
|
|
cc6b31427e | ||
|
|
af9679a290 | ||
|
|
c18a972b12 | ||
|
|
10d297d77f | ||
|
|
df10e539a9 | ||
|
|
16a090c019 | ||
|
|
5472b21447 | ||
|
|
0e17e15466 | ||
|
|
e30bd1adcc | ||
|
|
c47bf27689 | ||
|
|
84a308289c | ||
|
|
94b44c9519 | ||
|
|
1c431cd972 | ||
|
|
0233404429 | ||
|
|
ca2d2ff0be | ||
|
|
0b4fc8b138 | ||
|
|
29598b1306 | ||
|
|
4990368ac5 | ||
|
|
ef3969b33b | ||
|
|
4cb69a0a3c | ||
|
|
3488c54f3d | ||
|
|
ee211a362a | ||
|
|
ddc957d1b7 | ||
|
|
e677dc063d | ||
|
|
1079713432 | ||
|
|
ff0ed8c9a2 | ||
|
|
a5c731e471 | ||
|
|
dafbac5c9b | ||
|
|
668fca6f6e | ||
|
|
e97f03a680 | ||
|
|
0c272bcf36 | ||
|
|
58eca7efff | ||
|
|
5b31f55b18 |
@@ -0,0 +1 @@
|
||||
include DESCRIPTION.rst
|
||||
@@ -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, 11)
|
||||
version = (2, 2, 13)
|
||||
minimum_calibre_version = (1, 48, 0)
|
||||
|
||||
#: This field defines the GUI plugin class that contains all the code
|
||||
|
||||
+101
-98
@@ -117,7 +117,7 @@ save_collisions={
|
||||
SAVE_CALIBREONLY:CALIBREONLY,
|
||||
SAVE_CALIBREONLYSAVECOL:CALIBREONLYSAVECOL,
|
||||
}
|
||||
|
||||
|
||||
anthology_collision_order=[UPDATE,
|
||||
UPDATEALWAYS,
|
||||
OVERWRITEALWAYS]
|
||||
@@ -127,7 +127,7 @@ gpstyle='QGroupBox {border:0; padding-top:10px; padding-bottom:0px; margin-botto
|
||||
class RejectUrlEntry:
|
||||
|
||||
matchpat=re.compile(r"^(?P<url>[^,]+?)(,(?P<fullnote>(((?P<title>.+?) by (?P<auth>.+?)( - (?P<note>.+))?)|.*)))?$")
|
||||
|
||||
|
||||
def __init__(self,url_or_line,note=None,title=None,auth=None,
|
||||
addreasontext=None,fromline=False,book_id=None):
|
||||
|
||||
@@ -151,7 +151,7 @@ class RejectUrlEntry:
|
||||
self.note=note
|
||||
self.title=title
|
||||
self.auth=auth
|
||||
|
||||
|
||||
if not self.note:
|
||||
if addreasontext:
|
||||
self.note = addreasontext
|
||||
@@ -160,14 +160,14 @@ class RejectUrlEntry:
|
||||
else:
|
||||
if addreasontext:
|
||||
self.note = self.note + ' - ' + addreasontext
|
||||
|
||||
|
||||
self.url = getNormalStoryURL(self.url)
|
||||
self.valid = self.url != None
|
||||
|
||||
|
||||
def to_line(self):
|
||||
# always 'url,'
|
||||
return self.url+","+self.fullnote()
|
||||
|
||||
|
||||
def fullnote(self):
|
||||
retval = ""
|
||||
if self.title and self.auth:
|
||||
@@ -175,10 +175,10 @@ class RejectUrlEntry:
|
||||
retval = retval + "%s by %s"%(self.title,self.auth)
|
||||
if self.note:
|
||||
retval = retval + " - "
|
||||
|
||||
|
||||
if self.note:
|
||||
retval = retval + self.note
|
||||
|
||||
|
||||
return retval
|
||||
|
||||
class NotGoingToDownload(Exception):
|
||||
@@ -222,7 +222,7 @@ class DroppableQTextEdit(QTextEdit):
|
||||
self.append("\n".join(urllist))
|
||||
return None
|
||||
return QTextEdit.dropEvent(self,event)
|
||||
|
||||
|
||||
def canInsertFromMimeData(self, source):
|
||||
if source.hasUrls():
|
||||
return True
|
||||
@@ -234,7 +234,7 @@ class DroppableQTextEdit(QTextEdit):
|
||||
self.append(source.text())
|
||||
else:
|
||||
return QTextEdit.insertFromMimeData(self, source)
|
||||
|
||||
|
||||
class AddNewDialog(SizePersistedDialog):
|
||||
|
||||
go_signal = pyqtSignal(object, object, object, object)
|
||||
@@ -242,7 +242,7 @@ class AddNewDialog(SizePersistedDialog):
|
||||
def __init__(self, gui, prefs, icon):
|
||||
SizePersistedDialog.__init__(self, gui, 'fff:add new dialog')
|
||||
self.prefs = prefs
|
||||
|
||||
|
||||
self.setMinimumWidth(300)
|
||||
self.l = QVBoxLayout()
|
||||
self.setLayout(self.l)
|
||||
@@ -259,7 +259,7 @@ class AddNewDialog(SizePersistedDialog):
|
||||
|
||||
self.merge = self.newmerge = False
|
||||
self.extraoptions = {}
|
||||
|
||||
|
||||
# elements to hide when doing merge.
|
||||
self.mergehide = []
|
||||
# elements to show again when doing *update* merge
|
||||
@@ -282,11 +282,11 @@ class AddNewDialog(SizePersistedDialog):
|
||||
|
||||
self.gbf.setVisible(False)
|
||||
self.groupbox.toggled.connect(self.gbf.setVisible)
|
||||
|
||||
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel(_('Output &Format:'))
|
||||
self.mergehide.append(label)
|
||||
|
||||
|
||||
self.fileform = QComboBox(self)
|
||||
self.fileform.addItem('epub')
|
||||
self.fileform.addItem('mobi')
|
||||
@@ -294,7 +294,7 @@ class AddNewDialog(SizePersistedDialog):
|
||||
self.fileform.addItem('txt')
|
||||
self.fileform.setToolTip(_('Choose output format to create. May set default from plugin configuration.'))
|
||||
self.fileform.activated.connect(self.set_collisions)
|
||||
|
||||
|
||||
horz.addWidget(label)
|
||||
label.setBuddy(self.fileform)
|
||||
horz.addWidget(self.fileform)
|
||||
@@ -332,7 +332,7 @@ class AddNewDialog(SizePersistedDialog):
|
||||
self.updateepubcover.setChecked(self.prefs['updateepubcover'])
|
||||
horz.addWidget(self.updateepubcover)
|
||||
self.mergehide.append(self.updateepubcover)
|
||||
|
||||
|
||||
self.gbl.addLayout(horz)
|
||||
|
||||
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
@@ -340,7 +340,7 @@ class AddNewDialog(SizePersistedDialog):
|
||||
self.button_box.rejected.connect(self.reject)
|
||||
self.l.addWidget(self.button_box)
|
||||
|
||||
# invoke the
|
||||
# invoke the
|
||||
def ok_clicked(self):
|
||||
self.dialog_closing(None) # save persistent size.
|
||||
self.hide()
|
||||
@@ -376,7 +376,7 @@ class AddNewDialog(SizePersistedDialog):
|
||||
self.extrapayload = extrapayload
|
||||
|
||||
self.groupbox.setVisible(not(self.merge and self.newmerge))
|
||||
|
||||
|
||||
if self.merge:
|
||||
self.toplabel.setText(_('Story URLs for anthology, one per line:'))
|
||||
self.url.setToolTip(_('URLs for stories to include in the anthology, one per line.\nWill take URLs from clipboard, but only valid URLs.'))
|
||||
@@ -398,7 +398,7 @@ class AddNewDialog(SizePersistedDialog):
|
||||
# Need to re-able after hiding/showing
|
||||
self.setAcceptDrops(True)
|
||||
self.url.setFocus()
|
||||
|
||||
|
||||
if self.prefs['adddialogstaysontop']:
|
||||
QDialog.setWindowFlags ( self, Qt.Dialog | Qt.WindowStaysOnTopHint )
|
||||
else:
|
||||
@@ -415,18 +415,18 @@ class AddNewDialog(SizePersistedDialog):
|
||||
i = self.collision.findText(save_collisions[self.prefs['collision']])
|
||||
if i > -1:
|
||||
self.collision.setCurrentIndex(i)
|
||||
|
||||
|
||||
self.updatemeta.setChecked(self.prefs['updatemeta'])
|
||||
|
||||
|
||||
if not self.merge:
|
||||
self.updateepubcover.setChecked(self.prefs['updateepubcover'])
|
||||
|
||||
|
||||
self.url.setText(url_list_text)
|
||||
if url_list_text:
|
||||
self.button_box.button(QDialogButtonBox.Ok).setFocus()
|
||||
# restore saved size.
|
||||
self.resize_dialog()
|
||||
|
||||
|
||||
if show: # so anthology update can be modal still.
|
||||
self.show()
|
||||
#self.resize(self.sizeHint())
|
||||
@@ -447,11 +447,11 @@ class AddNewDialog(SizePersistedDialog):
|
||||
|
||||
for o in order:
|
||||
self.collision.addItem(o)
|
||||
|
||||
|
||||
i = self.collision.findText(prev)
|
||||
if i > -1:
|
||||
self.collision.setCurrentIndex(i)
|
||||
|
||||
|
||||
def get_fff_options(self):
|
||||
retval = {
|
||||
'fileform': unicode(self.fileform.currentText()),
|
||||
@@ -460,14 +460,14 @@ class AddNewDialog(SizePersistedDialog):
|
||||
'updateepubcover': self.updateepubcover.isChecked(),
|
||||
'smarten_punctuation':self.prefs['smarten_punctuation']
|
||||
}
|
||||
|
||||
|
||||
if self.merge:
|
||||
retval['fileform']=='epub'
|
||||
retval['updateepubcover']=True
|
||||
if self.newmerge:
|
||||
retval['updatemeta']=True
|
||||
retval['collision']=ADDNEW
|
||||
|
||||
|
||||
return dict(retval.items() + self.extraoptions.items() )
|
||||
|
||||
def get_urlstext(self):
|
||||
@@ -477,21 +477,21 @@ class AddNewDialog(SizePersistedDialog):
|
||||
class FakeLineEdit():
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
def text(self):
|
||||
pass
|
||||
|
||||
|
||||
class CollectURLDialog(SizePersistedDialog):
|
||||
'''
|
||||
Collect single url for get urls.
|
||||
'''
|
||||
def __init__(self, gui, title, url_text, anthology=False, indiv=True):
|
||||
def __init__(self, gui, title, url_text, anthology=False, indiv=True):
|
||||
SizePersistedDialog.__init__(self, gui, 'fff:get story urls')
|
||||
self.status=False
|
||||
self.anthology=False
|
||||
|
||||
self.setMinimumWidth(300)
|
||||
|
||||
|
||||
self.l = QVBoxLayout()
|
||||
self.setLayout(self.l)
|
||||
|
||||
@@ -561,16 +561,16 @@ class UserPassDialog(QDialog):
|
||||
else:
|
||||
self.setWindowTitle(_('User/Password'))
|
||||
self.l.addWidget(QLabel(_("%s requires you to login to download this story.")%site),0,0,1,2)
|
||||
|
||||
|
||||
self.l.addWidget(QLabel(_("User:")),1,0)
|
||||
self.user = QLineEdit(self)
|
||||
self.l.addWidget(self.user,1,1)
|
||||
|
||||
|
||||
self.l.addWidget(QLabel(_("Password:")),2,0)
|
||||
self.passwd = QLineEdit(self)
|
||||
self.passwd.setEchoMode(QLineEdit.Password)
|
||||
self.l.addWidget(self.passwd,2,1)
|
||||
|
||||
|
||||
self.ok_button = QPushButton(_('OK'), self)
|
||||
self.ok_button.clicked.connect(self.ok)
|
||||
self.l.addWidget(self.ok_button,3,0)
|
||||
@@ -616,6 +616,9 @@ class LoopProgressDialog(QProgressDialog):
|
||||
from calibre_plugins.fanficfare_plugin.prefs import prefs
|
||||
self.show_est_time = prefs['show_est_time']
|
||||
|
||||
self.setLabelText('%s %d / %d' % (self.status_prefix, self.i, len(self.book_list)))
|
||||
self.setValue(self.i)
|
||||
|
||||
## self.do_loop does QTimer.singleShot on self.do_loop also.
|
||||
## A weird way to do a loop, but that was the example I had.
|
||||
QTimer.singleShot(0, self.do_loop)
|
||||
@@ -642,7 +645,7 @@ class LoopProgressDialog(QProgressDialog):
|
||||
## collision spec passed into getadapter by partial from fff_plugin
|
||||
## no retval only if it exists, but collision is SKIP
|
||||
self.foreach_function(book)
|
||||
|
||||
|
||||
except NotGoingToDownload as d:
|
||||
book['good']=False
|
||||
book['comment']=unicode(d)
|
||||
@@ -653,10 +656,10 @@ class LoopProgressDialog(QProgressDialog):
|
||||
book['comment']=unicode(e)
|
||||
logger.error("Exception: %s:%s"%(book,unicode(e)))
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
self.updateStatus()
|
||||
self.i += 1
|
||||
|
||||
|
||||
if self.i >= len(self.book_list) or self.wasCanceled():
|
||||
return self.do_when_finished()
|
||||
else:
|
||||
@@ -747,7 +750,7 @@ class UpdateExistingDialog(SizePersistedDialog):
|
||||
self.prefs = prefs
|
||||
self.setWindowTitle(header)
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
self.setLayout(layout)
|
||||
title_layout = ImageTitleLayout(self, 'images/icon.png',
|
||||
@@ -790,7 +793,7 @@ class UpdateExistingDialog(SizePersistedDialog):
|
||||
|
||||
gbf.setVisible(False)
|
||||
groupbox.toggled.connect(gbf.setVisible)
|
||||
|
||||
|
||||
label = QLabel(_('Output &Format:'))
|
||||
gbl.addWidget(label)
|
||||
self.fileform = QComboBox(self)
|
||||
@@ -803,7 +806,7 @@ class UpdateExistingDialog(SizePersistedDialog):
|
||||
self.fileform.activated.connect(self.set_collisions)
|
||||
label.setBuddy(self.fileform)
|
||||
gbl.addWidget(self.fileform)
|
||||
|
||||
|
||||
label = QLabel(_('Update Mode:'))
|
||||
gbl.addWidget(label)
|
||||
self.collision = QComboBox(self)
|
||||
@@ -820,21 +823,21 @@ class UpdateExistingDialog(SizePersistedDialog):
|
||||
self.updatemeta.setToolTip(_("Update metadata for existing stories in Calibre from web site?\n(Columns set to 'New Only' in the column tabs will only be set for new books.)"))
|
||||
self.updatemeta.setChecked(self.prefs['updatemeta'])
|
||||
gbl.addWidget(self.updatemeta)
|
||||
|
||||
|
||||
self.updateepubcover = QCheckBox(_('Update EPUB Cover?'),self)
|
||||
self.updateepubcover.setToolTip(_('Update book cover image from site or defaults (if found) <i>inside</i> the EPUB when EPUB is updated.'))
|
||||
self.updateepubcover.setChecked(self.prefs['updateepubcover'])
|
||||
gbl.addWidget(self.updateepubcover)
|
||||
|
||||
|
||||
|
||||
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
button_box.accepted.connect(self.accept)
|
||||
button_box.rejected.connect(self.reject)
|
||||
options_layout.addWidget(button_box)
|
||||
|
||||
|
||||
layout.addLayout(options_layout)
|
||||
|
||||
|
||||
# Cause our dialog size to be restored from prefs or created on first usage
|
||||
self.resize_dialog()
|
||||
self.books_table.populate_table(books)
|
||||
@@ -850,14 +853,14 @@ class UpdateExistingDialog(SizePersistedDialog):
|
||||
order.remove(UPDATEALWAYS)
|
||||
if self.prefs['savemetacol'] == '':
|
||||
order.remove(CALIBREONLYSAVECOL)
|
||||
|
||||
|
||||
for o in order:
|
||||
self.collision.addItem(o)
|
||||
|
||||
|
||||
i = self.collision.findText(prev)
|
||||
if i > -1:
|
||||
self.collision.setCurrentIndex(i)
|
||||
|
||||
|
||||
def remove_from_list(self):
|
||||
self.books_table.remove_selected_rows()
|
||||
|
||||
@@ -924,16 +927,16 @@ class StoryListTableWidget(QTableWidget):
|
||||
status_cell = IconWidgetItem(None,icon,val)
|
||||
status_cell.setData(Qt.UserRole, val)
|
||||
self.setItem(row, 0, status_cell)
|
||||
|
||||
|
||||
title_cell = ReadOnlyTableWidgetItem(book['title'])
|
||||
title_cell.setData(Qt.UserRole, row)
|
||||
self.setItem(row, 1, title_cell)
|
||||
|
||||
|
||||
self.setItem(row, 2, AuthorTableWidgetItem(", ".join(book['author']), ", ".join(book['author_sort'])))
|
||||
|
||||
|
||||
url_cell = ReadOnlyTableWidgetItem(book['url'])
|
||||
self.setItem(row, 3, url_cell)
|
||||
|
||||
|
||||
comment_cell = ReadOnlyTableWidgetItem(book['comment'])
|
||||
self.setItem(row, 4, comment_cell)
|
||||
|
||||
@@ -1011,9 +1014,9 @@ class RejectListTableWidget(QTableWidget):
|
||||
self.setItem(row, 0, url_cell)
|
||||
self.setItem(row, 1, ReadOnlyTableWidgetItem(rej.title))
|
||||
self.setItem(row, 2, ReadOnlyTableWidgetItem(rej.auth))
|
||||
|
||||
|
||||
note_cell = EditWithComplete(self,sort_func=lambda x:1)
|
||||
|
||||
|
||||
items = [rej.note]+self.rejectreasons
|
||||
note_cell.update_items_cache(items)
|
||||
note_cell.show_initial_value(rej.note)
|
||||
@@ -1021,7 +1024,7 @@ class RejectListTableWidget(QTableWidget):
|
||||
note_cell.setToolTip(_('Select or Edit Reject Note.'))
|
||||
self.setCellWidget(row, 3, note_cell)
|
||||
note_cell.setCursorPosition(0)
|
||||
|
||||
|
||||
def remove_selected_rows(self):
|
||||
self.setFocus()
|
||||
rows = self.selectionModel().selectedRows()
|
||||
@@ -1053,10 +1056,10 @@ class RejectListDialog(SizePersistedDialog):
|
||||
show_all_reasons=True,
|
||||
save_size_name='fff:reject list dialog'):
|
||||
SizePersistedDialog.__init__(self, gui, save_size_name)
|
||||
|
||||
|
||||
self.setWindowTitle(header)
|
||||
self.setWindowIcon(get_icon(icon))
|
||||
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
self.setLayout(layout)
|
||||
title_layout = ImageTitleLayout(self, icon, header,
|
||||
@@ -1072,7 +1075,7 @@ class RejectListDialog(SizePersistedDialog):
|
||||
rejects_layout.addLayout(button_layout)
|
||||
spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
button_layout.addItem(spacerItem)
|
||||
|
||||
|
||||
self.remove_button = QtGui.QToolButton(self)
|
||||
self.remove_button.setToolTip(_('Remove selected URLs from the list'))
|
||||
self.remove_button.setIcon(get_icon('list_remove.png'))
|
||||
@@ -1084,13 +1087,13 @@ class RejectListDialog(SizePersistedDialog):
|
||||
|
||||
if show_all_reasons:
|
||||
self.reason_edit = EditWithComplete(self,sort_func=lambda x:1)
|
||||
|
||||
|
||||
items = ['']+rejectreasons
|
||||
self.reason_edit.update_items_cache(items)
|
||||
self.reason_edit.show_initial_value('')
|
||||
self.reason_edit.set_separator(None)
|
||||
self.reason_edit.setToolTip(_("This will be added to whatever note you've set for each URL above."))
|
||||
|
||||
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel(_("Add this reason to all URLs added:"))
|
||||
label.setToolTip(_("This will be added to whatever note you've set for each URL above."))
|
||||
@@ -1099,7 +1102,7 @@ class RejectListDialog(SizePersistedDialog):
|
||||
self.reason_edit.setCursorPosition(0)
|
||||
horz.insertStretch(-1)
|
||||
layout.addLayout(horz)
|
||||
|
||||
|
||||
options_layout = QHBoxLayout()
|
||||
|
||||
if show_delete:
|
||||
@@ -1107,14 +1110,14 @@ class RejectListDialog(SizePersistedDialog):
|
||||
self.deletebooks.setToolTip(_("Delete the selected books after adding them to the Rejected URLs list."))
|
||||
self.deletebooks.setChecked(True)
|
||||
options_layout.addWidget(self.deletebooks)
|
||||
|
||||
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
button_box.accepted.connect(self.accept)
|
||||
button_box.rejected.connect(self.reject)
|
||||
options_layout.addWidget(button_box)
|
||||
|
||||
|
||||
layout.addLayout(options_layout)
|
||||
|
||||
|
||||
# Cause our dialog size to be restored from prefs or created on first usage
|
||||
self.resize_dialog()
|
||||
self.rejects_table.populate_table(reject_list)
|
||||
@@ -1147,7 +1150,7 @@ class RejectListDialog(SizePersistedDialog):
|
||||
except:
|
||||
# doesn't have self.reason_edit when editing existing list.
|
||||
return None
|
||||
|
||||
|
||||
def get_deletebooks(self):
|
||||
return self.deletebooks.isChecked()
|
||||
|
||||
@@ -1169,7 +1172,7 @@ class EditTextDialog(SizePersistedDialog):
|
||||
if icon:
|
||||
self.setWindowIcon(icon)
|
||||
self.l.addWidget(self.label)
|
||||
|
||||
|
||||
self.textedit = QTextEdit(self)
|
||||
self.textedit.setLineWrapMode(QTextEdit.NoWrap)
|
||||
self.textedit.setReadOnly(read_only)
|
||||
@@ -1182,13 +1185,13 @@ class EditTextDialog(SizePersistedDialog):
|
||||
|
||||
if rejectreasons or reasonslabel:
|
||||
self.reason_edit = EditWithComplete(self,sort_func=lambda x:1)
|
||||
|
||||
|
||||
items = ['']+rejectreasons
|
||||
self.reason_edit.update_items_cache(items)
|
||||
self.reason_edit.show_initial_value('')
|
||||
self.reason_edit.set_separator(None)
|
||||
self.reason_edit.setToolTip(reasonslabel)
|
||||
|
||||
|
||||
if reasonslabel:
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel(reasonslabel)
|
||||
@@ -1199,12 +1202,12 @@ class EditTextDialog(SizePersistedDialog):
|
||||
else:
|
||||
self.l.addWidget(self.reason_edit)
|
||||
self.reason_edit.setCursorPosition(0)
|
||||
|
||||
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
button_box.accepted.connect(self.accept)
|
||||
button_box.rejected.connect(self.reject)
|
||||
self.l.addWidget(button_box)
|
||||
|
||||
|
||||
# Cause our dialog size to be restored from prefs or created on first usage
|
||||
self.resize_dialog()
|
||||
|
||||
@@ -1213,7 +1216,7 @@ class EditTextDialog(SizePersistedDialog):
|
||||
|
||||
def get_reason_text(self):
|
||||
return unicode(self.reason_edit.currentText()).strip()
|
||||
|
||||
|
||||
class IniTextDialog(SizePersistedDialog):
|
||||
|
||||
def __init__(self, parent, text,
|
||||
@@ -1223,9 +1226,9 @@ class IniTextDialog(SizePersistedDialog):
|
||||
save_size_name='fff:ini text dialog',
|
||||
):
|
||||
SizePersistedDialog.__init__(self, parent, save_size_name)
|
||||
|
||||
|
||||
self.keys=dict()
|
||||
|
||||
|
||||
self.l = QVBoxLayout()
|
||||
self.setLayout(self.l)
|
||||
self.label = QLabel(label)
|
||||
@@ -1234,7 +1237,7 @@ class IniTextDialog(SizePersistedDialog):
|
||||
if icon:
|
||||
self.setWindowIcon(icon)
|
||||
self.l.addWidget(self.label)
|
||||
|
||||
|
||||
self.textedit = QTextEdit(self)
|
||||
|
||||
highlighter = IniHighlighter(self.textedit,
|
||||
@@ -1243,7 +1246,7 @@ class IniTextDialog(SizePersistedDialog):
|
||||
entries=get_valid_entries(),
|
||||
entry_keywords=get_valid_entry_keywords(),
|
||||
)
|
||||
|
||||
|
||||
self.textedit.setLineWrapMode(QTextEdit.NoWrap)
|
||||
try:
|
||||
self.textedit.setFont(QFont("Courier",
|
||||
@@ -1261,41 +1264,41 @@ class IniTextDialog(SizePersistedDialog):
|
||||
if use_find:
|
||||
|
||||
findtooltip=_('Search for string in edit box.')
|
||||
|
||||
|
||||
horz = QHBoxLayout()
|
||||
label = QLabel(_('Find:'))
|
||||
|
||||
label.setToolTip(findtooltip)
|
||||
|
||||
|
||||
# Button to search the document for something
|
||||
self.findButton = QtGui.QPushButton(_('Find'),self)
|
||||
self.findButton.clicked.connect(self.find)
|
||||
self.findButton.setToolTip(findtooltip)
|
||||
|
||||
|
||||
# The field into which to type the query
|
||||
self.findField = QLineEdit(self)
|
||||
self.findField.setToolTip(findtooltip)
|
||||
self.findField.returnPressed.connect(self.findButton.setFocus)
|
||||
|
||||
|
||||
# Case Sensitivity option
|
||||
self.caseSens = QtGui.QCheckBox(_('Case sensitive'),self)
|
||||
self.caseSens.setToolTip(_("Search for case sensitive string; don't treat Harry, HARRY and harry all the same."))
|
||||
|
||||
|
||||
horz.addWidget(label)
|
||||
horz.addWidget(self.findField)
|
||||
horz.addWidget(self.findButton)
|
||||
horz.addWidget(self.caseSens)
|
||||
|
||||
|
||||
self.l.addLayout(horz)
|
||||
|
||||
self.addCtrlKeyPress(QtCore.Qt.Key_F,self.findFocus)
|
||||
self.addCtrlKeyPress(QtCore.Qt.Key_G,self.find)
|
||||
|
||||
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
button_box.accepted.connect(self.accept)
|
||||
button_box.rejected.connect(self.reject)
|
||||
self.l.addWidget(button_box)
|
||||
|
||||
|
||||
# Cause our dialog size to be restored from prefs or created on first usage
|
||||
self.resize_dialog()
|
||||
|
||||
@@ -1311,9 +1314,9 @@ class IniTextDialog(SizePersistedDialog):
|
||||
_('Go back to fix errors?'),
|
||||
errors)
|
||||
retry = d.exec_() == d.Accepted
|
||||
|
||||
|
||||
# print("retry:%s"%retry)
|
||||
|
||||
|
||||
if retry:
|
||||
lineno=d.get_lineno()
|
||||
if lineno:
|
||||
@@ -1335,19 +1338,19 @@ class IniTextDialog(SizePersistedDialog):
|
||||
return func()
|
||||
else:
|
||||
return SizePersistedDialog.keyPressEvent(self, event)
|
||||
|
||||
|
||||
def get_plain_text(self):
|
||||
return unicode(self.textedit.toPlainText())
|
||||
|
||||
def findFocus(self):
|
||||
# print("findFocus called")
|
||||
self.findField.setFocus()
|
||||
self.findField.selectAll()
|
||||
|
||||
self.findField.selectAll()
|
||||
|
||||
def find(self):
|
||||
|
||||
#print("find self.lastStart:%s"%self.lastStart)
|
||||
|
||||
|
||||
# Grab the parent's text
|
||||
text = self.textedit.toPlainText()
|
||||
|
||||
@@ -1362,7 +1365,7 @@ class IniTextDialog(SizePersistedDialog):
|
||||
# last starting position
|
||||
self.lastStart = text.find(query,self.lastStart + 1)
|
||||
# If the find() method didn't return -1 (not found)
|
||||
|
||||
|
||||
if self.lastStart >= 0:
|
||||
end = self.lastStart + len(query)
|
||||
self.moveCursor(self.lastStart,end)
|
||||
@@ -1370,7 +1373,7 @@ class IniTextDialog(SizePersistedDialog):
|
||||
# Make the next search start from the begining again
|
||||
self.lastStart = 0
|
||||
self.textedit.moveCursor(self.textedit.textCursor().Start)
|
||||
|
||||
|
||||
def moveCursor(self,start,end):
|
||||
|
||||
# We retrieve the QTextCursor object from the parent's QTextEdit
|
||||
@@ -1387,7 +1390,7 @@ class IniTextDialog(SizePersistedDialog):
|
||||
self.textedit.setTextCursor(cursor)
|
||||
|
||||
def select_line(self,lineno):
|
||||
|
||||
|
||||
# We retrieve the QTextCursor object from the parent's QTextEdit
|
||||
cursor = self.textedit.textCursor()
|
||||
|
||||
@@ -1413,8 +1416,8 @@ class ViewLog(SizePersistedDialog):
|
||||
|
||||
def get_lineno(self):
|
||||
return self.lineno
|
||||
|
||||
def __init__(self, parent, title, errors,
|
||||
|
||||
def __init__(self, parent, title, errors,
|
||||
save_size_name='fff:view log dialog',):
|
||||
SizePersistedDialog.__init__(self, parent,save_size_name)
|
||||
self.l = l = QVBoxLayout()
|
||||
@@ -1435,9 +1438,9 @@ class ViewLog(SizePersistedDialog):
|
||||
label.setToolTip(_('Click to go to line %s')%lineno)
|
||||
label.mouseReleaseEvent = partial(self.label_clicked, lineno=lineno)
|
||||
self.l.addWidget(label)
|
||||
|
||||
|
||||
# html='<p>'+'</p><p>'.join([ '(lineno: %s) %s'%e for e in errors ])+'</p>'
|
||||
|
||||
|
||||
# self.tb = QTextBrowser(self)
|
||||
# self.tb.setFont(QFont("Courier",
|
||||
# parent.font().pointSize()+1))
|
||||
@@ -1455,13 +1458,13 @@ class ViewLog(SizePersistedDialog):
|
||||
saveanyway = QPushButton(_('Save Anyway'), self)
|
||||
saveanyway.clicked.connect(self.reject)
|
||||
horz.addWidget(saveanyway)
|
||||
|
||||
|
||||
l.addLayout(horz)
|
||||
self.setModal(False)
|
||||
self.setWindowTitle(title)
|
||||
self.setWindowIcon(QIcon(I('debug.png')))
|
||||
#self.show()
|
||||
|
||||
|
||||
# Cause our dialog size to be restored from prefs or created on first usage
|
||||
self.resize_dialog()
|
||||
|
||||
@@ -1482,7 +1485,7 @@ class EmailPassDialog(QDialog):
|
||||
|
||||
self.setWindowTitle(_('Password'))
|
||||
self.l.addWidget(QLabel(_("Enter Email Password for %s:")%user),0,0,1,2)
|
||||
|
||||
|
||||
# self.l.addWidget(QLabel(_("Password:")),1,0)
|
||||
self.passwd = QLineEdit(self)
|
||||
self.passwd.setEchoMode(QLineEdit.Password)
|
||||
|
||||
@@ -454,6 +454,10 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
imap_pass,
|
||||
prefs['imapfolder'],
|
||||
prefs['imapmarkread'],)
|
||||
|
||||
## reject will now be redundant with reject check inside
|
||||
## prep_downloads because of change-able story URLs.
|
||||
## Keeping here to because it's the far more common case.
|
||||
reject_list=set()
|
||||
if prefs['auto_reject_from_email']:
|
||||
# need to normalize for reject list.
|
||||
@@ -931,6 +935,43 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
# LoopProgressDialog calls start_download_job at the end which goes
|
||||
# into the BG, or shows list if no 'good' books.
|
||||
|
||||
def reject_url(self,merge,book):
|
||||
url = book['url']
|
||||
if not merge and rejecturllist.check(url): # skip reject list when merging.
|
||||
rejnote = rejecturllist.get_full_note(url)
|
||||
if prefs['reject_always'] or question_dialog(self.gui, _('Reject URL?'),'''
|
||||
<h3>%s</h3>
|
||||
<p>%s</p>
|
||||
<p>"<b>%s</b>"</p>
|
||||
<p>%s</p>
|
||||
<p>%s</p>'''%(
|
||||
_('Reject URL?'),
|
||||
_('<b>%s</b> is on your Reject URL list:')%url,
|
||||
rejnote,
|
||||
_("Click '<b>Yes</b>' to Reject."),
|
||||
_("Click '<b>No</b>' to download anyway.")),
|
||||
show_copy_button=False):
|
||||
book['comment'] = _("Story on Reject URLs list (%s).")%rejnote
|
||||
book['good']=False
|
||||
book['icon']='rotate-right.png'
|
||||
book['status'] = _('Rejected')
|
||||
return True
|
||||
else:
|
||||
if question_dialog(self.gui, _('Remove Reject URL?'),'''
|
||||
<h3>%s</h3>
|
||||
<p>%s</p>
|
||||
<p>"<b>%s</b>"</p>
|
||||
<p>%s</p>
|
||||
<p>%s</p>'''%(
|
||||
_("Remove URL from Reject List?"),
|
||||
_('<b>%s</b> is on your Reject URL list:')%url,
|
||||
rejnote,
|
||||
_("Click '<b>Yes</b>' to remove it from the list,"),
|
||||
_("Click '<b>No</b>' to leave it on the list.")),
|
||||
show_copy_button=False):
|
||||
rejecturllist.remove(url)
|
||||
return False
|
||||
|
||||
def prep_download_loop(self,book,
|
||||
options={'fileform':'epub',
|
||||
'collision':ADDNEW,
|
||||
@@ -947,40 +988,11 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
logger.debug("url:%s"%url)
|
||||
mi = None
|
||||
|
||||
if not merge: # skip reject list when merging.
|
||||
if rejecturllist.check(url):
|
||||
rejnote = rejecturllist.get_full_note(url)
|
||||
if prefs['reject_always'] or question_dialog(self.gui, _('Reject URL?'),'''
|
||||
<h3>%s</h3>
|
||||
<p>%s</p>
|
||||
<p>"<b>%s</b>"</p>
|
||||
<p>%s</p>
|
||||
<p>%s</p>'''%(
|
||||
_('Reject URL?'),
|
||||
_('<b>%s</b> is on your Reject URL list:')%url,
|
||||
rejnote,
|
||||
_("Click '<b>Yes</b>' to Reject."),
|
||||
_("Click '<b>No</b>' to download anyway.")),
|
||||
show_copy_button=False):
|
||||
book['comment'] = _("Story on Reject URLs list (%s).")%rejnote
|
||||
book['good']=False
|
||||
book['icon']='rotate-right.png'
|
||||
book['status'] = _('Rejected')
|
||||
return
|
||||
else:
|
||||
if question_dialog(self.gui, _('Remove Reject URL?'),'''
|
||||
<h3>%s</h3>
|
||||
<p>%s</p>
|
||||
<p>"<b>%s</b>"</p>
|
||||
<p>%s</p>
|
||||
<p>%s</p>'''%(
|
||||
_("Remove URL from Reject List?"),
|
||||
_('<b>%s</b> is on your Reject URL list:')%url,
|
||||
rejnote,
|
||||
_("Click '<b>Yes</b>' to remove it from the list,"),
|
||||
_("Click '<b>No</b>' to leave it on the list.")),
|
||||
show_copy_button=False):
|
||||
rejecturllist.remove(url)
|
||||
## Check reject list. Redundant with below for when story URL
|
||||
## changes, but also kept here to avoid network hit in most
|
||||
## common case where given url is story url.
|
||||
if self.reject_url(merge,book):
|
||||
return
|
||||
|
||||
# The current database shown in the GUI
|
||||
# db is an instance of the class LibraryDatabase2 from database.py
|
||||
@@ -1074,7 +1086,16 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
|
||||
# let other exceptions percolate up.
|
||||
story = adapter.getStoryMetadataOnly(get_cover=False)
|
||||
book['title'] = story.getMetadata('title')
|
||||
book['author'] = [story.getMetadata('author')]
|
||||
book['url'] = story.getMetadata('storyUrl')
|
||||
|
||||
## Check reject list. Redundant with below for when story
|
||||
## URL changes, but also kept here to avoid network hit in
|
||||
## most common case where given url is story url.
|
||||
if self.reject_url(merge,book):
|
||||
return
|
||||
|
||||
series = story.getMetadata('series')
|
||||
if not merge and series and prefs['checkforseriesurlid']:
|
||||
# try to find *series anthology* by *seriesUrl* identifier url or uri first.
|
||||
@@ -1094,6 +1115,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
book['comment'] = _("Story in Series Anthology(%s).")%series
|
||||
book['title'] = story.getMetadata('title')
|
||||
book['author'] = [story.getMetadata('author')]
|
||||
book['url'] = story.getMetadata('storyUrl')
|
||||
book['good']=False
|
||||
book['icon']='rotate-right.png'
|
||||
book['status'] = _('Skipped')
|
||||
@@ -1114,6 +1136,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
book['title'] = story.getMetadata("title", removeallentities=True)
|
||||
book['author_sort'] = book['author'] = story.getList("author", removeallentities=True)
|
||||
book['publisher'] = story.getMetadata("site")
|
||||
book['url'] = story.getMetadata("storyUrl")
|
||||
book['tags'] = story.getSubjectTags(removeallentities=True)
|
||||
if story.getMetadata("description"):
|
||||
book['comments'] = sanitize_comments_html(story.getMetadata("description"))
|
||||
@@ -2134,6 +2157,7 @@ class FanFicFarePlugin(InterfaceAction):
|
||||
book['site'] = ''
|
||||
book['added'] = False
|
||||
book['pubdate'] = None
|
||||
book['publisher'] = None
|
||||
return book
|
||||
|
||||
def convert_urls_to_books(self, urls):
|
||||
|
||||
@@ -376,13 +376,14 @@ cover_exclusion_regexp:/clear.png
|
||||
strip_chapter_numbers:false
|
||||
|
||||
## Copy title to tagsfromtitle for parsing tags.
|
||||
add_to_extra_valid_entries:,tagsfromtitle
|
||||
add_to_extra_valid_entries:,tagsfromtitle,forumtags
|
||||
|
||||
## '.NOREPL' tells the system to *not* apply title's
|
||||
## in/exclude/replace_metadata -- Only works on include_in_ lines.
|
||||
include_in_tagsfromtitle:title.NOREPL
|
||||
|
||||
tagsfromtitle_label:Tags from Title
|
||||
forumtags_label:Tags from Forum
|
||||
|
||||
## might want to do this, maybe not. Will often include category, but
|
||||
## also often include non-category stuff.
|
||||
@@ -399,18 +400,20 @@ add_to_replace_metadata:
|
||||
tagsfromtitle=>[\(\)\[\]]=>
|
||||
# change (spaces)slash(or semicolon)(spaces) to comma
|
||||
tagsfromtitle=> *[/;] *=>,
|
||||
tagsfromtitle=> x =>,
|
||||
tagsfromtitle=> [xX] =>,
|
||||
|
||||
# remove [] or () blocks and leading/trailing spaces/dashes/colons
|
||||
title=>[-: ]*[\(\[]([^\]\)]+)[\)\]][-: ]*=>
|
||||
# remove 'Thread' and the next word, usually "Thread 2", "Thread
|
||||
# four", "Thread iv", etc
|
||||
title=>[-: ]*[Tt]hread [^ ]+[-: ]*=>
|
||||
title,tagsfromtitle=>[-: ]*[Tt]hread [^ ]+[-: ]*=>
|
||||
|
||||
add_to_extra_titlepage_entries:,tagsfromtitle
|
||||
add_to_extra_titlepage_entries:,tagsfromtitle,forumtags
|
||||
|
||||
## '.SPLIT' tells the system to split by ','
|
||||
add_to_include_subject_tags:,tagsfromtitle.SPLIT
|
||||
## Or to also include forumtags add this to personal.ini:
|
||||
#add_to_include_subject_tags:,forumtags
|
||||
|
||||
## base_xenforoforum reads Published and Updated datetimes from
|
||||
## Threadmarks if used, or from the posted & updated times of the
|
||||
@@ -1871,6 +1874,15 @@ rating_titles: R=RESTRICTED (16+), E=EXEMPT (18+), I=ART HOUSE, T=To every, A=IN
|
||||
adult_ratings: E,R
|
||||
|
||||
[www.mediaminer.org]
|
||||
dateUpdated_format:%%Y-%%m-%%d %%H:%%M:%%S
|
||||
## Note that mediaminer doesn't give datePublished on the story's
|
||||
## index page--it's collected from the earliest uploaded chapter. So
|
||||
## it's not available when only fetching metadata.
|
||||
datePublished_format:%%Y-%%m-%%d %%H:%%M:%%S
|
||||
|
||||
## some sites include images that we don't ever want becoming the
|
||||
## cover image. This lets you exclude them.
|
||||
cover_exclusion_regexp:/img/rss.png
|
||||
|
||||
[www.midnightwhispers.ca]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
@@ -1938,6 +1950,13 @@ extracategories:Psych
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Queer as Folk
|
||||
|
||||
[quotev.com]
|
||||
extra_valid_entries:pages,readers,reads,favorites
|
||||
pages_label:Pages
|
||||
readers_label:Readers
|
||||
reads_label:Reads
|
||||
favorites_label:Favorites
|
||||
|
||||
[www.restrictedsection.org]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
|
||||
@@ -13,7 +13,7 @@ msgstr ""
|
||||
"POT-Creation-Date: 2015-07-27 12:59+Central Daylight Time\n"
|
||||
"PO-Revision-Date: 2015-07-30 08:46+0000\n"
|
||||
"Last-Translator: Kovid Goyal <kovid@kovidgoyal.net>\n"
|
||||
"Language-Team: German (http://www.transifex.com/projects/p/calibre-plugins/language/de/)\n"
|
||||
"Language-Team: German (http://www.transifex.com/calibre/calibre-plugins/language/de/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: ENCODING\n"
|
||||
|
||||
@@ -12,7 +12,7 @@ msgstr ""
|
||||
"POT-Creation-Date: 2015-07-27 12:59+Central Daylight Time\n"
|
||||
"PO-Revision-Date: 2015-07-30 09:06+0000\n"
|
||||
"Last-Translator: Jellby <jellby@yahoo.com>\n"
|
||||
"Language-Team: Spanish (http://www.transifex.com/projects/p/calibre-plugins/language/es/)\n"
|
||||
"Language-Team: Spanish (http://www.transifex.com/calibre/calibre-plugins/language/es/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: ENCODING\n"
|
||||
|
||||
@@ -5,14 +5,16 @@
|
||||
# Xotes <alois.glibert@gmail.com>, 2015
|
||||
# Franck, 2015
|
||||
# Ptitprince <leporello1791@gmail.com>, 2014-2015
|
||||
# Piconcely Yoann <yoanncoolazz@gmail.com>, 2015
|
||||
# sengian <sengian1@gmail.com>, 2015
|
||||
# Thibault San <diablodu02@gmail.com>, 2015
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: calibre-plugins\n"
|
||||
"POT-Creation-Date: 2015-07-27 12:59+Central Daylight Time\n"
|
||||
"PO-Revision-Date: 2015-07-30 08:58+0000\n"
|
||||
"Last-Translator: Ptitprince <leporello1791@gmail.com>\n"
|
||||
"Language-Team: French (http://www.transifex.com/projects/p/calibre-plugins/language/fr/)\n"
|
||||
"PO-Revision-Date: 2015-08-11 08:48+0000\n"
|
||||
"Last-Translator: Piconcely Yoann <yoanncoolazz@gmail.com>\n"
|
||||
"Language-Team: French (http://www.transifex.com/calibre/calibre-plugins/language/fr/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: ENCODING\n"
|
||||
@@ -1556,19 +1558,19 @@ msgstr "Pas d'URLs de récit trouvée dans les livres sélectionnés"
|
||||
|
||||
#: fff_plugin.py:578
|
||||
msgid "Can only UnNew books in library"
|
||||
msgstr ""
|
||||
msgstr "Seuls les livres dans la bibliothèque peuvent être marqués vu"
|
||||
|
||||
#: fff_plugin.py:595
|
||||
msgid "UnNewing books..."
|
||||
msgstr ""
|
||||
msgstr "Anciens livres..."
|
||||
|
||||
#: fff_plugin.py:596
|
||||
msgid "UnNew Books"
|
||||
msgstr ""
|
||||
msgstr "Anciens livres..."
|
||||
|
||||
#: fff_plugin.py:597
|
||||
msgid "Books UnNewed"
|
||||
msgstr ""
|
||||
msgstr "Livre non vu"
|
||||
|
||||
#: fff_plugin.py:636 fff_plugin.py:1541
|
||||
msgid "Starting auto conversion of %d books."
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"POT-Creation-Date: 2015-07-27 12:59+Central Daylight Time\n"
|
||||
"POT-Creation-Date: 2015-09-14 10:18+Central Daylight Time\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -390,7 +390,7 @@ msgid ""
|
||||
"<b>http://...,title by author - note</b>"
|
||||
msgstr ""
|
||||
|
||||
#: config.py:616 dialogs.py:1095
|
||||
#: config.py:616 dialogs.py:1098
|
||||
msgid "Add this reason to all URLs added:"
|
||||
msgstr ""
|
||||
|
||||
@@ -654,7 +654,7 @@ msgstr ""
|
||||
msgid "Language"
|
||||
msgstr ""
|
||||
|
||||
#: config.py:1151 fff_plugin.py:1384 fff_plugin.py:1582 fff_plugin.py:1612
|
||||
#: config.py:1151 fff_plugin.py:1409 fff_plugin.py:1607 fff_plugin.py:1637
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
@@ -682,7 +682,7 @@ msgstr ""
|
||||
msgid "Published"
|
||||
msgstr ""
|
||||
|
||||
#: config.py:1158 fff_plugin.py:1695 fff_plugin.py:1714
|
||||
#: config.py:1158 fff_plugin.py:1720 fff_plugin.py:1739
|
||||
msgid "Updated"
|
||||
msgstr ""
|
||||
|
||||
@@ -722,8 +722,8 @@ msgstr ""
|
||||
msgid "Extra Tags"
|
||||
msgstr ""
|
||||
|
||||
#: config.py:1168 config.py:1294 dialogs.py:886 dialogs.py:982
|
||||
#: fff_plugin.py:1384 fff_plugin.py:1582 fff_plugin.py:1612
|
||||
#: config.py:1168 config.py:1294 dialogs.py:889 dialogs.py:985
|
||||
#: fff_plugin.py:1409 fff_plugin.py:1607 fff_plugin.py:1637
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
|
||||
@@ -735,8 +735,8 @@ msgstr ""
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
#: config.py:1171 dialogs.py:886 dialogs.py:982 fff_plugin.py:1384
|
||||
#: fff_plugin.py:1582 fff_plugin.py:1612
|
||||
#: config.py:1171 dialogs.py:889 dialogs.py:985 fff_plugin.py:1409
|
||||
#: fff_plugin.py:1607 fff_plugin.py:1637
|
||||
msgid "Author"
|
||||
msgstr ""
|
||||
|
||||
@@ -976,33 +976,33 @@ msgstr ""
|
||||
msgid "Update Calibre Metadata from Saved Metadata Column"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:268 dialogs.py:777
|
||||
#: dialogs.py:268 dialogs.py:780
|
||||
msgid "Show Download Options"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:287 dialogs.py:794
|
||||
#: dialogs.py:287 dialogs.py:797
|
||||
msgid "Output &Format:"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:295 dialogs.py:802
|
||||
#: dialogs.py:295 dialogs.py:805
|
||||
msgid "Choose output format to create. May set default from plugin configuration."
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:323 dialogs.py:819
|
||||
#: dialogs.py:323 dialogs.py:822
|
||||
msgid "Update Calibre &Metadata?"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:324 dialogs.py:820
|
||||
#: dialogs.py:324 dialogs.py:823
|
||||
msgid ""
|
||||
"Update metadata for existing stories in Calibre from web site?\n"
|
||||
"(Columns set to 'New Only' in the column tabs will only be set for new books.)"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:330 dialogs.py:824
|
||||
#: dialogs.py:330 dialogs.py:827
|
||||
msgid "Update EPUB Cover?"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:331 dialogs.py:825
|
||||
#: dialogs.py:331 dialogs.py:828
|
||||
msgid "Update book cover image from site or defaults (if found) <i>inside</i> the EPUB when EPUB is updated."
|
||||
msgstr ""
|
||||
|
||||
@@ -1061,11 +1061,11 @@ msgid ""
|
||||
"Requires %s plugin."
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:524 dialogs.py:578 dialogs.py:605 dialogs.py:1495
|
||||
#: dialogs.py:524 dialogs.py:578 dialogs.py:605 dialogs.py:1498
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:556 dialogs.py:1483
|
||||
#: dialogs.py:556 dialogs.py:1486
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
@@ -1089,7 +1089,7 @@ msgstr ""
|
||||
msgid "Password:"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:574 dialogs.py:719 dialogs.py:1491
|
||||
#: dialogs.py:574 dialogs.py:722 dialogs.py:1494
|
||||
msgid "OK"
|
||||
msgstr ""
|
||||
|
||||
@@ -1105,155 +1105,155 @@ msgstr ""
|
||||
msgid "Fetched metadata for"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:629
|
||||
#: dialogs.py:632
|
||||
msgid " - %s estimated until done"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:679
|
||||
#: dialogs.py:682
|
||||
msgid "%d day"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:679
|
||||
#: dialogs.py:682
|
||||
msgid "%d days"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:680
|
||||
#: dialogs.py:683
|
||||
msgid "%d hour"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:680
|
||||
#: dialogs.py:683
|
||||
msgid "%d hours"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:681
|
||||
#: dialogs.py:684
|
||||
msgid "%d minute"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:681
|
||||
#: dialogs.py:684
|
||||
msgid "%d minutes"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:682
|
||||
#: dialogs.py:685
|
||||
msgid "%d second"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:682
|
||||
#: dialogs.py:685
|
||||
msgid "%d seconds"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:697
|
||||
#: dialogs.py:700
|
||||
msgid "less than 1 second"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:714 fff_plugin.py:365 fff_plugin.py:368
|
||||
#: dialogs.py:717 fff_plugin.py:365 fff_plugin.py:368
|
||||
msgid "About FanFicFare"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:768
|
||||
#: dialogs.py:771
|
||||
msgid "Remove selected books from the list"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:807
|
||||
#: dialogs.py:810
|
||||
msgid "Update Mode:"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:810
|
||||
#: dialogs.py:813
|
||||
msgid "What sort of update to perform. May set default from plugin configuration."
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:886 fff_plugin.py:1384 fff_plugin.py:1582 fff_plugin.py:1612
|
||||
#: dialogs.py:889 fff_plugin.py:1409 fff_plugin.py:1607 fff_plugin.py:1637
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:954
|
||||
#: dialogs.py:957
|
||||
msgid "Are you sure you want to remove this book from the list?"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:956
|
||||
#: dialogs.py:959
|
||||
msgid "Are you sure you want to remove the selected %d books from the list?"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:982
|
||||
#: dialogs.py:985
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:1021
|
||||
#: dialogs.py:1024
|
||||
msgid "Select or Edit Reject Note."
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:1030
|
||||
#: dialogs.py:1033
|
||||
msgid "Are you sure you want to remove this URL from the list?"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:1032
|
||||
#: dialogs.py:1035
|
||||
msgid "Are you sure you want to remove the %d selected URLs from the list?"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:1050
|
||||
#: dialogs.py:1053
|
||||
msgid "List of Books to Reject"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:1063
|
||||
#: dialogs.py:1066
|
||||
msgid "FFF will remember these URLs and display the note and offer to reject them if you try to download them again later."
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:1077
|
||||
#: dialogs.py:1080
|
||||
msgid "Remove selected URLs from the list"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:1092 dialogs.py:1096
|
||||
#: dialogs.py:1095 dialogs.py:1099
|
||||
msgid "This will be added to whatever note you've set for each URL above."
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:1106
|
||||
#: dialogs.py:1109
|
||||
msgid "Delete Books (including books without FanFiction URLs)?"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:1107
|
||||
#: dialogs.py:1110
|
||||
msgid "Delete the selected books after adding them to the Rejected URLs list."
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:1263
|
||||
#: dialogs.py:1266
|
||||
msgid "Search for string in edit box."
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:1266
|
||||
#: dialogs.py:1269
|
||||
msgid "Find:"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:1271
|
||||
#: dialogs.py:1274
|
||||
msgid "Find"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:1281
|
||||
#: dialogs.py:1284
|
||||
msgid "Case sensitive"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:1282
|
||||
#: dialogs.py:1285
|
||||
msgid "Search for case sensitive string; don't treat Harry, HARRY and harry all the same."
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:1311
|
||||
#: dialogs.py:1314
|
||||
msgid "Go back to fix errors?"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:1423
|
||||
#: dialogs.py:1426
|
||||
msgid "Click an error below to return to Editing directly on that line:"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:1435
|
||||
#: dialogs.py:1438
|
||||
msgid "Click to go to line %s"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:1451
|
||||
#: dialogs.py:1454
|
||||
msgid "Return to Editing"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:1455
|
||||
#: dialogs.py:1458
|
||||
msgid "Save Anyway"
|
||||
msgstr ""
|
||||
|
||||
#: dialogs.py:1484
|
||||
#: dialogs.py:1487
|
||||
msgid "Enter Email Password for %s:"
|
||||
msgstr ""
|
||||
|
||||
@@ -1281,7 +1281,7 @@ msgstr ""
|
||||
msgid "Get Story URLs from &Email"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:291 fff_plugin.py:495
|
||||
#: fff_plugin.py:291 fff_plugin.py:499
|
||||
msgid "Get Story URLs from Web Page"
|
||||
msgstr ""
|
||||
|
||||
@@ -1365,515 +1365,515 @@ msgstr ""
|
||||
msgid "Fetching Story URLs from Email..."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:463 fff_plugin.py:478
|
||||
#: fff_plugin.py:467 fff_plugin.py:482
|
||||
msgid "No Valid Story URLs Found in Unread Emails."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:472
|
||||
#: fff_plugin.py:476
|
||||
msgid "Finished Fetching Story URLs from Email."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:480
|
||||
#: fff_plugin.py:484
|
||||
msgid "(%d Story URLs Skipped, on Rejected URL List)"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:481
|
||||
#: fff_plugin.py:485
|
||||
msgid "Get Story URLs from Email"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:504
|
||||
#: fff_plugin.py:508
|
||||
msgid "Fetching Story URLs from Page..."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:508
|
||||
#: fff_plugin.py:512
|
||||
msgid "Finished Fetching Story URLs from Page."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:514 fff_plugin.py:566
|
||||
#: fff_plugin.py:518 fff_plugin.py:570
|
||||
msgid "List of Story URLs"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:515
|
||||
#: fff_plugin.py:519
|
||||
msgid "No Valid Story URLs found on given page."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:530 fff_plugin.py:583
|
||||
#: fff_plugin.py:534 fff_plugin.py:587
|
||||
msgid "No Selected Books to Get URLs From"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:548
|
||||
#: fff_plugin.py:552
|
||||
msgid "Collecting URLs for stories..."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:549
|
||||
#: fff_plugin.py:553
|
||||
msgid "Get URLs for stories"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:550 fff_plugin.py:661 fff_plugin.py:854
|
||||
#: fff_plugin.py:554 fff_plugin.py:665 fff_plugin.py:858
|
||||
msgid "URL retrieved"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:570
|
||||
#: fff_plugin.py:574
|
||||
msgid "List of URLs"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:571
|
||||
#: fff_plugin.py:575
|
||||
msgid "No Story URLs found in selected books."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:578
|
||||
#: fff_plugin.py:582
|
||||
msgid "Can only UnNew books in library"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:595
|
||||
#: fff_plugin.py:599
|
||||
msgid "UnNewing books..."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:596
|
||||
#: fff_plugin.py:600
|
||||
msgid "UnNew Books"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:597
|
||||
#: fff_plugin.py:601
|
||||
msgid "Books UnNewed"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:636 fff_plugin.py:1541
|
||||
#: fff_plugin.py:640 fff_plugin.py:1566
|
||||
msgid "Starting auto conversion of %d books."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:651
|
||||
#: fff_plugin.py:655
|
||||
msgid "No Selected Books have URLs to Reject"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:659
|
||||
#: fff_plugin.py:663
|
||||
msgid "Collecting URLs for Reject List..."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:660
|
||||
#: fff_plugin.py:664
|
||||
msgid "Get URLs for Reject List"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:695
|
||||
#: fff_plugin.py:699
|
||||
msgid "Proceed to Remove?"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:695
|
||||
#: fff_plugin.py:699
|
||||
msgid "Rejecting FanFicFare URLs: None of the books selected have FanFiction URLs."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:717
|
||||
#: fff_plugin.py:721
|
||||
msgid "Cannot Make Anthologys without %s"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:721 fff_plugin.py:831
|
||||
#: fff_plugin.py:725 fff_plugin.py:835
|
||||
msgid "Cannot Update Books from Device View"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:725
|
||||
#: fff_plugin.py:729
|
||||
msgid "Can only update 1 anthology at a time"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:734
|
||||
#: fff_plugin.py:738
|
||||
msgid "Can only Update Epub Anthologies"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:752 fff_plugin.py:753
|
||||
#: fff_plugin.py:756 fff_plugin.py:757
|
||||
msgid "Cannot Update Anthology"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:753
|
||||
#: fff_plugin.py:757
|
||||
msgid "Book isn't an FanFicFare Anthology or contains book(s) without valid Story URLs."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:760
|
||||
#: fff_plugin.py:764
|
||||
msgid "Fetching Story URLs for Series..."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:770
|
||||
#: fff_plugin.py:774
|
||||
msgid "Finished Fetching Story URLs for Series."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:817
|
||||
#: fff_plugin.py:821
|
||||
msgid "There are %d stories in the current anthology that are <b>not</b> going to be kept if you go ahead."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:818
|
||||
#: fff_plugin.py:822
|
||||
msgid "Story URLs that will be removed:"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:820
|
||||
#: fff_plugin.py:824
|
||||
msgid "Update anyway?"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:821
|
||||
#: fff_plugin.py:825
|
||||
msgid "Stories Removed"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:838
|
||||
#: fff_plugin.py:842
|
||||
msgid "No Selected Books to Update"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:852
|
||||
#: fff_plugin.py:856
|
||||
msgid "Collecting stories for update..."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:853
|
||||
#: fff_plugin.py:857
|
||||
msgid "Get stories for updates"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:863
|
||||
#: fff_plugin.py:867
|
||||
msgid "Update Existing List"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:922
|
||||
#: fff_plugin.py:926
|
||||
msgid "Started fetching metadata for %s stories."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:928
|
||||
#: fff_plugin.py:932
|
||||
msgid "No valid story URLs entered."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:953 fff_plugin.py:959
|
||||
#: fff_plugin.py:942 fff_plugin.py:948
|
||||
msgid "Reject URL?"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:960 fff_plugin.py:978
|
||||
#: fff_plugin.py:949 fff_plugin.py:967
|
||||
msgid "<b>%s</b> is on your Reject URL list:"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:962
|
||||
#: fff_plugin.py:951
|
||||
msgid "Click '<b>Yes</b>' to Reject."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:963 fff_plugin.py:1090
|
||||
#: fff_plugin.py:952 fff_plugin.py:1113
|
||||
msgid "Click '<b>No</b>' to download anyway."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:965
|
||||
#: fff_plugin.py:954
|
||||
msgid "Story on Reject URLs list (%s)."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:968
|
||||
#: fff_plugin.py:957
|
||||
msgid "Rejected"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:971
|
||||
#: fff_plugin.py:960
|
||||
msgid "Remove Reject URL?"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:977
|
||||
#: fff_plugin.py:966
|
||||
msgid "Remove URL from Reject List?"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:980
|
||||
#: fff_plugin.py:969
|
||||
msgid "Click '<b>Yes</b>' to remove it from the list,"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:981
|
||||
#: fff_plugin.py:970
|
||||
msgid "Click '<b>No</b>' to leave it on the list."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:998
|
||||
#: fff_plugin.py:1010
|
||||
msgid "Cannot update non-epub format."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1068
|
||||
#: fff_plugin.py:1082
|
||||
msgid "Are You an Adult?"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1069
|
||||
#: fff_plugin.py:1083
|
||||
msgid "%s requires that you be an adult. Please confirm you are an adult in your locale:"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1081
|
||||
#: fff_plugin.py:1104
|
||||
msgid "Skip Story?"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1087
|
||||
#: fff_plugin.py:1110
|
||||
msgid "Skip Anthology Story?"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1088
|
||||
#: fff_plugin.py:1111
|
||||
msgid "\"<b>%s</b>\" is in series \"<b><a href=\"%s\">%s</a></b>\" that you have an anthology book for."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1089
|
||||
#: fff_plugin.py:1112
|
||||
msgid "Click '<b>Yes</b>' to Skip."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1092
|
||||
#: fff_plugin.py:1115
|
||||
msgid "Story in Series Anthology(%s)."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1097
|
||||
#: fff_plugin.py:1121
|
||||
msgid "Skipped"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1127
|
||||
#: fff_plugin.py:1152
|
||||
msgid "Add"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1140
|
||||
#: fff_plugin.py:1165
|
||||
msgid "Meta"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1171
|
||||
#: fff_plugin.py:1196
|
||||
msgid "Skipping duplicate story."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1174
|
||||
#: fff_plugin.py:1199
|
||||
msgid "More than one identical book by Identifer URL or title/author(s)--can't tell which book to update/overwrite."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1185
|
||||
#: fff_plugin.py:1210
|
||||
msgid "Update"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1193 fff_plugin.py:1200
|
||||
#: fff_plugin.py:1218 fff_plugin.py:1225
|
||||
msgid "Change Story URL?"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1201
|
||||
#: fff_plugin.py:1226
|
||||
msgid "<b>%s</b> by <b>%s</b> is already in your library with a different source URL:"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1202
|
||||
#: fff_plugin.py:1227
|
||||
msgid "In library: <a href=\"%(liburl)s\">%(liburl)s</a>"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1203 fff_plugin.py:1217
|
||||
#: fff_plugin.py:1228 fff_plugin.py:1242
|
||||
msgid "New URL: <a href=\"%(newurl)s\">%(newurl)s</a>"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1204
|
||||
#: fff_plugin.py:1229
|
||||
msgid "Click '<b>Yes</b>' to update/overwrite book with new URL."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1205
|
||||
#: fff_plugin.py:1230
|
||||
msgid "Click '<b>No</b>' to skip updating/overwriting this book."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1207 fff_plugin.py:1214
|
||||
#: fff_plugin.py:1232 fff_plugin.py:1239
|
||||
msgid "Download as New Book?"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1215
|
||||
#: fff_plugin.py:1240
|
||||
msgid "<b>%s</b> by <b>%s</b> is already in your library with a different source URL."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1216
|
||||
#: fff_plugin.py:1241
|
||||
msgid "You chose not to update the existing book. Do you want to add a new book for this URL?"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1218
|
||||
#: fff_plugin.py:1243
|
||||
msgid "Click '<b>Yes</b>' to a new book with new URL."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1219
|
||||
#: fff_plugin.py:1244
|
||||
msgid "Click '<b>No</b>' to skip URL."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1225
|
||||
#: fff_plugin.py:1250
|
||||
msgid "Update declined by user due to differing story URL(%s)"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1228
|
||||
#: fff_plugin.py:1253
|
||||
msgid "Different URL"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1233
|
||||
#: fff_plugin.py:1258
|
||||
msgid "Metadata collected."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1249
|
||||
#: fff_plugin.py:1274
|
||||
msgid "Already contains %d chapters."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1251 jobs.py:211
|
||||
#: fff_plugin.py:1276 jobs.py:211
|
||||
msgid "Existing epub contains %d chapters, web site only has %d. Use Overwrite to force update."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1253
|
||||
#: fff_plugin.py:1278
|
||||
msgid "FanFicFare doesn't recognize chapters in existing epub, epub is probably from a different source. Use Overwrite to force update."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1265
|
||||
#: fff_plugin.py:1290
|
||||
msgid "Not Overwriting, web site is not newer."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1380
|
||||
#: fff_plugin.py:1405
|
||||
msgid "None of the <b>%d</b> URLs/stories given can be/need to be downloaded."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1381 fff_plugin.py:1578 fff_plugin.py:1608
|
||||
#: fff_plugin.py:1406 fff_plugin.py:1603 fff_plugin.py:1633
|
||||
msgid "See log for details."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1382
|
||||
#: fff_plugin.py:1407
|
||||
msgid "Proceed with updating your library(Error Column, if configured)?"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1389 fff_plugin.py:1590
|
||||
#: fff_plugin.py:1414 fff_plugin.py:1615
|
||||
msgid "Bad"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1397
|
||||
#: fff_plugin.py:1422
|
||||
msgid "FanFicFare download ended"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1397 fff_plugin.py:1633
|
||||
#: fff_plugin.py:1422 fff_plugin.py:1658
|
||||
msgid "FanFicFare log"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1417
|
||||
#: fff_plugin.py:1442
|
||||
msgid "Download FanFiction Book"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1424
|
||||
#: fff_plugin.py:1449
|
||||
msgid "Starting %d FanFicFare Downloads"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1454
|
||||
#: fff_plugin.py:1479
|
||||
msgid "Story Details:"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1457
|
||||
#: fff_plugin.py:1482
|
||||
msgid "Error Updating Metadata"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1458
|
||||
#: fff_plugin.py:1483
|
||||
msgid "An error has occurred while FanFicFare was updating calibre's metadata for <a href='%s'>%s</a>."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1459
|
||||
#: fff_plugin.py:1484
|
||||
msgid "The ebook has been updated, but the metadata has not."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1511
|
||||
#: fff_plugin.py:1536
|
||||
msgid "Finished Adding/Updating %d books."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1562
|
||||
#: fff_plugin.py:1587
|
||||
msgid "No Good Stories for Anthology"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1563
|
||||
#: fff_plugin.py:1588
|
||||
msgid "No good stories/updates where downloaded, Anthology creation/update aborted."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1568 fff_plugin.py:1607
|
||||
#: fff_plugin.py:1593 fff_plugin.py:1632
|
||||
msgid "FanFicFare found <b>%s</b> good and <b>%s</b> bad updates."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1575
|
||||
#: fff_plugin.py:1600
|
||||
msgid "Are you sure you want to continue with creating/updating this Anthology?"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1576
|
||||
#: fff_plugin.py:1601
|
||||
msgid "Any updates that failed will <b>not</b> be included in the Anthology."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1577
|
||||
#: fff_plugin.py:1602
|
||||
msgid "However, if there's an older version, it will still be included."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1580
|
||||
#: fff_plugin.py:1605
|
||||
msgid "Proceed with updating this anthology and your library?"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1588
|
||||
#: fff_plugin.py:1613
|
||||
msgid "Good"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1609
|
||||
#: fff_plugin.py:1634
|
||||
msgid "Proceed with updating your library?"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1633
|
||||
#: fff_plugin.py:1658
|
||||
msgid "FanFicFare download complete"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1646
|
||||
#: fff_plugin.py:1671
|
||||
msgid "Merging %s books."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1686
|
||||
#: fff_plugin.py:1711
|
||||
msgid "FanFicFare Adding/Updating books."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1693
|
||||
#: fff_plugin.py:1718
|
||||
msgid "Updating calibre for FanFiction stories..."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1694
|
||||
#: fff_plugin.py:1719
|
||||
msgid "Update calibre for FanFiction stories"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1703
|
||||
#: fff_plugin.py:1728
|
||||
msgid "Adding/Updating %s BAD books."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1712
|
||||
#: fff_plugin.py:1737
|
||||
msgid "Updating calibre for BAD FanFiction stories..."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1713
|
||||
#: fff_plugin.py:1738
|
||||
msgid "Update calibre for BAD FanFiction stories"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1739
|
||||
#: fff_plugin.py:1764
|
||||
msgid "Adding format to book failed for some reason..."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:1742
|
||||
#: fff_plugin.py:1767
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:2055
|
||||
#: fff_plugin.py:2080
|
||||
msgid "You configured FanFicFare to automatically update Reading Lists, but you don't have the %s plugin installed anymore?"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:2067
|
||||
#: fff_plugin.py:2092
|
||||
msgid "You configured FanFicFare to automatically update \"To Read\" Reading Lists, but you don't have any lists set?"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:2077 fff_plugin.py:2095
|
||||
#: fff_plugin.py:2102 fff_plugin.py:2120
|
||||
msgid "You configured FanFicFare to automatically update Reading List '%s', but you don't have a list of that name?"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:2083
|
||||
#: fff_plugin.py:2108
|
||||
msgid "You configured FanFicFare to automatically update \"Send to Device\" Reading Lists, but you don't have any lists set?"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:2204
|
||||
#: fff_plugin.py:2230
|
||||
msgid "No story URL found."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:2207
|
||||
#: fff_plugin.py:2233
|
||||
msgid "Not Found"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:2213
|
||||
#: fff_plugin.py:2239
|
||||
msgid "URL is not a valid story URL."
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:2216
|
||||
#: fff_plugin.py:2242
|
||||
msgid "Bad URL"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:2355 fff_plugin.py:2358
|
||||
#: fff_plugin.py:2381 fff_plugin.py:2384
|
||||
msgid "Anthology containing:"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:2356
|
||||
#: fff_plugin.py:2382
|
||||
msgid "%s by %s"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:2378
|
||||
#: fff_plugin.py:2404
|
||||
msgid " Anthology"
|
||||
msgstr ""
|
||||
|
||||
#: fff_plugin.py:2421
|
||||
#: fff_plugin.py:2447
|
||||
msgid "(was set, removed for security)"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ msgstr ""
|
||||
"POT-Creation-Date: 2015-07-27 12:59+Central Daylight Time\n"
|
||||
"PO-Revision-Date: 2015-07-30 08:46+0000\n"
|
||||
"Last-Translator: Kovid Goyal <kovid@kovidgoyal.net>\n"
|
||||
"Language-Team: Dutch (http://www.transifex.com/projects/p/calibre-plugins/language/nl/)\n"
|
||||
"Language-Team: Dutch (http://www.transifex.com/calibre/calibre-plugins/language/nl/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: ENCODING\n"
|
||||
|
||||
@@ -10,7 +10,7 @@ msgstr ""
|
||||
"POT-Creation-Date: 2015-07-27 12:59+Central Daylight Time\n"
|
||||
"PO-Revision-Date: 2015-07-30 08:46+0000\n"
|
||||
"Last-Translator: Kovid Goyal <kovid@kovidgoyal.net>\n"
|
||||
"Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/calibre-plugins/language/pt_BR/)\n"
|
||||
"Language-Team: Portuguese (Brazil) (http://www.transifex.com/calibre/calibre-plugins/language/pt_BR/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: ENCODING\n"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Copyright (C) YEAR ORGANIZATION
|
||||
#
|
||||
# Translators:
|
||||
# Dex <dexteritymaster@gmail.com>, 2014-2015
|
||||
# Andrii <dexteritymaster@gmail.com>, 2014-2015
|
||||
# Yuri Chornoivan <yurchor@ukr.net>, 2014
|
||||
msgid ""
|
||||
msgstr ""
|
||||
@@ -10,7 +10,7 @@ msgstr ""
|
||||
"POT-Creation-Date: 2015-07-27 12:59+Central Daylight Time\n"
|
||||
"PO-Revision-Date: 2015-07-30 08:46+0000\n"
|
||||
"Last-Translator: Kovid Goyal <kovid@kovidgoyal.net>\n"
|
||||
"Language-Team: Ukrainian (http://www.transifex.com/projects/p/calibre-plugins/language/uk/)\n"
|
||||
"Language-Team: Ukrainian (http://www.transifex.com/calibre/calibre-plugins/language/uk/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: ENCODING\n"
|
||||
|
||||
@@ -137,8 +137,10 @@ import adapter_tgstorytimecom
|
||||
import adapter_itcouldhappennet
|
||||
import adapter_forumsspacebattlescom
|
||||
import adapter_forumssufficientvelocitycom
|
||||
import adapter_questionablequestingcom
|
||||
import adapter_ninelivesdarksolaceorg
|
||||
import adapter_masseffect2in
|
||||
import adapter_quotevcom
|
||||
|
||||
## This bit of complexity allows adapters to be added by just adding
|
||||
## importing. It eliminates the long if/else clauses we used to need
|
||||
|
||||
@@ -16,321 +16,26 @@
|
||||
#
|
||||
|
||||
# Software: eFiction
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
from base_efiction_adapter import BaseEfictionAdapter
|
||||
|
||||
from .. import BeautifulSoup as bs
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
class DarkSolaceOrgAdapter(BaseEfictionAdapter):
|
||||
|
||||
from base_adapter import BaseSiteAdapter, makeDate
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return 'dark-solace.org'
|
||||
|
||||
@classmethod
|
||||
def getPathToArchive(self):
|
||||
return '/elysian'
|
||||
|
||||
@classmethod
|
||||
def getSiteAbbrev(self):
|
||||
return 'dksl'
|
||||
|
||||
@classmethod
|
||||
def getDateFormat(self):
|
||||
return "%B %d, %Y"
|
||||
|
||||
def getClass():
|
||||
return DarkSolaceOrgAdapter
|
||||
|
||||
# Class name has to be unique. Our convention is camel case the
|
||||
# sitename with Adapter at the end. www is skipped.
|
||||
class DarkSolaceOrgAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.decode = ["Windows-1252",
|
||||
"utf8"] # 1252 is a superset of iso-8859-1.
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
self.is_adult=False
|
||||
|
||||
# get storyId from url--url validation guarantees query is only sid=1234
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/elysian/viewstory.php?sid='+self.story.getMetadata('storyId'))
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','dksl')
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%B %d, %Y"
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'dark-solace.org'
|
||||
|
||||
@classmethod
|
||||
def getAcceptDomains(cls):
|
||||
return ['www.dark-solace.org','dark-solace.org']
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/elysian/viewstory.php?sid=1234"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return re.escape("http://")+"(www\.)?"+re.escape(self.getSiteDomain()+"/elysian/viewstory.php?sid=")+r"\d+$"
|
||||
|
||||
## Login seems to be reasonably standard across eFiction sites.
|
||||
def needToLoginCheck(self, data):
|
||||
if 'This story contains adult content not suitable for children' in data \
|
||||
or "That password doesn't match the one in our database" in data \
|
||||
or "Registered Users Only" in data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def performLogin(self, url):
|
||||
params = {}
|
||||
|
||||
if self.password:
|
||||
params['penname'] = self.username
|
||||
params['password'] = self.password
|
||||
else:
|
||||
params['penname'] = self.getConfig("username")
|
||||
params['password'] = self.getConfig("password")
|
||||
params['action'] = 'login'
|
||||
params['submit'] = 'Submit'
|
||||
|
||||
loginUrl = 'http://www.' + self.getSiteDomain() + '/elysian/user.php'
|
||||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['penname']))
|
||||
|
||||
d = self._postUrl(loginUrl, params)
|
||||
|
||||
if "Member Account" not in d : #User Account Page
|
||||
logger.info("Failed to login to URL %s as %s, or have no authorization to access the story" % (loginUrl, params['penname']))
|
||||
raise exceptions.FailedToLogin(url,params['penname'])
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
## Getting the chapter list and the meta data, plus 'is adult' checking.
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
# Weirdly, different sites use different warning numbers.
|
||||
# If the title search below fails, there's a good chance
|
||||
# you need a different number. print data at that point
|
||||
# and see what the 'click here to continue' url says.
|
||||
addurl = "&ageconsent=ok&warning=5"
|
||||
else:
|
||||
addurl=""
|
||||
|
||||
# index=1 makes sure we see the story chapter index. Some
|
||||
# sites skip that for one-chapter stories.
|
||||
url = self.url+'&index=1'+addurl
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
if self.needToLoginCheck(data):
|
||||
# need to log in for this one.
|
||||
self.performLogin(url)
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data)
|
||||
if m != None:
|
||||
if self.is_adult or self.getConfig("is_adult"):
|
||||
# We tried the default and still got a warning, so
|
||||
# let's pull the warning number from the 'continue'
|
||||
# link and reload data.
|
||||
addurl = m.group(1)
|
||||
# correct stupid & error in url.
|
||||
addurl = addurl.replace("&","&")
|
||||
url = self.url+'&index=1'+addurl
|
||||
logger.debug("URL 2nd try: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
else:
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
|
||||
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = bs.BeautifulSoup(data)
|
||||
# print data
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
## Title and author
|
||||
div = soup.find('div', {'id' : 'pagetitle'})
|
||||
|
||||
aut = div.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
|
||||
self.story.setMetadata('authorId',aut['href'].split('=')[1])
|
||||
self.story.setMetadata('authorUrl','http://'+self.host+'/elysian/'+aut['href'])
|
||||
self.story.setMetadata('author',aut.string)
|
||||
aut.extract()
|
||||
|
||||
# first a tag in pagetitle is title
|
||||
self.story.setMetadata('title',stripHTML(div.find('a')))
|
||||
div.find('a').extract()
|
||||
# only thing left in div(pagetitle) now should be 'by' and rating.
|
||||
rating = stripHTML(div)
|
||||
if '[' in rating:
|
||||
self.story.setMetadata('rating', rating[rating.index('[')+1:-1])
|
||||
|
||||
for chapa in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+
|
||||
self.story.getMetadata('storyId')+'&chapter=\d+')):
|
||||
self.chapterUrls.append((stripHTML(chapa),'http://'+self.host+'/elysian/'+chapa['href']))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')))
|
||||
storylink = asoup.find('a', href=re.compile(r'viewstory.php\?sid='+
|
||||
self.story.getMetadata('storyId')+'($|[^\d])'))
|
||||
# author's story list is paginated if there's a pagelinks div.
|
||||
# Only need to look in it if the story wasn't on the first page.
|
||||
pagelinks = asoup.find('div',{'id':'pagelinks'})
|
||||
if pagelinks and storylink==None:
|
||||
authpageslist = pagelinks.findAll('a',href=re.compile(r'action=storiesby'))
|
||||
for page in authpageslist[1:]: # skip first, already checked above.
|
||||
asoup = bs.BeautifulSoup(self._fetchUrl('http://'+self.host+'/elysian/'+page['href']))
|
||||
storylink = asoup.find('a', href=re.compile(r'viewstory.php\?sid='+
|
||||
self.story.getMetadata('storyId')+'($|[^\d])'))
|
||||
if storylink:
|
||||
break
|
||||
|
||||
if not storylink:
|
||||
raise exceptions.FailedToDownload("Unable to find story metadata on author's page(s)")
|
||||
|
||||
metalist = storylink.parent.parent
|
||||
# eFiction sites don't help us out a lot with their meta data
|
||||
# formating, so it's a little ugly.
|
||||
|
||||
# utility method
|
||||
def defaultGetattr(d,k):
|
||||
try:
|
||||
return d[k]
|
||||
except:
|
||||
return ""
|
||||
|
||||
|
||||
# <span class="label">Rated:</span> NC-17<br /> etc
|
||||
labels = metalist.findAll('span', {'class' : 'label'})
|
||||
for labelspan in labels:
|
||||
label = labelspan.text
|
||||
value = labelspan.nextSibling
|
||||
|
||||
if 'Summary' in label:
|
||||
## Everything until the next span class='label'
|
||||
svalue = ""
|
||||
while value and not (defaultGetattr(value,'class') == 'label' or "Chapters: " in stripHTML(value)):
|
||||
svalue += str(value)
|
||||
value = value.nextSibling
|
||||
self.setDescription(url,svalue)
|
||||
#self.story.setMetadata('description',stripHTML(svalue))
|
||||
|
||||
if 'Word count' in label:
|
||||
self.story.setMetadata('numWords', value)
|
||||
|
||||
if 'Categories' in label:
|
||||
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
|
||||
for cat in cats:
|
||||
self.story.addToList('category',cat.string)
|
||||
|
||||
if 'Characters' in label:
|
||||
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
|
||||
for char in chars:
|
||||
self.story.addToList('characters',char.string)
|
||||
|
||||
if 'Genre' in label:
|
||||
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1'))
|
||||
for genre in genres:
|
||||
self.story.addToList('genre',genre.string)
|
||||
|
||||
if 'Warnings' in label:
|
||||
warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2'))
|
||||
for warning in warnings:
|
||||
self.story.addToList('warnings',warning.string)
|
||||
|
||||
if 'Completed' in label:
|
||||
if 'Yes' in value:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
if 'Published' in label:
|
||||
self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
if 'Updated' in label:
|
||||
# there's a stray [ at the end.
|
||||
#value = value[0:-1]
|
||||
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
|
||||
|
||||
try:
|
||||
# Find Series name from series URL.
|
||||
a = metalist.find('a', href=re.compile(r"series.php\?seriesid=\d+"))
|
||||
series_name = a.string
|
||||
series_url = 'http://'+self.host+'/elysian/'+a['href']
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url))
|
||||
# can't use ^viewstory...$ in case of higher rated stories with javascript href.
|
||||
storylink = seriessoup.find('a', href=re.compile(r'viewstory.php\?sid='+
|
||||
self.story.getMetadata('storyId')+'($|[^\d])'))
|
||||
if storylink and storylink.parent and storylink.parent['class'] != 'title': # in case of links inside story summaries.
|
||||
storylink = None
|
||||
|
||||
offset = 0
|
||||
# series story list is paginated if there's a pagelinks div.
|
||||
# Only need to look in it if the story wasn't on the first page.
|
||||
pagelinks = seriessoup.find('div',{'id':'pagelinks'})
|
||||
if pagelinks and storylink==None:
|
||||
authpageslist = pagelinks.findAll('a',href=re.compile(r'offset='))
|
||||
for page in authpageslist[1:]: # skip first, already checked above.
|
||||
seriessoup = bs.BeautifulSoup(self._fetchUrl('http://'+self.host+'/elysian/'+page['href']))
|
||||
storylink = seriessoup.find('a', href=re.compile(r'viewstory.php\?sid='+
|
||||
self.story.getMetadata('storyId')+'($|[^\d])'))
|
||||
if storylink and storylink.parent and storylink.parent['class'] != 'title': # in case of links inside story summaries.
|
||||
storylink = None
|
||||
if storylink:
|
||||
offset = int(page['href'].split('=')[-1]) # offset is last.
|
||||
break
|
||||
|
||||
# for reasons I don't understand, searching for story
|
||||
# links by regex wasn't working reliably. It was missing
|
||||
# the javascript links sometimes. This is cleaner anyway.
|
||||
for i, div in enumerate(seriessoup.findAll('div', {'class':'title'})):
|
||||
a = div.find('a') # first a is story link.
|
||||
# skip 'report this' and 'TOC' links
|
||||
if a == storylink:
|
||||
self.setSeries(series_name, 1+i+offset)
|
||||
self.story.setMetadata('seriesUrl',series_url)
|
||||
break
|
||||
|
||||
except Exception, e:
|
||||
logger.debug("Series parsing failed: %s"%e)
|
||||
|
||||
# grab the text for an individual chapter.
|
||||
def getChapterText(self, url):
|
||||
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
soup = bs.BeautifulSoup(self._fetchUrl(url))
|
||||
|
||||
div = soup.find('div', {'id' : 'story'})
|
||||
|
||||
if None == div:
|
||||
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
|
||||
|
||||
return self.utf8FromSoup(url,div)
|
||||
|
||||
@@ -38,8 +38,9 @@ class DramioneOrgAdapter(BaseSiteAdapter):
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
self.decode = ["Windows-1252",
|
||||
"utf8"] # 1252 is a superset of iso-8859-1.
|
||||
self.decode = ["utf8",
|
||||
"Windows-1252",]
|
||||
# 1252 is a superset of iso-8859-1.
|
||||
# Most sites that claim to be
|
||||
# iso-8859-1 (and some that claim to be
|
||||
# utf8) are really windows-1252.
|
||||
|
||||
@@ -104,7 +104,7 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
data = self._fetchUrl(url)
|
||||
#logger.debug("\n===================\n%s\n===================\n"%data)
|
||||
soup = self.make_soup(data)
|
||||
except urllib2.HTTPError, e:
|
||||
except urllib2.HTTPError as e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(url)
|
||||
else:
|
||||
@@ -135,11 +135,15 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
chapcount+1)
|
||||
logger.debug('=Trying newer chapter: %s' % tryurl)
|
||||
newdata = self._fetchUrl(tryurl)
|
||||
if "not found. Please check to see you are not using an outdated url." \
|
||||
not in newdata:
|
||||
if "not found. Please check to see you are not using an outdated url." not in newdata \
|
||||
and "This request takes too long to process, it is timed out by the server." not in newdata:
|
||||
logger.debug('=======Found newer chapter: %s' % tryurl)
|
||||
soup = self.make_soup(newdata)
|
||||
except:
|
||||
except urllib2.HTTPError as e:
|
||||
if e.code == 503:
|
||||
raise e
|
||||
except e:
|
||||
logger.warn("Caught an exception reading URL: %s sleeptime(%s) Exception %s."%(unicode(url),sleeptime,unicode(e)))
|
||||
pass
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
|
||||
@@ -178,8 +178,11 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
|
||||
if get_cover:
|
||||
# try setting from href, if fails, try using the img src
|
||||
if self.setCoverImage(self.url,coverurl)[0] == "failedtoload":
|
||||
coverurl = storyImage.find('img')['src']
|
||||
self.setCoverImage(self.url,coverurl)
|
||||
img = storyImage.find('img')
|
||||
# try src, then data-src, then leave None.
|
||||
coverurl = img.get('src',img.get('data-src',None))
|
||||
if coverurl:
|
||||
self.setCoverImage(self.url,coverurl)
|
||||
|
||||
coverSource = storyImage.find('a', {'class':'source'})
|
||||
if coverSource:
|
||||
|
||||
@@ -15,15 +15,6 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
from ..htmlcleanup import stripHTML
|
||||
from .. import exceptions as exceptions
|
||||
|
||||
from base_xenforoforum_adapter import BaseXenForoForumAdapter
|
||||
|
||||
def getClass():
|
||||
@@ -42,7 +33,3 @@ class ForumsSpacebattlesComAdapter(BaseXenForoForumAdapter):
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'forums.spacebattles.com'
|
||||
|
||||
@classmethod
|
||||
def getURLPrefix(cls):
|
||||
return 'https://' + cls.getSiteDomain()
|
||||
|
||||
|
||||
@@ -32,7 +32,3 @@ class ForumsSufficientVelocityComAdapter(BaseXenForoForumAdapter):
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'forums.sufficientvelocity.com'
|
||||
|
||||
@classmethod
|
||||
def getURLPrefix(cls):
|
||||
return 'http://' + cls.getSiteDomain()
|
||||
|
||||
@@ -98,6 +98,26 @@ class HPFandomNetAdapterAdapter(BaseSiteAdapter): # XXX
|
||||
|
||||
# Now go hunting for all the meta data and the chapter list.
|
||||
|
||||
# set first URL
|
||||
a = soup.find('a', href=re.compile(r"viewstory.php\?sid=\d+"))
|
||||
self._setURL('http://' + self.getSiteDomain() + '/eff/'+a['href'])
|
||||
if self.parsedUrl.query.split('=',)[1] != self.story.getMetadata('storyId'):
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
url = self.url
|
||||
logger.debug("reset URL: "+url)
|
||||
try:
|
||||
data = self._fetchUrl(url)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
|
||||
# use BeautifulSoup HTML parser to make everything easier to find.
|
||||
soup = bs.BeautifulSoup(data)
|
||||
|
||||
# self.story.setMetadata('storyId', re.compile(self.getSiteURLPattern()).match(a).group('storyId'))
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
|
||||
self.story.setMetadata('authorId',a['href'].split('=')[1])
|
||||
|
||||
@@ -40,39 +40,53 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
# get storyId from url--url validation guarantees query correct
|
||||
m = re.match(self.getSiteURLPattern(),url)
|
||||
urltitle='urltitle'
|
||||
if m:
|
||||
if m.group('id'):
|
||||
self.story.setMetadata('storyId',m.group('id'))
|
||||
if m.group('id1'):
|
||||
self.story.setMetadata('storyId',m.group('id1'))
|
||||
urltitle=m.group('urltitle1')
|
||||
elif m.group('id2'):
|
||||
self.story.setMetadata('storyId',m.group('id2'))
|
||||
urltitle=m.group('urltitle2')
|
||||
elif m.group('id3'):
|
||||
self.story.setMetadata('storyId',m.group('id2'))
|
||||
self.story.setMetadata('storyId',m.group('id3'))
|
||||
else:
|
||||
raise InvalidStoryURL(url,
|
||||
self.getSiteDomain(),
|
||||
self.getSiteExampleURLs())
|
||||
|
||||
# normalized story URL.
|
||||
self._setURL('http://' + self.getSiteDomain() + '/fanfic/view_st.php/'+self.story.getMetadata('storyId'))
|
||||
self._setURL('http://' + self.getSiteDomain() + '/fanfic/s/'+urltitle+'/'+self.story.getMetadata('storyId'))
|
||||
else:
|
||||
raise exceptions.InvalidStoryURL(url,
|
||||
self.getSiteDomain(),
|
||||
self.getSiteExampleURLs())
|
||||
|
||||
# The date format will vary from site to site.
|
||||
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
self.dateformat = "%B %d, %Y %H:%M"
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return 'www.mediaminer.org'
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return "http://"+cls.getSiteDomain()+"/fanfic/view_st.php/123456 http://"+cls.getSiteDomain()+"/fanfic/view_ch.php/1234123/123444#fic_c"
|
||||
return "http://"+cls.getSiteDomain()+"/fanfic/s/story-title/123456 http://"+cls.getSiteDomain()+"/fanfic/c/story-title/chapter-title/123456/987612"
|
||||
|
||||
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())+\
|
||||
r"/fanfic/view_(st|ch)\.php"+\
|
||||
r"(/(?P<id>\d+)(/\d+(#fic_c)?)?/?|"+\
|
||||
r"\?((submit=View(\+| )Chapter|id=(?P<id2>\d+)|cid=\d+)&?)+)"
|
||||
|
||||
## old urls
|
||||
## http://www.mediaminer.org/fanfic/view_st.php/76882
|
||||
## new urls
|
||||
## http://www.mediaminer.org/fanfic/s/ghosts-from-the-past/72
|
||||
## http://www.mediaminer.org/fanfic/c/ghosts-from-the-past/chapter-2/72/174
|
||||
## http://www.mediaminer.org/fanfic/s/robtech-final-missions/61553
|
||||
## http://www.mediaminer.org/fanfic/c/robtech-final-missions/robotech-final-missions-oneshot/61553/189830
|
||||
return re.escape("http://"+self.getSiteDomain())+r"/fanfic/"+\
|
||||
r"((s/(?P<urltitle1>[^/]+)/(?P<id1>\d+))|"+\
|
||||
r"((c/(?P<urltitle2>[^/]+)/[^/]+/(?P<id2>\d+))/\d+)|"+\
|
||||
r"(view_st\.php/(?P<id3>\d+)))"
|
||||
|
||||
# Override stripURLParameters so the id parameter won't get stripped
|
||||
@classmethod
|
||||
def stripURLParameters(cls, url):
|
||||
@@ -84,9 +98,10 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
|
||||
logger.debug("URL: "+url)
|
||||
|
||||
try:
|
||||
data = self._fetchUrl(url+'/') # trailing / gets 'chapter list' page even for one-shots.
|
||||
data = self._fetchUrl(url) # w/o trailing / gets 'chapter list' page even for one-shots.
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
logger.error("404 on %s"%url)
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
else:
|
||||
raise e
|
||||
@@ -96,11 +111,13 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
# [ A - All Readers ], strip '[' ']'
|
||||
## Above title because we remove the smtxt font to get title.
|
||||
smtxt = soup.find("font",{"class":"smtxt"})
|
||||
smtxt = soup.find("h3",{"id":"post-rating"})
|
||||
if not smtxt:
|
||||
logger.error("can't find rating")
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
rating = smtxt.string[1:-1]
|
||||
self.story.setMetadata('rating',rating)
|
||||
else:
|
||||
rating = smtxt.string[1:-1]
|
||||
self.story.setMetadata('rating',rating)
|
||||
|
||||
# Find authorid and URL from... author url.
|
||||
a = soup.find('a', href=re.compile(r"/fanfic/src.php/u/\d+"))
|
||||
@@ -116,37 +133,24 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
|
||||
## <td class="ffh">The Kraut, The Bartender, and The Drunkard: Chapter 1</b> <font class="smtxt">[ P - Pre-Teen ]</font></td>
|
||||
## <td class="ffh">Betrayal and Justice: A Cold Heart</b> <font size="-1">( Chapter 1 )</font> <font class="smtxt">[ A - All Readers ]</font></td>
|
||||
## <td class="ffh">Question and Answer: Question and Answer</b> <font size="-1">( One-Shot )</font> <font class="smtxt">[ A - All Readers ]</font></td>
|
||||
title = soup.find('td',{'class':'ffh'})
|
||||
for font in title.findAll('font'):
|
||||
font.extract() # removes 'font' tags from inside the td.
|
||||
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)
|
||||
# title = soup.find('td',{'class':'ffh'})
|
||||
# for font in title.findAll('font'):
|
||||
# font.extract() # removes 'font' tags from inside the td.
|
||||
# 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',stripHTML(soup.find('h1',{'id':'post-title'})))
|
||||
|
||||
# save date from first for later.
|
||||
firstdate=None
|
||||
|
||||
# Find the chapters
|
||||
select = soup.find('select',{'name':'cid'})
|
||||
if not select:
|
||||
self.chapterUrls.append(( self.story.getMetadata('title'),self.url))
|
||||
else:
|
||||
for option in select.findAll("option"):
|
||||
chapter = stripHTML(option.string)
|
||||
## chapter can be: Chapter 7 [Jan 23, 2011]
|
||||
## or: Vigilant Moonlight ( Chapter 1 ) [Jan 30, 2004]
|
||||
## or even: Prologue ( Prologue ) [Jul 31, 2010]
|
||||
m = re.match(r'^(.*?) (\( .*? \) )?\[(.*?)\]$',chapter)
|
||||
chapter = m.group(1)
|
||||
# save date from first for later.
|
||||
if not firstdate:
|
||||
firstdate = m.group(3)
|
||||
# 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']))
|
||||
# Find the chapters - one-shot now have chapter list, too.
|
||||
chap_p = soup.find('p',{'style':'margin-left:10px;'})
|
||||
for (atag,aurl,name) in [ (x,x['href'],stripHTML(x)) for x in chap_p.find_all('a') ]:
|
||||
self.chapterUrls.append((name,'http://'+self.host+aurl))
|
||||
|
||||
self.story.setMetadata('numChapters',len(self.chapterUrls))
|
||||
|
||||
# category
|
||||
@@ -155,27 +159,20 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
|
||||
self.story.addToList('category',a.string)
|
||||
|
||||
# genre
|
||||
# <a href="/fanfic/src.php/a/567">Ranma 1/2</a>
|
||||
# <a href="/fanfic/src.php/g/567">Ranma 1/2</a>
|
||||
for a in soup.findAll('a',href=re.compile(r"^/fanfic/src.php/g/")):
|
||||
self.story.addToList('genre',a.string)
|
||||
|
||||
# if firstdate, then the block below will only have last updated.
|
||||
if firstdate:
|
||||
self.story.setMetadata('datePublished', makeDate(firstdate, "%b %d, %Y"))
|
||||
# Everything else is in <tr bgcolor="#EEEED4">
|
||||
|
||||
metastr = stripHTML(soup.find("tr",{"bgcolor":"#EEEED4"})).replace('\n',' ').replace('\r',' ').replace('\t',' ')
|
||||
# Latest Revision: August 03, 2010
|
||||
m = re.match(r".*?(?:Latest Revision|Uploaded On): ([a-zA-Z]+ \d\d, \d\d\d\d)",metastr)
|
||||
metastr = stripHTML(soup.find("div",{"class":"post-meta"}))
|
||||
|
||||
# Latest Revision: February 07, 2015 15:21 PST
|
||||
m = re.match(r".*?(?:Latest Revision|Uploaded On): ([a-zA-Z]+ \d\d, \d\d\d\d \d\d:\d\d)",metastr)
|
||||
if m:
|
||||
self.story.setMetadata('dateUpdated', makeDate(m.group(1), "%B %d, %Y"))
|
||||
if not firstdate:
|
||||
self.story.setMetadata('datePublished',
|
||||
self.story.getMetadataRaw('dateUpdated'))
|
||||
|
||||
else:
|
||||
self.story.setMetadata('dateUpdated',
|
||||
self.story.getMetadataRaw('datePublished'))
|
||||
self.story.setMetadata('dateUpdated', makeDate(m.group(1), self.dateformat))
|
||||
# site doesn't give date published on index page.
|
||||
# set to updated, change in chapters below.
|
||||
# self.story.setMetadata('datePublished',
|
||||
# self.story.getMetadataRaw('dateUpdated'))
|
||||
|
||||
# Words: 123456
|
||||
m = re.match(r".*?\| Words: (\d+) \|",metastr)
|
||||
@@ -201,43 +198,54 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
data=self._fetchUrl(url)
|
||||
data = self._fetchUrl(url)
|
||||
soup = self.make_soup(data)
|
||||
|
||||
header = soup.find('div',{'class':'post-meta clearfix '})
|
||||
headerstr = stripHTML(soup.find('div',{'class':'post-meta clearfix '}))
|
||||
# print("data:%s"%data)
|
||||
#header.extract()
|
||||
|
||||
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 = 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.
|
||||
chapter.append(div)
|
||||
del div['style']
|
||||
del div['align']
|
||||
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('<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.
|
||||
m = re.match(r".*?Uploaded On: ([a-zA-Z]+ \d\d, \d\d\d\d \d\d:\d\d)",headerstr)
|
||||
if m:
|
||||
date = makeDate(m.group(1), self.dateformat)
|
||||
if not self.story.getMetadataRaw('datePublished') or date < self.story.getMetadataRaw('datePublished'):
|
||||
self.story.setMetadata('datePublished', date)
|
||||
|
||||
return self.utf8FromSoup(url,soup)
|
||||
chapter = soup.find('div',{'id':'fanfic-text'})
|
||||
|
||||
return self.utf8FromSoup(url,chapter)
|
||||
|
||||
# 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 = 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.
|
||||
# chapter.append(div)
|
||||
# del div['style']
|
||||
# del div['align']
|
||||
# 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('<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.
|
||||
|
||||
# return self.utf8FromSoup(url,soup)
|
||||
|
||||
|
||||
def getClass():
|
||||
|
||||
@@ -258,6 +258,7 @@ class PortkeyOrgAdapter(BaseSiteAdapter): # XXX
|
||||
data = self._fetchUrl(url)
|
||||
|
||||
data = data.replace("HTML>","div>")
|
||||
data = data.replace("html>","div>")
|
||||
|
||||
soup = bs.BeautifulSoup(data)
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2015 FanFicFare team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import re
|
||||
|
||||
from base_xenforoforum_adapter import BaseXenForoForumAdapter
|
||||
|
||||
def getClass():
|
||||
return QuestionablequestingComAdapter
|
||||
|
||||
class QuestionablequestingComAdapter(BaseXenForoForumAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseXenForoForumAdapter.__init__(self, config, url)
|
||||
|
||||
# Each adapter needs to have a unique site abbreviation.
|
||||
self.story.setMetadata('siteabbrev','qq')
|
||||
|
||||
@staticmethod # must be @staticmethod, don't remove it.
|
||||
def getSiteDomain():
|
||||
# The site domain. Does have www here, if it uses it.
|
||||
return 'questionablequesting.com'
|
||||
|
||||
@classmethod
|
||||
def getAcceptDomains(cls):
|
||||
return ["forum."+cls.getSiteDomain(),cls.getSiteDomain()]
|
||||
|
||||
@classmethod
|
||||
def getConfigSections(cls):
|
||||
"Only needs to be overriden if has additional ini sections."
|
||||
return ['base_xenforoforum','forum.'+cls.getSiteDomain(),cls.getSiteDomain()]
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return r'https?://(forum.)?'+re.escape(self.getSiteDomain())+r'/(?P<tp>threads|posts)/(.+\.)?(?P<id>\d+)/?'
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import re
|
||||
import urlparse
|
||||
import urllib2
|
||||
import datetime
|
||||
|
||||
from .. import exceptions
|
||||
from base_adapter import BaseSiteAdapter
|
||||
|
||||
SITE_DOMAIN = 'quotev.com'
|
||||
STORY_URL_TEMPLATE = 'http://www.quotev.com/story/%s'
|
||||
|
||||
|
||||
def getClass():
|
||||
return QuotevComAdapter
|
||||
|
||||
|
||||
def get_url_path_segments(url):
|
||||
return tuple(filter(None, url.split('/')[3:]))
|
||||
|
||||
|
||||
class QuotevComAdapter(BaseSiteAdapter):
|
||||
|
||||
def __init__(self, config, url):
|
||||
BaseSiteAdapter.__init__(self, config, url)
|
||||
|
||||
story_id = get_url_path_segments(url)[1]
|
||||
self._setURL(STORY_URL_TEMPLATE % story_id)
|
||||
self.story.setMetadata('storyId', story_id)
|
||||
self.story.setMetadata('siteabbrev', SITE_DOMAIN)
|
||||
|
||||
@staticmethod
|
||||
def getSiteDomain():
|
||||
return SITE_DOMAIN
|
||||
|
||||
@classmethod
|
||||
def getSiteExampleURLs(cls):
|
||||
return STORY_URL_TEMPLATE % '1234'
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
pattern = re.escape(STORY_URL_TEMPLATE.rsplit('%', 1)[0]) + r'(.+?)($|&|/)'
|
||||
pattern = pattern.replace(r'http\:', r'https?\:')
|
||||
pattern = pattern.replace(r'https?\:\/\/www\.', r'https?\:\/\/(www\.)?')
|
||||
return pattern
|
||||
|
||||
def use_pagecache(self):
|
||||
return True
|
||||
|
||||
def extractChapterUrlsAndMetadata(self):
|
||||
try:
|
||||
data = self._fetchUrl(self.url)
|
||||
except urllib2.HTTPError:
|
||||
raise exceptions.FailedToDownload(self.url)
|
||||
|
||||
soup = self.make_soup(data)
|
||||
|
||||
element = soup.find('div', {'class': 'result_head'})
|
||||
if not element:
|
||||
raise exceptions.StoryDoesNotExist(self.url)
|
||||
|
||||
self.story.setMetadata('title', element.find('span', recursive=False).get_text())
|
||||
|
||||
element = soup.find('div', {'class': 'desc_creator'})('a')[1]
|
||||
self.story.setMetadata('author', element.get_text())
|
||||
self.story.setMetadata('authorId', get_url_path_segments(element['href'])[0])
|
||||
self.story.setMetadata('authorUrl', urlparse.urljoin(self.url, element['href']))
|
||||
self.setDescription(self.url, soup.find('div', id='qdesct'))
|
||||
self.setCoverImage(self.url, urlparse.urljoin(self.url, soup.find('img', {'class': 'logo'})['src']))
|
||||
|
||||
for a in soup.find('div', {'class': 'tag'})('a'):
|
||||
if a['href'] == '#':
|
||||
continue
|
||||
|
||||
self.story.addToList('category', a.get_text())
|
||||
|
||||
elements = soup('span', {'class': 'q_time'})
|
||||
self.story.setMetadata('datePublished', datetime.datetime.fromtimestamp(float(elements[0]['ts'])))
|
||||
self.story.setMetadata('dateUpdated', datetime.datetime.fromtimestamp(float(elements[1]['ts'])))
|
||||
|
||||
for a in soup.find('div', id='rselect')('a'):
|
||||
self.chapterUrls.append((a.get_text(), urlparse.urljoin(self.url, a['href'])))
|
||||
|
||||
self.story.setMetadata('numChapters', len(self.chapterUrls))
|
||||
|
||||
element = soup.find('div', {'class': 't'})('div', recursive=False)[1].div
|
||||
data = filter(None, (x.strip() for x in element('span')[1].next_sibling.split(u'\xb7')))
|
||||
|
||||
if 'completed' in data:
|
||||
self.story.setMetadata('status', 'Completed')
|
||||
data.remove('completed')
|
||||
else:
|
||||
self.story.setMetadata('status', 'In-Progress')
|
||||
|
||||
for datum in data:
|
||||
value, key = datum.split()
|
||||
self.story.setMetadata(key, value.replace(',', '').replace('.', ''))
|
||||
|
||||
self.story.setMetadata('favorites', soup.find('div', id='favqn').get_text())
|
||||
self.story.setMetadata('comments', soup.find('a', id='comment_btn').span.get_text())
|
||||
|
||||
def getChapterText(self, url):
|
||||
data = self._fetchUrl(url)
|
||||
soup = self.make_soup(data)
|
||||
|
||||
element = soup.find('div', id='restxt')
|
||||
for a in element('a'):
|
||||
a.unwrap()
|
||||
|
||||
return self.utf8FromSoup(url, element)
|
||||
@@ -81,6 +81,12 @@ class TestSiteAdapter(BaseSiteAdapter):
|
||||
|
||||
return
|
||||
|
||||
if idnum >= 700 and idnum <= 710:
|
||||
self._setURL('http://test1.com?sid=%s'%(idnum+100))
|
||||
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
|
||||
idstr = self.story.getMetadata('storyId')
|
||||
idnum = int(idstr)
|
||||
|
||||
if idstr == '665' and not (self.is_adult or self.getConfig("is_adult")):
|
||||
logger.warn("self.is_adult:%s"%self.is_adult)
|
||||
raise exceptions.AdultCheckRequired(self.url)
|
||||
|
||||
@@ -600,7 +600,8 @@ class BaseSiteAdapter(Configurable):
|
||||
if t.name not in ('p') and t.string != None and len(t.string.strip()) == 0 :
|
||||
t.extract()
|
||||
except AttributeError, ae:
|
||||
logger.error("Error parsing HTML, probably poor input HTML. %s"%ae)
|
||||
if "%s"%ae != "'NoneType' object has no attribute 'next_element'":
|
||||
logger.error("Error parsing HTML, probably poor input HTML. %s"%ae)
|
||||
|
||||
retval = unicode(soup)
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ class BaseEfictionAdapter(BaseSiteAdapter):
|
||||
"""
|
||||
Get the URL to a user page on this site.
|
||||
"""
|
||||
return "%s?sid=%s" % (self.getUrlForPhp(self.getViewUserPhpName()), userId)
|
||||
return "%s?uid=%s" % (self.getUrlForPhp(self.getViewUserPhpName()), userId)
|
||||
|
||||
@classmethod
|
||||
def getLoginUrl(self):
|
||||
|
||||
@@ -77,7 +77,7 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
return cls.getURLPrefix()+"/threads/some-story-name.123456/"
|
||||
|
||||
def getSiteURLPattern(self):
|
||||
return r"https?://"+re.escape(self.getSiteDomain())+r"/(?P<tp>threads|posts)/(.+\.)?(?P<id>\d+)/"
|
||||
return r"https?://"+re.escape(self.getSiteDomain())+r"/(?P<tp>threads|posts)/(.+\.)?(?P<id>\d+)/?"
|
||||
|
||||
def use_pagecache(self):
|
||||
'''
|
||||
@@ -131,7 +131,12 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
self.story.setMetadata('dateUpdated', date)
|
||||
|
||||
self.chapterUrls.append((name,self.getURLPrefix()+'/'+url))
|
||||
|
||||
|
||||
## only use tags if threadmarks for chapters.
|
||||
## a bit arbitrary, but likely.
|
||||
for tag in soup.findAll('a',{'class':'tag'}):
|
||||
self.story.addToList('forumtags',stripHTML(tag))
|
||||
|
||||
soup = soup.find('li',{'class':'message'}) # limit first post for date stuff below. ('#' posts above)
|
||||
|
||||
# Now go hunting for the 'chapter list'.
|
||||
@@ -204,6 +209,15 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
def getChapterText(self, url):
|
||||
logger.debug('Getting chapter text from: %s' % url)
|
||||
|
||||
## there's some history of stories with links to the wrong
|
||||
## page. This changes page#post URLs to perma-link URLs.
|
||||
## Which will be redirected back to page#posts, but the
|
||||
## *correct* ones.
|
||||
# http://forums.sufficientvelocity.com/threads/harry-potter-and-the-not-fatal-at-all-cultural-exchange-program.330/page-4#post-39915
|
||||
# https://forums.sufficientvelocity.com/posts/39915/
|
||||
if '#post-' in url:
|
||||
url = self.getURLPrefix()+'/posts/'+url.split('#post-')[1]+'/'
|
||||
|
||||
origurl = url
|
||||
(data,opened) = self._fetchUrlOpened(url)
|
||||
url = opened.geturl()
|
||||
@@ -216,6 +230,7 @@ class BaseXenForoForumAdapter(BaseSiteAdapter):
|
||||
if '#' in url:
|
||||
anchorid = url.split('#')[1]
|
||||
soup = soup.find('li',id=anchorid)
|
||||
|
||||
bq = soup.find('blockquote')
|
||||
|
||||
bq.name='div'
|
||||
|
||||
@@ -220,6 +220,7 @@ def get_valid_keywords():
|
||||
'chapter_title_add_pattern',
|
||||
'chapter_title_new_pattern',
|
||||
'chapter_title_addnew_pattern',
|
||||
'title_chapter_range_pattern',
|
||||
'mark_new_chapters',
|
||||
'check_next_chapter',
|
||||
'skip_author_cover',
|
||||
|
||||
+27
-6
@@ -148,8 +148,10 @@ zip_filename: ${title}-${siteabbrev}_${storyId}${formatext}.zip
|
||||
## zip_filename.
|
||||
allow_unsafe_filename: false
|
||||
|
||||
## The regex pattern of 'unsafe' filename chars for above.
|
||||
#output_filename_safepattern:[^a-zA-Z0-9_\. \[\]\(\)&'-]+
|
||||
## The regex pattern of 'unsafe' filename chars for above. First
|
||||
## character . OR any one or more characters that are NOT a letter,
|
||||
## number, or one of _. []()&'-
|
||||
output_filename_safepattern:(^\.|/\.|[^a-zA-Z0-9_\. \[\]\(\)&'-]+)
|
||||
|
||||
## entries to make epub subjects and calibre tags
|
||||
## lastupdate creates two tags: "Last Update Year/Month: %Y/%m" and "Last Update: %Y/%m/%d"
|
||||
@@ -373,13 +375,14 @@ cover_exclusion_regexp:/clear.png
|
||||
strip_chapter_numbers:false
|
||||
|
||||
## Copy title to tagsfromtitle for parsing tags.
|
||||
add_to_extra_valid_entries:,tagsfromtitle
|
||||
add_to_extra_valid_entries:,tagsfromtitle,forumtags
|
||||
|
||||
## '.NOREPL' tells the system to *not* apply title's
|
||||
## in/exclude/replace_metadata -- Only works on include_in_ lines.
|
||||
include_in_tagsfromtitle:title.NOREPL
|
||||
|
||||
tagsfromtitle_label:Tags from Title
|
||||
forumtags_label:Tags from Forum
|
||||
|
||||
## might want to do this, maybe not. Will often include category, but
|
||||
## also often include non-category stuff.
|
||||
@@ -396,18 +399,20 @@ add_to_replace_metadata:
|
||||
tagsfromtitle=>[\(\)\[\]]=>
|
||||
# change (spaces)slash(or semicolon)(spaces) to comma
|
||||
tagsfromtitle=> *[/;] *=>,
|
||||
tagsfromtitle=> x =>,
|
||||
tagsfromtitle=> [xX] =>,
|
||||
|
||||
# remove [] or () blocks and leading/trailing spaces/dashes/colons
|
||||
title=>[-: ]*[\(\[]([^\]\)]+)[\)\]][-: ]*=>
|
||||
# remove 'Thread' and the next word, usually "Thread 2", "Thread
|
||||
# four", "Thread iv", etc
|
||||
title=>[-: ]*[Tt]hread [^ ]+[-: ]*=>
|
||||
title,tagsfromtitle=>[-: ]*[Tt]hread [^ ]+[-: ]*=>
|
||||
|
||||
add_to_extra_titlepage_entries:,tagsfromtitle
|
||||
add_to_extra_titlepage_entries:,tagsfromtitle,forumtags
|
||||
|
||||
## '.SPLIT' tells the system to split by ','
|
||||
add_to_include_subject_tags:,tagsfromtitle.SPLIT
|
||||
## Or to also include forumtags add this to personal.ini:
|
||||
#add_to_include_subject_tags:,forumtags
|
||||
|
||||
## base_xenforoforum reads Published and Updated datetimes from
|
||||
## Threadmarks if used, or from the posted & updated times of the
|
||||
@@ -1851,6 +1856,15 @@ rating_titles: R=RESTRICTED (16+), E=EXEMPT (18+), I=ART HOUSE, T=To every, A=IN
|
||||
adult_ratings: E,R
|
||||
|
||||
[www.mediaminer.org]
|
||||
dateUpdated_format:%%Y-%%m-%%d %%H:%%M:%%S
|
||||
## Note that mediaminer doesn't give datePublished on the story's
|
||||
## index page--it's collected from the earliest uploaded chapter. So
|
||||
## it's not available when only fetching metadata.
|
||||
datePublished_format:%%Y-%%m-%%d %%H:%%M:%%S
|
||||
|
||||
## some sites include images that we don't ever want becoming the
|
||||
## cover image. This lets you exclude them.
|
||||
cover_exclusion_regexp:/img/rss.png
|
||||
|
||||
[www.midnightwhispers.ca]
|
||||
## Some sites do not require a login, but do require the user to
|
||||
@@ -1918,6 +1932,13 @@ extracategories:Psych
|
||||
## Site dedicated to these categories/characters/ships
|
||||
extracategories:Queer as Folk
|
||||
|
||||
[quotev.com]
|
||||
extra_valid_entries:pages,readers,reads,favorites
|
||||
pages_label:Pages
|
||||
readers_label:Readers
|
||||
reads_label:Reads
|
||||
favorites_label:Favorites
|
||||
|
||||
[www.restrictedsection.org]
|
||||
## Some sites require login (or login for some rated stories) The
|
||||
## program can prompt you, or you can save it in config. In
|
||||
|
||||
+52
-28
@@ -41,7 +41,7 @@ def get_update_data(inputio,
|
||||
|
||||
## Save the path to the .opf file--hrefs inside it are relative to it.
|
||||
relpath = get_path_part(rootfilename)
|
||||
|
||||
|
||||
oldcover = None
|
||||
calibrebookmark = None
|
||||
logfile = None
|
||||
@@ -158,20 +158,20 @@ def get_update_data(inputio,
|
||||
chapterorigtitle = soup.find('meta',{'name':'chapterorigtitle'})
|
||||
if chapterorigtitle:
|
||||
datamaps[currenturl]['chapterorigtitle'] = chapterorigtitle['content']
|
||||
|
||||
|
||||
chaptertitle = soup.find('meta',{'name':'chaptertitle'})
|
||||
if chaptertitle:
|
||||
datamaps[currenturl]['chaptertitle'] = chaptertitle['content']
|
||||
|
||||
|
||||
soups.append(bodysoup)
|
||||
|
||||
|
||||
filecount+=1
|
||||
|
||||
try:
|
||||
calibrebookmark = epub.read("META-INF/calibre_bookmarks.txt")
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
#for k in images.keys():
|
||||
#print("\tlongdesc:%s\n\tData len:%s\n"%(k,len(images[k])))
|
||||
# print("datamaps:%s"%datamaps)
|
||||
@@ -199,7 +199,7 @@ def get_story_url_from_html(inputio,_is_good_url=None):
|
||||
|
||||
## Save the path to the .opf file--hrefs inside it are relative to it.
|
||||
relpath = get_path_part(rootfilename)
|
||||
|
||||
|
||||
# spin through the manifest--only place there are item tags.
|
||||
for item in contentdom.getElementsByTagName("item"):
|
||||
# First, count the 'chapter' files. FFF uses file0000.xhtml,
|
||||
@@ -225,7 +225,7 @@ def reset_orig_chapters_epub(inputio,outfile):
|
||||
|
||||
## build zip in memory in case updating in place(CLI).
|
||||
zipio = StringIO()
|
||||
|
||||
|
||||
## Write mimetype file, must be first and uncompressed.
|
||||
## Older versions of python(2.4/5) don't allow you to specify
|
||||
## compression by individual file.
|
||||
@@ -240,19 +240,28 @@ def reset_orig_chapters_epub(inputio,outfile):
|
||||
outputepub.debug = 3
|
||||
|
||||
changed = False
|
||||
|
||||
|
||||
unmerge_tocncxdoms = {}
|
||||
## spin through file contents, saving any unmerge toc.ncx files.
|
||||
for zf in inputepub.namelist():
|
||||
## logger.debug("zf:%s"%zf)
|
||||
if zf.endswith('/toc.ncx'):
|
||||
## logger.debug("toc.ncx zf:%s"%zf)
|
||||
unmerge_tocncxdoms[zf] = parseString(inputepub.read(zf))
|
||||
|
||||
tocncxdom = parseString(inputepub.read('toc.ncx'))
|
||||
## spin through file contents.
|
||||
for zf in inputepub.namelist():
|
||||
if zf not in ['mimetype','toc.ncx'] :
|
||||
if zf not in ['mimetype','toc.ncx'] and not zf.endswith('/toc.ncx'):
|
||||
entrychanged = False
|
||||
data = inputepub.read(zf)
|
||||
# if isinstance(data,unicode):
|
||||
# logger.debug("\n\n\ndata is unicode\n\n\n")
|
||||
if re.match(r'.*/file\d+\.xhtml',zf):
|
||||
#logger.debug("zf:%s"%zf)
|
||||
data = data.decode('utf-8')
|
||||
soup = bs.BeautifulSoup(data,"html5lib")
|
||||
|
||||
|
||||
chapterorigtitle = None
|
||||
tag = soup.find('meta',{'name':'chapterorigtitle'})
|
||||
if tag:
|
||||
@@ -265,7 +274,7 @@ def reset_orig_chapters_epub(inputio,outfile):
|
||||
chaptertoctitle = tag['content']
|
||||
elif chapterorigtitle:
|
||||
chaptertoctitle = chapterorigtitle
|
||||
|
||||
|
||||
chaptertitle = None
|
||||
tag = soup.find('meta',{'name':'chaptertitle'})
|
||||
if tag:
|
||||
@@ -281,34 +290,33 @@ def reset_orig_chapters_epub(inputio,outfile):
|
||||
|
||||
entrychanged = ( origdata != data )
|
||||
changed = changed or entrychanged
|
||||
|
||||
|
||||
if entrychanged:
|
||||
## go after the TOC entry, too.
|
||||
# <navPoint id="file0005" playOrder="6">
|
||||
# <navLabel>
|
||||
# <text>5. (new) Chapter 4</text>
|
||||
# </navLabel>
|
||||
# <content src="OEBPS/file0005.xhtml"/>
|
||||
# </navPoint>
|
||||
for contenttag in tocncxdom.getElementsByTagName("content"):
|
||||
if contenttag.getAttribute('src') == zf:
|
||||
texttag = contenttag.parentNode.getElementsByTagName('navLabel')[0].getElementsByTagName('text')[0]
|
||||
texttag.childNodes[0].replaceWholeText(chaptertoctitle)
|
||||
# logger.debug("text label:%s"%texttag.toxml())
|
||||
continue
|
||||
|
||||
_replace_tocncx(tocncxdom,zf,chaptertoctitle)
|
||||
## Also look for and update individual
|
||||
## book toc.ncx files for anthology in case
|
||||
## it's unmerged.
|
||||
zf_toc = zf[:zf.rfind('/OEBPS/')]+'/toc.ncx'
|
||||
mergedprefix_len = len(zf[:zf.rfind('/OEBPS/')])+1
|
||||
|
||||
if zf_toc in unmerge_tocncxdoms:
|
||||
_replace_tocncx(unmerge_tocncxdoms[zf_toc],zf[mergedprefix_len:],chaptertoctitle)
|
||||
|
||||
outputepub.writestr(zf,data.encode('utf-8'))
|
||||
else:
|
||||
# possibly binary data, thus no .encode().
|
||||
outputepub.writestr(zf,data)
|
||||
|
||||
for tocnm, tocdom in unmerge_tocncxdoms.items():
|
||||
outputepub.writestr(tocnm,tocdom.toxml(encoding='utf-8'))
|
||||
|
||||
outputepub.writestr('toc.ncx',tocncxdom.toxml(encoding='utf-8'))
|
||||
outputepub.close()
|
||||
# declares all the files created by Windows. otherwise, when
|
||||
# it runs in appengine, windows unzips the files as 000 perms.
|
||||
for zf in outputepub.filelist:
|
||||
zf.create_system = 0
|
||||
|
||||
|
||||
# only *actually* write if changed.
|
||||
if changed:
|
||||
if isinstance(outfile,basestring):
|
||||
@@ -319,5 +327,21 @@ def reset_orig_chapters_epub(inputio,outfile):
|
||||
|
||||
inputepub.close()
|
||||
zipio.close()
|
||||
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
def _replace_tocncx(tocncxdom,zf,chaptertoctitle):
|
||||
## go after the TOC entry, too.
|
||||
# <navPoint id="file0005" playOrder="6">
|
||||
# <navLabel>
|
||||
# <text>5. (new) Chapter 4</text>
|
||||
# </navLabel>
|
||||
# <content src="OEBPS/file0005.xhtml"/>
|
||||
# </navPoint>
|
||||
for contenttag in tocncxdom.getElementsByTagName("content"):
|
||||
if contenttag.getAttribute('src') == zf:
|
||||
texttag = contenttag.parentNode.getElementsByTagName('navLabel')[0].getElementsByTagName('text')[0]
|
||||
texttag.childNodes[0].replaceWholeText(chaptertoctitle)
|
||||
#logger.debug("text label:%s"%texttag.toxml())
|
||||
continue
|
||||
|
||||
+5
-2
@@ -927,9 +927,12 @@ class Story(Configurable):
|
||||
|
||||
if not allowunsafefilename:
|
||||
values={}
|
||||
pattern = re_compile(self.getConfig("output_filename_safepattern",r"[^a-zA-Z0-9_\. \[\]\(\)&'-]+"),"output_filename_safepattern")
|
||||
pattern = re_compile(self.getConfig("output_filename_safepattern",r"(^\.|/\.|[^a-zA-Z0-9_\. \[\]\(\)&'-]+)"),"output_filename_safepattern")
|
||||
for k in origvalues.keys():
|
||||
values[k]=re.sub(pattern,'_', removeAllEntities(self.getMetadata(k)))
|
||||
if k == 'formatext': # don't do file extension--we set it anyway.
|
||||
values[k]=self.getMetadata(k)
|
||||
else:
|
||||
values[k]=re.sub(pattern,'_', removeAllEntities(self.getMetadata(k)))
|
||||
|
||||
return string.Template(template).substitute(values).encode('utf8')
|
||||
|
||||
|
||||
@@ -10,13 +10,11 @@ https://github.com/pypa/sampleproject
|
||||
# Always prefer setuptools over distutils
|
||||
from setuptools import setup, find_packages
|
||||
# To use a consistent encoding
|
||||
from codecs import open
|
||||
import codecs
|
||||
from os import path
|
||||
|
||||
here = path.abspath(path.dirname(__file__))
|
||||
|
||||
# Get the long description from the relevant file
|
||||
with open(path.join(here, 'DESCRIPTION.rst'), encoding='utf-8') as f:
|
||||
with codecs.open('DESCRIPTION.rst', encoding='utf-8') as f:
|
||||
long_description = f.read()
|
||||
|
||||
setup(
|
||||
@@ -25,7 +23,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.11",
|
||||
version="2.2.13",
|
||||
|
||||
description='A tool for downloading fanfiction to eBook formats',
|
||||
long_description=long_description,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# ffd-retief-hrd fanficfare
|
||||
application: fanficfare
|
||||
version: 2-2-11
|
||||
version: 2-2-13
|
||||
runtime: python27
|
||||
api_version: 1
|
||||
threadsafe: true
|
||||
|
||||
+10
-36
@@ -27,43 +27,17 @@
|
||||
much easier. </p>
|
||||
</div>
|
||||
|
||||
<h3>New Name and URL</h3>
|
||||
<p>
|
||||
This version changes the name of the project from
|
||||
FanFictionDownLoader(FFDL) to FanFicFare. Along with
|
||||
the name change, there are changes to the internal
|
||||
structuring of the project code, but not to the functionality.
|
||||
</p>
|
||||
<p>
|
||||
If you had saved configuration (including your
|
||||
username/password for fic sites) in FFDL, you will need to
|
||||
copy it over to FanFicFare. Visit
|
||||
the <a href="http://fanfictiondownloader.appspot.com/editconfig">FFDL
|
||||
User Config</a> page, copy your settings and then paste
|
||||
them on
|
||||
the <a href="http://fanficfare.appspot.com/editconfig">FanFicFare
|
||||
User Config</a> page.
|
||||
</p>
|
||||
<h3>Changes:</h3>
|
||||
<ul>
|
||||
<li>Add chapter limits with URL by giving chapter range.<br>
|
||||
Examples:<br>
|
||||
<ul>
|
||||
<li>https://www.fanfiction.net/s/2565609/1/[4] <i>Chapter 4 only</i></li>
|
||||
<li>https://www.fanfiction.net/s/2565609/1/[6-10] <i>Chapters 6, 7, 8, 9 & 10 only</i></li>
|
||||
<li>https://www.fanfiction.net/s/2565609/1/[-10] <i>Chapters 1-10 only</i></li>
|
||||
<li>https://www.fanfiction.net/s/2565609/1/[150-] <i>Chapters 150 and up only</i></li>
|
||||
</ul>
|
||||
The chapter range will be included in the ebook title. Can be changed or disabled in config with title_chapter_range_pattern.
|
||||
</li>
|
||||
<li>New base_xenforoforum adapter type. The details are complex enough that I've started a <a href="https://github.com/JimmXinu/FanFicFare/wiki/BaseXenForoForumAdapters">new wiki page</a> for future reference. Please refer to it for more details.</li>
|
||||
<li>New [base_efiction] and [base_xenforoforum] sections for common settings for eFiction Base and XenForoForum Base adapters respectively.</li>
|
||||
<li>New site: <a href="https://forums.spacebattles.com/forums/creative-writing.18/">forums.spacebattles.com</a> base_xenforoforum adapter</li>
|
||||
<li>New site: <a href="https://forums.sufficientvelocity.com/forums/user-fiction.2/">forums.sufficientvelocity.com</a> base_xenforoforum adapter</li>
|
||||
<li>New site: <a href="http://ninelives.dark-solace.org/">ninelives.dark-solace.org</a> base_efiction adapter</li>
|
||||
<li>New description_limit feature to explicitly limit the allowable length of the description.</li>
|
||||
<li>Fixes for http://spikeluver.com</li>
|
||||
<li>New Russian language site: masseffect2.in -- Thanks to PlushBeaver for adding this.</li>
|
||||
<li>New site: <a href="http://www.quotev.com/">quotev.com</a>, thanks cryzed!</li>
|
||||
<li>Convert dark-solace.org(Elysium fields) to eFiction Base adapter to correct for site changes.</li>
|
||||
<li>Fix eFiction Base Author URLs.</li>
|
||||
<li>Switch chapter #post-12345 chapter URLs to permalinks for XenForoForum Base adapters.</li>
|
||||
<li>Record forum thread tags in XenForoForum Base adapters. Appears on title page by default, but not calibre tags.</li>
|
||||
<li>Change output_filename_safepattern to replace leading '.' for benefit of *nix OSes.</li>
|
||||
<li>Fix mediaminer.org for site changes.</li>
|
||||
<li>Fix to hpfandom.net change story URL when given chapter URL instead of story start URL.</li>
|
||||
<li>Allow forum.questionablequesting.com for questionablequesting.com.</li>
|
||||
</ul>
|
||||
<p>
|
||||
Questions? Check out our
|
||||
@@ -73,7 +47,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-10.fanficfare.appspot.com">previous version
|
||||
<a href="http://2-2-12.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