diff --git a/app.yaml b/app.yaml index 1fd9e75..c1eec87 100644 --- a/app.yaml +++ b/app.yaml @@ -1,6 +1,6 @@ # ffd-retief-hrd fanfictiondownloader application: fanfictiondownloader -version: 4-3-2 +version: 4-4-0 runtime: python27 api_version: 1 threadsafe: true diff --git a/calibre-plugin/__init__.py b/calibre-plugin/__init__.py index af7832c..a9255c4 100644 --- a/calibre-plugin/__init__.py +++ b/calibre-plugin/__init__.py @@ -27,7 +27,7 @@ class FanFictionDownLoaderBase(InterfaceActionBase): description = 'UI plugin to download FanFiction stories from various sites.' supported_platforms = ['windows', 'osx', 'linux'] author = 'Jim Miller' - version = (1, 4, 6) + version = (1, 5, 7) minimum_calibre_version = (0, 8, 30) #: This field defines the GUI plugin class that contains all the code diff --git a/calibre-plugin/about.txt b/calibre-plugin/about.txt index b63c1ac..2dbfe0b 100644 --- a/calibre-plugin/about.txt +++ b/calibre-plugin/about.txt @@ -16,5 +16,5 @@ The official distro channel for this plugin is there: here. +The source for this plugin is available at it's +project home. diff --git a/calibre-plugin/config.py b/calibre-plugin/config.py index 50cfb3c..cf46574 100644 --- a/calibre-plugin/config.py +++ b/calibre-plugin/config.py @@ -35,30 +35,39 @@ all_prefs = JSONConfig('plugins/fanfictiondownloader_plugin') # Set defaults used by all. Library specific settings continue to # take from here. all_prefs.defaults['personal.ini'] = get_resources('plugin-example.ini') + all_prefs.defaults['updatemeta'] = True +all_prefs.defaults['updatecover'] = False all_prefs.defaults['keeptags'] = False all_prefs.defaults['urlsfromclip'] = True all_prefs.defaults['updatedefault'] = True all_prefs.defaults['fileform'] = 'epub' all_prefs.defaults['collision'] = OVERWRITE all_prefs.defaults['deleteotherforms'] = False +all_prefs.defaults['adddialogstaysontop'] = False +all_prefs.defaults['includeimages'] = False + all_prefs.defaults['send_lists'] = '' all_prefs.defaults['read_lists'] = '' all_prefs.defaults['addtolists'] = False all_prefs.defaults['addtoreadlists'] = False all_prefs.defaults['addtolistsonread'] = False + all_prefs.defaults['custom_cols'] = {} # The list of settings to copy from all_prefs or the previous library # when config is called for the first time on a library. copylist = ['personal.ini', 'updatemeta', + 'updatecover', 'keeptags', 'urlsfromclip', 'updatedefault', 'fileform', 'collision', - 'deleteotherforms'] + 'deleteotherforms', + 'adddialogstaysontop', + 'includeimages'] # fake out so I don't have to change the prefs calls anywhere. The # Java programmer in me is offended by op-overloading, but it's very @@ -144,10 +153,13 @@ class ConfigWidget(QWidget): prefs['fileform'] = unicode(self.basic_tab.fileform.currentText()) prefs['collision'] = unicode(self.basic_tab.collision.currentText()) prefs['updatemeta'] = self.basic_tab.updatemeta.isChecked() + prefs['updatecover'] = self.basic_tab.updatecover.isChecked() prefs['keeptags'] = self.basic_tab.keeptags.isChecked() prefs['urlsfromclip'] = self.basic_tab.urlsfromclip.isChecked() prefs['updatedefault'] = self.basic_tab.updatedefault.isChecked() prefs['deleteotherforms'] = self.basic_tab.deleteotherforms.isChecked() + prefs['adddialogstaysontop'] = self.basic_tab.adddialogstaysontop.isChecked() + prefs['includeimages'] = self.basic_tab.includeimages.isChecked() if self.list_tab: # lists @@ -234,6 +246,11 @@ class BasicTab(QWidget): self.updatemeta.setChecked(prefs['updatemeta']) self.l.addWidget(self.updatemeta) + self.updatecover = QCheckBox('Update Cover when Updating Metadata?',self) + self.updatecover.setToolTip("Update cover image from EPUB when metadata is updated. (EPUB only.)\nDoesn't go looking for new images on 'Update Calibre Metadata Only'.") + self.updatecover.setChecked(prefs['updatecover']) + self.l.addWidget(self.updatecover) + self.keeptags = QCheckBox('Keep Existing Tags when Updating Metadata?',self) self.keeptags.setToolTip('Existing tags will be kept and any new tags added.\nCompleted and In-Progress tags will be still be updated, if known.\nLast Updated tags will be updated if lastupdate in include_subject_tags.') self.keeptags.setChecked(prefs['keeptags']) @@ -255,6 +272,17 @@ class BasicTab(QWidget): self.deleteotherforms.setChecked(prefs['deleteotherforms']) self.l.addWidget(self.deleteotherforms) + self.adddialogstaysontop = QCheckBox("Keep 'Add New from URL(s)' dialog on top?",self) + self.adddialogstaysontop.setToolTip("Instructs the OS and Window Manager to keep the 'Add New from URL(s)'\ndialog on top of all other windows. Useful for dragging URLs onto it.") + self.adddialogstaysontop.setChecked(prefs['adddialogstaysontop']) + self.l.addWidget(self.adddialogstaysontop) + + # this is a cheat to make it easier for users to realize there's a new include_images features. + self.includeimages = QCheckBox("Include images in EPUBs?",self) + self.includeimages.setToolTip("Download and include images in EPUB stories. This is equivalent to adding:\n\n[defaults]\ninclude_images:true\nkeep_summary_html:true\nmake_firstimage_cover:true\n\n ...to the top of personal.ini. Your settings in personal.ini will override this.") + self.includeimages.setChecked(prefs['includeimages']) + self.l.addWidget(self.includeimages) + self.l.insertStretch(-1) def set_collisions(self): diff --git a/calibre-plugin/dcsource.py b/calibre-plugin/dcsource.py deleted file mode 100644 index 0391041..0000000 --- a/calibre-plugin/dcsource.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai -from __future__ import (unicode_literals, division, absolute_import, - print_function) - -__license__ = 'GPL v3' -__copyright__ = '2012, Jim Miller' -__docformat__ = 'restructuredtext en' - -from zipfile import ZipFile - -from xml.dom.minidom import parseString - -def get_dcsource(inputio): - epub = ZipFile(inputio, 'r') - - ## Find the .opf file. - container = epub.read("META-INF/container.xml") - containerdom = parseString(container) - rootfilenodelist = containerdom.getElementsByTagName("rootfile") - rootfilename = rootfilenodelist[0].getAttribute("full-path") - - metadom = parseString(epub.read(rootfilename)) - firstmetadom = metadom.getElementsByTagName("metadata")[0] - try: - source=firstmetadom.getElementsByTagName("dc:source")[0].firstChild.data.encode("utf-8") - except: - source=None - - return source diff --git a/calibre-plugin/dialogs.py b/calibre-plugin/dialogs.py index cd6f276..87c8e41 100644 --- a/calibre-plugin/dialogs.py +++ b/calibre-plugin/dialogs.py @@ -13,7 +13,7 @@ from PyQt4 import QtGui from PyQt4.Qt import (QDialog, QTableWidget, QMessageBox, QVBoxLayout, QHBoxLayout, QGridLayout, QPushButton, QProgressDialog, QString, QLabel, QCheckBox, QIcon, QTextCursor, QTextEdit, QLineEdit, QInputDialog, QComboBox, QClipboard, QVariant, - QProgressDialog, QTimer, QDialogButtonBox, QPixmap, Qt,QAbstractItemView ) + QProgressDialog, QTimer, QDialogButtonBox, QPixmap, Qt, QAbstractItemView ) from calibre.gui2 import error_dialog, warning_dialog, question_dialog, info_dialog from calibre.gui2.dialogs.confirm_delete import confirm @@ -71,6 +71,9 @@ class AddNewDialog(SizePersistedDialog): SizePersistedDialog.__init__(self, gui, 'FanFictionDownLoader plugin:add new dialog') self.gui = gui + if prefs['adddialogstaysontop']: + QDialog.setWindowFlags ( self, Qt.Dialog|Qt.WindowStaysOnTopHint ) + self.setMinimumWidth(300) self.l = QVBoxLayout() self.setLayout(self.l) @@ -156,25 +159,40 @@ class AddNewDialog(SizePersistedDialog): def get_urlstext(self): return unicode(self.url.toPlainText()) - + + +class FakeLineEdit(): + def __init__(self): + pass + + def text(self): + pass + class UserPassDialog(QDialog): ''' Need to collect User/Pass for some sites. ''' - def __init__(self, gui, site): + def __init__(self, gui, site, exception=None): QDialog.__init__(self, gui) self.gui = gui self.status=False - self.setWindowTitle('User/Password') self.l = QGridLayout() self.setLayout(self.l) - self.l.addWidget(QLabel("%s requires you to login to download this story."%site),0,0,1,2) + if exception.passwdonly: + self.setWindowTitle('Password') + self.l.addWidget(QLabel("Author requires a password for this story(%s)."%exception.url),0,0,1,2) + # user isn't used, but it's easier to still have it for + # post processing. + self.user = FakeLineEdit() + 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("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) diff --git a/calibre-plugin/ffdl_plugin.py b/calibre-plugin/ffdl_plugin.py index 1900340..93f8320 100644 --- a/calibre-plugin/ffdl_plugin.py +++ b/calibre-plugin/ffdl_plugin.py @@ -15,6 +15,10 @@ from datetime import datetime from PyQt4.Qt import (QApplication, QMenu, QToolButton) +from PyQt4.Qt import QPixmap, Qt +from PyQt4.QtCore import QBuffer + + from calibre.ptempfile import PersistentTemporaryFile, PersistentTemporaryDirectory, remove_dir from calibre.ebooks.metadata import MetaInformation, authors_to_string from calibre.ebooks.metadata.meta import get_metadata @@ -30,8 +34,8 @@ from calibre_plugins.fanfictiondownloader_plugin.common_utils import (set_plugin create_menu_action_unique, get_library_uuid) from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters, writers, exceptions -from calibre_plugins.fanfictiondownloader_plugin.epubmerge import doMerge -from calibre_plugins.fanfictiondownloader_plugin.dcsource import get_dcsource +from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.htmlcleanup import stripHTML +from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.epubutils import get_dcsource, get_dcsource_chaptercount from calibre_plugins.fanfictiondownloader_plugin.config import (prefs, permitted_values) from calibre_plugins.fanfictiondownloader_plugin.dialogs import ( @@ -93,6 +97,8 @@ class FanFictionDownLoaderPlugin(InterfaceAction): # are not found in the zip file will result in null QIcons. icon = get_icon('images/icon.png') + #self.qaction.setText('FFDL') + # The qaction is automatically created from the action_spec defined # above self.qaction.setIcon(icon) @@ -194,9 +200,6 @@ class FanFictionDownLoaderPlugin(InterfaceAction): shortcut_name='About FanFictionDownLoader', triggered=self.about) - # self.update_action.setEnabled( len(self.gui.library_view.get_selected_ids()) > 0 ) - # self.get_list_action.setEnabled( len(self.gui.library_view.get_selected_ids()) > 0 ) - # Before we finalize, make sure we delete any actions for menus that are no longer displayed for menu_id, unique_name in self.old_actions_unique_map.iteritems(): if menu_id not in self.actions_unique_map: @@ -217,7 +220,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction): text = get_resources('about.txt') AboutDialog(self.gui,self.qaction.icon(),self.version + text).exec_() - + def create_menu_item_ex(self, parent_menu, menu_text, image=None, tooltip=None, shortcut=None, triggered=None, is_checked=None, shortcut_name=None, unique_name=None): @@ -236,7 +239,6 @@ class FanFictionDownLoaderPlugin(InterfaceAction): if len(self.gui.library_view.get_selected_ids()) > 0 and \ (prefs['addtolists'] or prefs['addtoreadlists']) : self._update_reading_lists(self.gui.library_view.get_selected_ids(),add) - #self.gui.library_view.model().refresh_ids(self.gui.library_view.get_selected_ids()) def get_list_urls(self): if len(self.gui.library_view.get_selected_ids()) > 0: @@ -408,18 +410,25 @@ class FanFictionDownLoaderPlugin(InterfaceAction): ffdlconfig = SafeConfigParser() ffdlconfig.readfp(StringIO(get_resources("plugin-defaults.ini"))) ffdlconfig.readfp(StringIO(prefs['personal.ini'])) - adapter = adapters.getAdapter(ffdlconfig,url) + adapter = adapters.getAdapter(ffdlconfig,url,fileform) options['personal.ini'] = prefs['personal.ini'] + if prefs['includeimages']: + # this is a cheat to make it easier for users. + options['personal.ini'] = '''[defaults] +include_images:true +keep_summary_html:true +make_firstimage_cover:true +''' + options['personal.ini'] ## three tries, that's enough if both user/pass & is_adult needed, ## or a couple tries of one or the other for x in range(0,2): try: adapter.getStoryMetadataOnly() - except exceptions.FailedToLogin: + except exceptions.FailedToLogin, f: print("Login Failed, Need Username/Password.") - userpass = UserPassDialog(self.gui,url) + userpass = UserPassDialog(self.gui,url,f) userpass.exec_() # exec_ will make it act modal if userpass.status: adapter.username = userpass.user.text() @@ -439,8 +448,8 @@ class FanFictionDownLoaderPlugin(InterfaceAction): book['title'] = story.getMetadata("title", removeallentities=True) book['author_sort'] = book['author'] = story.getMetadata("author", removeallentities=True) book['publisher'] = story.getMetadata("site") - book['tags'] = writer.getTags() - book['comments'] = story.getMetadata("description") #, removeallentities=True) comments handles entities better. + book['tags'] = writer.getTags() # getTags could be moved up into adapter now. Adapter didn't used to know the fileform + book['comments'] = stripHTML(story.getMetadata("description")) #, removeallentities=True) comments handles entities better. book['series'] = story.getMetadata("series") # adapter.opener is the element with a threadlock. But del @@ -517,13 +526,9 @@ class FanFictionDownLoaderPlugin(InterfaceAction): # 'book' can exist without epub. If there's no existing epub, # let it go and it will download it. if db.has_format(book_id,fileform,index_is_id=True): - toupdateio = StringIO() - (epuburl,chaptercount) = doMerge(toupdateio, - [StringIO(db.format(book_id,'EPUB', - index_is_id=True))], - titlenavpoints=False, - striptitletoc=True, - forceunique=False) + (epuburl,chaptercount) = \ + get_dcsource_chaptercount(StringIO(db.format(book_id,'EPUB', + index_is_id=True))) urlchaptercount = int(story.getMetadata('numChapters')) if chaptercount == urlchaptercount: if collision == UPDATE: @@ -630,7 +635,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction): if options['collision'] == CALIBREONLY or \ (options['updatemeta'] and book['good']): - self._update_metadata(db, book['calibre_id'], book, mi) + self._update_metadata(db, book['calibre_id'], book, mi, options) def _update_books_completed(self, book_list, options={}): @@ -649,6 +654,9 @@ class FanFictionDownLoaderPlugin(InterfaceAction): self.gui.library_view.model().current_changed(current, self.previous) self.gui.tags_view.recount() + if self.gui.cover_flow: + self.gui.cover_flow.dataChanged() + self.gui.status_bar.show_message(_('Finished Adding/Updating %d books.'%(len(update_list) + len(add_list))), 3000) if len(update_list) + len(add_list) != len(book_list): @@ -729,7 +737,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction): return book_id - def _update_metadata(self, db, book_id, book, mi): + def _update_metadata(self, db, book_id, book, mi, options): if prefs['keeptags']: old_tags = db.get_tags(book_id) # remove old Completed/In-Progress only if there's a new one. @@ -748,6 +756,13 @@ class FanFictionDownLoaderPlugin(InterfaceAction): oldmi = db.get_metadata(book_id,index_is_id=True) if not oldmi.languages: mi.languages=['eng'] + + if options['fileform'] == 'epub' and prefs['updatecover']: + existingepub = db.format(book_id,'EPUB',index_is_id=True, as_file=True) + epubmi = get_metadata(existingepub,'EPUB') + if epubmi.cover_data[1] is not None: + db.set_cover(book_id, epubmi.cover_data[1]) + #mi.cover = epubmi.cover_data[1] db.set_metadata(book_id,mi) @@ -780,7 +795,7 @@ class FanFictionDownLoaderPlugin(InterfaceAction): if meta == 'status-I': val = book['all_metadata']['status'] == 'In-Progress' db.set_custom(book_id, val, label=label, commit=False) - + db.commit() def _get_clean_reading_lists(self,lists): @@ -900,13 +915,6 @@ class FanFictionDownLoaderPlugin(InterfaceAction): book['added'] = False return book - - - # def _convert_calibre_ids_to_books(self, db, ids): - # books = [] - # for book_id in ids: - # books.append(self._convert_calibre_id_to_book(db,book_id)) - # return books def _populate_book_from_calibre_id(self, book, db=None): mi = db.get_metadata(book['calibre_id'], index_is_id=True) diff --git a/calibre-plugin/jobs.py b/calibre-plugin/jobs.py index 33372c5..55c9853 100644 --- a/calibre-plugin/jobs.py +++ b/calibre-plugin/jobs.py @@ -12,10 +12,7 @@ import time, os, traceback from ConfigParser import SafeConfigParser from StringIO import StringIO -#from itertools import izip -#from threading import Event -#from calibre.gui2.convert.single import sort_formats_by_preference from calibre.utils.ipc.server import Server from calibre.utils.ipc.job import ParallelJob from calibre.utils.logging import Log @@ -23,7 +20,7 @@ from calibre.utils.logging import Log from calibre_plugins.fanfictiondownloader_plugin.dialogs import (NotGoingToDownload, OVERWRITE, OVERWRITEALWAYS, UPDATE, UPDATEALWAYS, ADDNEW, SKIP, CALIBREONLY) from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters, writers, exceptions -from calibre_plugins.fanfictiondownloader_plugin.epubmerge import doMerge +from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.epubutils import get_update_data # ------------------------------------------------------------------------------ # @@ -110,7 +107,7 @@ def do_download_for_worker(book,options): ffdlconfig.readfp(StringIO(get_resources("plugin-defaults.ini"))) ffdlconfig.readfp(StringIO(options['personal.ini'])) - adapter = adapters.getAdapter(ffdlconfig,book['url']) + adapter = adapters.getAdapter(ffdlconfig,book['url'],options['fileform']) adapter.is_adult = book['is_adult'] adapter.username = book['username'] adapter.password = book['password'] @@ -135,39 +132,17 @@ def do_download_for_worker(book,options): ## checks were done earlier, just update it. elif 'epub_for_update' in book and options['collision'] in (UPDATE, UPDATEALWAYS): + # update now handled by pre-populating the old images and + # chapters in the adapter rather than merging epubs. urlchaptercount = int(story.getMetadata('numChapters')) - ## First, get existing epub with titlepage and tocpage stripped. - updateio = StringIO() - (epuburl,chaptercount) = doMerge(updateio, - [book['epub_for_update']], - titlenavpoints=False, - striptitletoc=True, - forceunique=False) + (url,chaptercount, + adapter.oldchapters, + adapter.oldimgs) = get_update_data(book['epub_for_update']) + print("Do update - epub(%d) vs url(%d)" % (chaptercount, urlchaptercount)) print("write to %s"%outfile) - ## Get updated title page/metadata by itself in an epub. - ## Even if the title page isn't included, this carries the metadata. - titleio = StringIO() - writer.writeStory(outstream=titleio,metaonly=True) - - newchaptersio = None - if urlchaptercount > chaptercount : - ## Go get the new chapters - newchaptersio = StringIO() - adapter.setChaptersRange(chaptercount+1,urlchaptercount) - - adapter.config.set("overrides",'include_tocpage','false') - adapter.config.set("overrides",'include_titlepage','false') - writer.writeStory(outstream=newchaptersio) - - ## Merge the three epubs together. - doMerge(outfile, - [titleio,updateio,newchaptersio], - fromfirst=True, - titlenavpoints=False, - striptitletoc=False, - forceunique=False) + writer.writeStory(outfilename=outfile, forceOverwrite=True) book['comment'] = 'Update %s completed, added %s chapters for %s total.'%\ (options['fileform'],(urlchaptercount-chaptercount),urlchaptercount) diff --git a/defaults.ini b/defaults.ini index 5b623f9..9c793a2 100644 --- a/defaults.ini +++ b/defaults.ini @@ -126,7 +126,6 @@ extratags: FanFiction ## number of seconds to sleep between calls to the story site. May by ## useful if pulling large numbers of stories or if the site is slow. -## Primarily for commandline. #slow_down_sleep_time:0.5 ## For use only with stand-alone CLI version--run a command on the @@ -148,6 +147,11 @@ extratags: FanFiction # (.*)Great(.*)=>\1Moderate\2 # .*-Centered=> +## Some readers don't show horizontal rule (
w/o
) leads # to all labels being included. svalue=svalue[:svalue.find('')] - self.story.setMetadata('description',stripHTML(svalue)) + self.setDescription(url,svalue) + #self.story.setMetadata('description',stripHTML(svalue)) if 'Rated' in label: self.story.setMetadata('rating', value) @@ -220,7 +221,7 @@ class AdAstraFanficComSiteAdapter(BaseSiteAdapter): if None == span: raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) - return utf8FromSoup(span) + return self.utf8FromSoup(url,span) def getClass(): return AdAstraFanficComSiteAdapter diff --git a/fanficdownloader/adapters/adapter_archiveofourownorg.py b/fanficdownloader/adapters/adapter_archiveofourownorg.py index 76b0b03..ede2a02 100644 --- a/fanficdownloader/adapters/adapter_archiveofourownorg.py +++ b/fanficdownloader/adapters/adapter_archiveofourownorg.py @@ -24,7 +24,7 @@ from .. import BeautifulSoup as bs from ..htmlcleanup import stripHTML from .. import exceptions as exceptions -from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate +from base_adapter import BaseSiteAdapter, makeDate def getClass(): return ArchiveOfOurOwnOrgAdapter @@ -126,7 +126,8 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter): a = metasoup.find('blockquote',{'class':'userstuff'}) if a != None: - self.story.setMetadata('description',a.text) + self.setDescription(url,a.text) + #self.story.setMetadata('description',a.text) a = metasoup.find('dd',{'class':"rating tags"}) if a != None: @@ -216,7 +217,8 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter): logging.debug('Getting chapter text from: %s' % url) chapter=bs.BeautifulSoup('') - soup = bs.BeautifulSoup(self._fetchUrl(url),selfClosingTags=('br','hr')) + data = self._fetchUrl(url) + soup = bs.BeautifulSoup(data,selfClosingTags=('br','hr')) headnotes = soup.find('div', {'class' : "preface group"}).find('div', {'class' : "notes module"}) if headnotes != None: @@ -257,5 +259,5 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter): if None == soup: raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) - - return utf8FromSoup(chapter) + + return self.utf8FromSoup(url,chapter) diff --git a/fanficdownloader/adapters/adapter_castlefansorg.py b/fanficdownloader/adapters/adapter_castlefansorg.py index 4b8d40f..b53ec9b 100644 --- a/fanficdownloader/adapters/adapter_castlefansorg.py +++ b/fanficdownloader/adapters/adapter_castlefansorg.py @@ -24,7 +24,7 @@ from .. import BeautifulSoup as bs from ..htmlcleanup import stripHTML from .. import exceptions as exceptions -from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate +from base_adapter import BaseSiteAdapter, makeDate # By virtue of being recent and requiring both is_adult and user/pass, # adapter_fanficcastletvnet.py is the best choice for learning to @@ -218,7 +218,8 @@ class CastleFansOrgAdapter(BaseSiteAdapter): # XXX while not defaultGetattr(value,'class') == 'label': svalue += str(value) value = value.nextSibling - self.story.setMetadata('description',stripHTML(svalue)) + self.setDescription(url,svalue) + #self.story.setMetadata('description',stripHTML(svalue)) if 'Rated' in label: self.story.setMetadata('rating', value) @@ -305,4 +306,4 @@ class CastleFansOrgAdapter(BaseSiteAdapter): # XXX if None == div: raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) - return utf8FromSoup(div) + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_fanfictionnet.py b/fanficdownloader/adapters/adapter_fanfictionnet.py index 73c8f63..ee0d59c 100644 --- a/fanficdownloader/adapters/adapter_fanfictionnet.py +++ b/fanficdownloader/adapters/adapter_fanfictionnet.py @@ -24,7 +24,7 @@ import time from .. import BeautifulSoup as bs from .. import exceptions as exceptions -from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate +from base_adapter import BaseSiteAdapter, makeDate class FanFictionNetSiteAdapter(BaseSiteAdapter): @@ -153,7 +153,8 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter): if 'title_t' in var: self.story.setMetadata('title', value) if 'summary' in var: - self.story.setMetadata('description', value) + self.setDescription(url,value) + #self.story.setMetadata('description', value) if 'datep' in var: self.story.setMetadata('datePublished',makeDate(value, '%m-%d-%y')) if 'dateu' in var: @@ -270,7 +271,7 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter): logging.debug('div id=storytext not found. data:%s'%data) raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) - return utf8FromSoup(div) + return self.utf8FromSoup(url,div) def getClass(): return FanFictionNetSiteAdapter diff --git a/fanficdownloader/adapters/adapter_ficbooknet.py b/fanficdownloader/adapters/adapter_ficbooknet.py index c9a98bc..5e2154f 100644 --- a/fanficdownloader/adapters/adapter_ficbooknet.py +++ b/fanficdownloader/adapters/adapter_ficbooknet.py @@ -26,7 +26,7 @@ from .. import BeautifulSoup as bs from ..htmlcleanup import stripHTML from .. import exceptions as exceptions -from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate +from base_adapter import BaseSiteAdapter, makeDate def getClass(): @@ -201,7 +201,8 @@ class FicBookNetAdapter(BaseSiteAdapter): break summary=soup.find('span', {'class' : 'urlize'}) - self.story.setMetadata('description', summary.text) + self.setDescription(url,summary.text) + #self.story.setMetadata('description', summary.text) # grab the text for an individual chapter. def getChapterText(self, url): @@ -218,4 +219,4 @@ class FicBookNetAdapter(BaseSiteAdapter): if None == chapter: raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) - return utf8FromSoup(chapter) + return self.utf8FromSoup(url,chapter) diff --git a/fanficdownloader/adapters/adapter_fictionalleyorg.py b/fanficdownloader/adapters/adapter_fictionalleyorg.py index fba4411..b374c5d 100644 --- a/fanficdownloader/adapters/adapter_fictionalleyorg.py +++ b/fanficdownloader/adapters/adapter_fictionalleyorg.py @@ -25,7 +25,7 @@ from .. import BeautifulSoup as bs from ..htmlcleanup import stripHTML from .. import exceptions as exceptions -from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate +from base_adapter import BaseSiteAdapter, makeDate class FictionAlleyOrgSiteAdapter(BaseSiteAdapter): @@ -187,7 +187,8 @@ class FictionAlleyOrgSiteAdapter(BaseSiteAdapter): for small in storydd.findAll('small'): small.extract() ## removes the tags, leaving only the summary. - self.story.setMetadata('description',stripHTML(storydd)) + self.setDescription(url,storydd) + #self.story.setMetadata('description',stripHTML(storydd)) return @@ -223,7 +224,7 @@ class FictionAlleyOrgSiteAdapter(BaseSiteAdapter): if not data or not text: raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) - return utf8FromSoup(text) + return self.utf8FromSoup(url,text) def getClass(): return FictionAlleyOrgSiteAdapter diff --git a/fanficdownloader/adapters/adapter_ficwadcom.py b/fanficdownloader/adapters/adapter_ficwadcom.py index e556041..2adfeae 100644 --- a/fanficdownloader/adapters/adapter_ficwadcom.py +++ b/fanficdownloader/adapters/adapter_ficwadcom.py @@ -26,7 +26,7 @@ from .. import BeautifulSoup as bs from .. import exceptions as exceptions from ..htmlcleanup import stripHTML -from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate +from base_adapter import BaseSiteAdapter, makeDate class FicwadComSiteAdapter(BaseSiteAdapter): @@ -124,7 +124,8 @@ class FicwadComSiteAdapter(BaseSiteAdapter): # description storydiv = soup.find("div",{"id":"story"}) - self.story.setMetadata('description', storydiv.find("blockquote",{'class':'summary'}).p.string) + self.setDescription(url,storydiv.find("blockquote",{'class':'summary'}).p.string) + #self.story.setMetadata('description', storydiv.find("blockquote",{'class':'summary'}).p.string) # most of the meta data is here: metap = storydiv.find("p",{"class":"meta"}) @@ -209,7 +210,7 @@ class FicwadComSiteAdapter(BaseSiteAdapter): if None == span: raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) - return utf8FromSoup(span) + return self.utf8FromSoup(url,span) def getClass(): return FicwadComSiteAdapter diff --git a/fanficdownloader/adapters/adapter_fimfictionnet.py b/fanficdownloader/adapters/adapter_fimfictionnet.py index b829bea..cc58b51 100644 --- a/fanficdownloader/adapters/adapter_fimfictionnet.py +++ b/fanficdownloader/adapters/adapter_fimfictionnet.py @@ -26,7 +26,7 @@ from .. import BeautifulSoup as bs from ..htmlcleanup import stripHTML from .. import exceptions as exceptions -from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate +from base_adapter import BaseSiteAdapter, makeDate def getClass(): return FimFictionNetSiteAdapter @@ -83,9 +83,23 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter): if "Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result resource" in data: raise exceptions.StoryDoesNotExist(self.url) + + if "/images/missing_story.png" in data: + raise exceptions.StoryDoesNotExist(self.url) if "This story has been marked as having adult content." in data: raise exceptions.AdultCheckRequired(self.url) + + if self.password: + params = {} + params['password'] = self.password + data = self._postUrl(self.url,params) + + if "Enter the password the author set for this story to view it." in data: + if self.getConfig('fail_on_password'): + raise exceptions.FailedToDownload("%s requires story password and fail_on_password is true."%self.url) + else: + raise exceptions.FailedToLogin(self.url,"Story requires individual password",passwdonly=True) soup = bs.BeautifulSoup(data).find("div", {"class":"content_box post_content_box"}) @@ -102,7 +116,7 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter): for chapter in soup.findAll("a", {"class":"chapter_link"}): chapterDates.append(chapter.span.extract().text.strip("()")) self.chapterUrls.append((chapter.text.strip(), "http://"+self.getSiteDomain() + chapter['href'])) - + self.story.setMetadata('numChapters',len(self.chapterUrls)) for character in [character_icon['title'] for character_icon in soup.findAll("a", {"class":"character_icon"})]: @@ -141,7 +155,13 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter): description_soup.find('a', {"class":"more"}).extract() except: pass - self.story.setMetadata('description', description_soup.text) + + # fimfic is the first site with an explicit cover image. + story_img = soup.find('img',{'class':'story_image'}) + if self.getConfig('include_images') and story_img: + self.story.addImgUrl(self,self.url,story_img['src'],self._fetchUrlRaw,cover=True) + self.setDescription(self.url,description_soup.text) + #self.story.setMetadata('description', description_soup.text) # Unfortunately, nowhere on the page is the year mentioned. # Best effort to deal with this: @@ -151,25 +171,29 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter): now = datetime.datetime.now() - # Get the date of creation from the first chapter - datePublished_text = chapterDates[0] - day, month = datePublished_text.split() - day = re.sub(r"[^\d.]+", '', day) - datePublished = makeDate("%s%s%s"%(now.year,month,day), "%Y%b%d") - if datePublished > now : - datePublished = datePublished.replace(year=now.year-1) - self.story.setMetadata("datePublished", datePublished) dateUpdated_soup = bs.BeautifulSoup(data).find("div", {"class":"calendar"}) dateUpdated_soup.find('span').extract() dateUpdated = makeDate("%s%s"%(now.year,dateUpdated_soup.text), "%Y%b%d") if dateUpdated > now : - dateUpdated = datePublished.replace(year=now.year-1) + dateUpdated = dateUpdated.replace(year=now.year-1) self.story.setMetadata("dateUpdated", dateUpdated) + # Get the date of creation from the first chapter + if len(chapterDates) > 0: + datePublished_text = chapterDates[0] + day, month = datePublished_text.split() + day = re.sub(r"[^\d.]+", '', day) + datePublished = makeDate("%s%s%s"%(now.year,month,day), "%Y%b%d") + if datePublished > now : + datePublished = datePublished.replace(year=now.year-1) + self.story.setMetadata("datePublished", datePublished) + else: + self.story.setMetadata("datePublished", dateUpdated) + def getChapterText(self, url): logging.debug('Getting chapter text from: %s' % url) soup = bs.BeautifulSoup(self._fetchUrl(url),selfClosingTags=('br','hr')).find('div', {'id' : 'chapter_container'}) if soup == None: raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) - return utf8FromSoup(soup) + return self.utf8FromSoup(url,soup) diff --git a/fanficdownloader/adapters/adapter_gayauthorsorg.py b/fanficdownloader/adapters/adapter_gayauthorsorg.py index ab4984e..60f728d 100644 --- a/fanficdownloader/adapters/adapter_gayauthorsorg.py +++ b/fanficdownloader/adapters/adapter_gayauthorsorg.py @@ -25,7 +25,7 @@ from .. import BeautifulSoup as bs from ..htmlcleanup import stripHTML from .. import exceptions as exceptions -from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate +from base_adapter import BaseSiteAdapter, makeDate def getClass(): return GayAuthorsAdapter @@ -162,7 +162,8 @@ class GayAuthorsAdapter(BaseSiteAdapter): self.story.setMetadata('rating',rating.text) summary = msoup.find('span', {'itemprop' : 'description'}) - self.story.setMetadata('description',summary.text) + self.setDescription(self.url,summary.text) + #self.story.setMetadata('description',summary.text) stats = msoup.find('dl',{'class':'info'}) @@ -192,12 +193,13 @@ class GayAuthorsAdapter(BaseSiteAdapter): logging.debug('Getting chapter text from: %s' % url) - soup = bs.BeautifulStoneSoup(self._fetchUrl(url), - selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + data = self._fetchUrl(url) + data = data[data.index("Lorem '''+self.crazystring+''' italics, bold, underline consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
br breaks
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
- Thanks to Ida Leter's hard work, we now support gayauthors.org, a fanfiction site specializing in gay stories. + This web service has been having almost daily outages + recently due to problems with running out of quota for the + application. We are working to improve the situation. +
++ This most recent code update is mostly for new features in + the calibre plugin and CLI versions. However, the web + service does now also offer a a new option, 'replace_hr' + <hr> tags with '* * *' and support for + fimfiction.net's locked story passwords. When it asks for + user/pass, give the story password.
If you have any problems with this application, please report them in the FanFictionDownLoader Google Group. The - Previous Version is also available for you to use if necessary. + Previous Version is also available for you to use if necessary.