Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-fanficfare for openSUSE:Factory checked in at 2022-02-15 23:57:48 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-fanficfare (Old) and /work/SRC/openSUSE:Factory/.python-fanficfare.new.1956 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-fanficfare" Tue Feb 15 23:57:48 2022 rev:38 rq:955094 version:4.10.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-fanficfare/python-fanficfare.changes 2022-01-26 21:27:52.513623913 +0100 +++ /work/SRC/openSUSE:Factory/.python-fanficfare.new.1956/python-fanficfare.changes 2022-02-15 23:58:16.088376180 +0100 @@ -1,0 +2,27 @@ +Tue Feb 15 18:30:59 UTC 2022 - Matej Cepl <mc...@suse.com> + +- Upgrade to 4.10.0: + - adapter_fanfiktionde: Update where description comes from. + - ReadOnlyMindAdapter: Add series_tags feature to populate + series metadata (#803) - Thanks Nothorse + - Add use_flaresolverr_proxy:withimages option for FlareSolverr + v1 users. + - Correct use_flaresolverr_proxy error checking. + - adapter_royalroadcom: Add status 'Dropped' + - New Site: readonlymind.com, thanks Nothorse Issue #767 PR + #801 + - Force include_images:false when use_flaresolverr_proxy:true + -- FlareSolverr v2.2.0 crashes on image request. + - Remove defunct site: hpfanficarchive.com + - base_efiction: Add 'Igen' as equiv to 'Yes, Completed' in + Hungarian + - adapter_royalroadcom: Add status 'Stub' Closes #800 + - New site: merengo.hu (Hungarian), thanks estherflails + - Remove site: fanfic.hu (moved to merengo.hu, storyIds don't + appear to be the same) + - Fix for py2 for base_xenforoforum tagsfromtitle. Thanks hseg + for the help. See #791 + - Extend base_xenforoforum tagsfromtitle for ')(' '][' + - Changes for upcoming Qt6 Calibre + +------------------------------------------------------------------- Old: ---- FanFicFare-4.9.0.tar.gz New: ---- FanFicFare-4.10.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-fanficfare.spec ++++++ --- /var/tmp/diff_new_pack.vpnaj2/_old 2022-02-15 23:58:16.536377417 +0100 +++ /var/tmp/diff_new_pack.vpnaj2/_new 2022-02-15 23:58:16.540377428 +0100 @@ -21,7 +21,7 @@ %define skip_python2 1 %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-fanficfare -Version: 4.9.0 +Version: 4.10.0 Release: 0 Summary: Tool for making eBooks from stories on fanfiction and other web sites License: GPL-3.0-only @@ -34,6 +34,7 @@ BuildRequires: %{python_module cloudscraper} BuildRequires: %{python_module html2text} BuildRequires: %{python_module html5lib} +BuildRequires: %{python_module requests-file} BuildRequires: %{python_module setuptools >= 17.1} BuildRequires: dos2unix BuildRequires: fdupes @@ -43,6 +44,7 @@ Requires: python-cloudscraper Requires: python-html2text Requires: python-html5lib +Requires: python-requests-file Requires: python-setuptools Requires(post): update-alternatives Requires(postun):update-alternatives ++++++ FanFicFare-4.9.0.tar.gz -> FanFicFare-4.10.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/FanFicFare-4.9.0/calibre-plugin/__init__.py new/FanFicFare-4.10.0/calibre-plugin/__init__.py --- old/FanFicFare-4.9.0/calibre-plugin/__init__.py 2022-01-11 22:58:22.000000000 +0100 +++ new/FanFicFare-4.10.0/calibre-plugin/__init__.py 2022-02-14 16:39:42.000000000 +0100 @@ -33,7 +33,7 @@ from calibre.customize import InterfaceActionBase # pulled out from FanFicFareBase for saving in prefs.py -__version__ = (4, 9, 0) +__version__ = (4, 10, 0) ## Apparently the name for this class doesn't matter--it was still ## 'demo' for the first few versions. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/FanFicFare-4.9.0/calibre-plugin/basicinihighlighter.py new/FanFicFare-4.10.0/calibre-plugin/basicinihighlighter.py --- old/FanFicFare-4.9.0/calibre-plugin/basicinihighlighter.py 2022-01-11 22:58:22.000000000 +0100 +++ new/FanFicFare-4.10.0/calibre-plugin/basicinihighlighter.py 2022-02-14 16:39:42.000000000 +0100 @@ -9,10 +9,7 @@ import re -try: - from PyQt5.Qt import (Qt, QSyntaxHighlighter, QTextCharFormat, QBrush) -except ImportError as e: - from PyQt4.Qt import (Qt, QSyntaxHighlighter, QTextCharFormat, QBrush) +from PyQt5.Qt import (Qt, QSyntaxHighlighter, QTextCharFormat, QBrush) from fanficfare.six import string_types diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/FanFicFare-4.9.0/calibre-plugin/common_utils.py new/FanFicFare-4.10.0/calibre-plugin/common_utils.py --- old/FanFicFare-4.9.0/calibre-plugin/common_utils.py 2022-01-11 22:58:22.000000000 +0100 +++ new/FanFicFare-4.10.0/calibre-plugin/common_utils.py 2022-02-14 16:39:42.000000000 +0100 @@ -10,18 +10,10 @@ import os from contextlib import contextmanager -try: - from PyQt5 import QtWidgets as QtGui - from PyQt5.Qt import (QApplication, Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout, - QTableWidgetItem, QFont, QLineEdit, QComboBox, - QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime, - QTextEdit, QListWidget, QAbstractItemView, QCursor) -except ImportError as e: - from PyQt4 import QtGui - from PyQt4.Qt import (QApplication, Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout, - QTableWidgetItem, QFont, QLineEdit, QComboBox, - QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime, - QTextEdit, QListWidget, QAbstractItemView, QCursor) +from PyQt5.Qt import (QApplication, Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout, + QTableWidgetItem, QFont, QLineEdit, QComboBox, + QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime, + QTextEdit, QListWidget, QAbstractItemView, QCursor) from calibre.constants import iswindows, DEBUG from calibre.gui2 import UNDEFINED_QDATETIME, gprefs, info_dialog @@ -266,7 +258,7 @@ def __init__(self, text): if text is None: text = '' - QTableWidgetItem.__init__(self, text, QtGui.QTableWidgetItem.UserType) + QTableWidgetItem.__init__(self, text) self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled|Qt.ItemIsEditable) class ReadOnlyTableWidgetItem(QTableWidgetItem): @@ -274,14 +266,14 @@ def __init__(self, text): if text is None: text = '' - QTableWidgetItem.__init__(self, text, QtGui.QTableWidgetItem.UserType) + QTableWidgetItem.__init__(self, text) self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled) class RatingTableWidgetItem(QTableWidgetItem): def __init__(self, rating, is_read_only=False): - QTableWidgetItem.__init__(self, '', QtGui.QTableWidgetItem.UserType) + QTableWidgetItem.__init__(self, '') self.setData(Qt.DisplayRole, rating) if is_read_only: self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled) @@ -293,10 +285,10 @@ if date_read == UNDEFINED_DATE and default_to_today: date_read = now() if is_read_only: - QTableWidgetItem.__init__(self, format_date(date_read, None), QtGui.QTableWidgetItem.UserType) + QTableWidgetItem.__init__(self, format_date(date_read, None)) self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled) else: - QTableWidgetItem.__init__(self, '', QtGui.QTableWidgetItem.UserType) + QTableWidgetItem.__init__(self, '') self.setData(Qt.DisplayRole, QDateTime(date_read)) @@ -507,7 +499,6 @@ self.keys_list.setAlternatingRowColors(True) ml.addWidget(self.keys_list) self.value_text = QTextEdit(self) - self.value_text.setTabStopWidth(24) self.value_text.setReadOnly(True) ml.addWidget(self.value_text, 1) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/FanFicFare-4.9.0/calibre-plugin/config.py new/FanFicFare-4.10.0/calibre-plugin/config.py --- old/FanFicFare-4.9.0/calibre-plugin/config.py 2022-01-11 22:58:22.000000000 +0100 +++ new/FanFicFare-4.10.0/calibre-plugin/config.py 2022-02-14 16:39:42.000000000 +0100 @@ -15,33 +15,11 @@ import threading from collections import OrderedDict -try: - from PyQt5 import QtWidgets as QtGui - from PyQt5.Qt import (QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, - QLineEdit, QComboBox, QCheckBox, QPushButton, QTabWidget, - QScrollArea, QGroupBox, QButtonGroup, QRadioButton, - Qt) -except ImportError as e: - from PyQt4 import QtGui - from PyQt4.Qt import (QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, - QLineEdit, QComboBox, QCheckBox, QPushButton, QTabWidget, - QScrollArea, QGroupBox, QButtonGroup, QRadioButton, - Qt) -try: - from calibre.gui2 import QVariant - del QVariant -except ImportError: - is_qt4 = False - convert_qvariant = lambda x: x -else: - is_qt4 = True - def convert_qvariant(x): - vt = x.type() - if vt == x.String: - return unicode(x.toString()) - if vt == x.List: - return [convert_qvariant(i) for i in x.toList()] - return x.toPyObject() +from PyQt5 import QtWidgets as QtGui +from PyQt5.Qt import (QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, + QLineEdit, QComboBox, QCheckBox, QPushButton, QTabWidget, + QScrollArea, QGroupBox, QButtonGroup, QRadioButton, + Qt) from calibre.gui2 import dynamic, info_dialog from calibre.gui2.complete2 import EditWithComplete @@ -357,7 +335,7 @@ prefs['gcnewonly'] = self.calibrecover_tab.gcnewonly.isChecked() gc_site_settings = {} for (site,combo) in six.iteritems(self.calibrecover_tab.gc_dropdowns): - val = unicode(convert_qvariant(combo.itemData(combo.currentIndex()))) + val = unicode(combo.itemData(combo.currentIndex())) if val != 'none': gc_site_settings[site] = val #print("gc_site_settings[%s]:%s"%(site,gc_site_settings[site])) @@ -400,19 +378,19 @@ # Custom Columns tab # error column - prefs['errorcol'] = unicode(convert_qvariant(self.cust_columns_tab.errorcol.itemData(self.cust_columns_tab.errorcol.currentIndex()))) + prefs['errorcol'] = unicode(self.cust_columns_tab.errorcol.itemData(self.cust_columns_tab.errorcol.currentIndex())) prefs['save_all_errors'] = self.cust_columns_tab.save_all_errors.isChecked() # metadata column - prefs['savemetacol'] = unicode(convert_qvariant(self.cust_columns_tab.savemetacol.itemData(self.cust_columns_tab.savemetacol.currentIndex()))) + prefs['savemetacol'] = unicode(self.cust_columns_tab.savemetacol.itemData(self.cust_columns_tab.savemetacol.currentIndex())) # lastchecked column - prefs['lastcheckedcol'] = unicode(convert_qvariant(self.cust_columns_tab.lastcheckedcol.itemData(self.cust_columns_tab.lastcheckedcol.currentIndex()))) + prefs['lastcheckedcol'] = unicode(self.cust_columns_tab.lastcheckedcol.itemData(self.cust_columns_tab.lastcheckedcol.currentIndex())) # cust cols tab colsmap = {} for (col,combo) in six.iteritems(self.cust_columns_tab.custcol_dropdowns): - val = unicode(convert_qvariant(combo.itemData(combo.currentIndex()))) + val = unicode(combo.itemData(combo.currentIndex())) if val != 'none': colsmap[col] = val #print("colsmap[%s]:%s"%(col,colsmap[col])) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/FanFicFare-4.9.0/calibre-plugin/dialogs.py new/FanFicFare-4.10.0/calibre-plugin/dialogs.py --- old/FanFicFare-4.9.0/calibre-plugin/dialogs.py 2022-01-11 22:58:22.000000000 +0100 +++ new/FanFicFare-4.10.0/calibre-plugin/dialogs.py 2022-02-14 16:39:42.000000000 +0100 @@ -16,38 +16,19 @@ from datetime import datetime +from PyQt5 import QtWidgets as QtGui +from PyQt5 import QtCore +from PyQt5.Qt import (QApplication, QDialog, QWidget, QTableWidget, QVBoxLayout, QHBoxLayout, + QGridLayout, QPushButton, QFont, QLabel, QCheckBox, QIcon, + QLineEdit, QComboBox, QProgressDialog, QTimer, QDialogButtonBox, + QScrollArea, QPixmap, Qt, QAbstractItemView, QTextEdit, + pyqtSignal, QGroupBox, QFrame) try: - from PyQt5 import QtWidgets as QtGui - from PyQt5 import QtCore - from PyQt5.Qt import (QApplication, QDialog, QWidget, QTableWidget, QVBoxLayout, QHBoxLayout, - QGridLayout, QPushButton, QFont, QLabel, QCheckBox, QIcon, - QLineEdit, QComboBox, QProgressDialog, QTimer, QDialogButtonBox, - QScrollArea, QPixmap, Qt, QAbstractItemView, QTextEdit, - pyqtSignal, QGroupBox, QFrame) -except ImportError as e: - from PyQt4 import QtGui - from PyQt4 import QtCore - from PyQt4.Qt import (QApplication, QDialog, QWidget, QTableWidget, QVBoxLayout, QHBoxLayout, - QGridLayout, QPushButton, QFont, QLabel, QCheckBox, QIcon, - QLineEdit, QComboBox, QProgressDialog, QTimer, QDialogButtonBox, - QScrollArea, QPixmap, Qt, QAbstractItemView, QTextEdit, - pyqtSignal, QGroupBox, QFrame) - -try: - from calibre.gui2 import QVariant - del QVariant -except ImportError: - is_qt4 = False - convert_qvariant = lambda x: x -else: - is_qt4 = True - def convert_qvariant(x): - vt = x.type() - if vt == x.String: - return unicode(x.toString()) - if vt == x.List: - return [convert_qvariant(i) for i in x.toList()] - return x.toPyObject() + # qt6 Calibre v6+ + QTextEditNoWrap = QTextEdit.LineWrapMode.NoWrap +except: + # qt5 Calibre v2-5 + QTextEditNoWrap = QTextEdit.NoWrap from calibre.gui2 import gprefs show_download_options = 'fff:add new/update dialogs:show_download_options' @@ -249,7 +230,7 @@ self.url = DroppableQTextEdit(self) self.url.setToolTip("UrlTooltip") - self.url.setLineWrapMode(QTextEdit.NoWrap) + self.url.setLineWrapMode(QTextEditNoWrap) self.l.addWidget(self.url) self.groupbox = QGroupBox(_("Show Download Options")) @@ -1028,7 +1009,7 @@ books = [] #print("=========================\nbooks:%s"%self.books) for row in range(self.rowCount()): - rnum = convert_qvariant(self.item(row, 1).data(Qt.UserRole)) + rnum = self.item(row, 1).data(Qt.UserRole) book = self.books[rnum] books.append(book) return books @@ -1216,7 +1197,7 @@ rejectrows = [] for row in range(self.rejects_table.rowCount()): url = unicode(self.rejects_table.item(row, 0).text()).strip() - book_id =convert_qvariant(self.rejects_table.item(row, 0).data(Qt.UserRole)) + book_id =self.rejects_table.item(row, 0).data(Qt.UserRole) title = unicode(self.rejects_table.item(row, 1).text()).strip() auth = unicode(self.rejects_table.item(row, 2).text()).strip() note = unicode(self.rejects_table.cellWidget(row, 3).currentText()).strip() @@ -1226,7 +1207,7 @@ def get_reject_list_ids(self): rejectrows = [] for row in range(self.rejects_table.rowCount()): - book_id = convert_qvariant(self.rejects_table.item(row, 0).data(Qt.UserRole)) + book_id = self.rejects_table.item(row, 0).data(Qt.UserRole) if book_id: rejectrows.append(book_id) return rejectrows @@ -1261,7 +1242,7 @@ self.l.addWidget(self.label) self.textedit = QTextEdit(self) - self.textedit.setLineWrapMode(QTextEdit.NoWrap) + self.textedit.setLineWrapMode(QTextEditNoWrap) self.textedit.setReadOnly(read_only) self.textedit.setText(text) self.l.addWidget(self.textedit) @@ -1334,7 +1315,7 @@ entry_keywords=get_valid_entry_keywords(), ) - self.textedit.setLineWrapMode(QTextEdit.NoWrap) + self.textedit.setLineWrapMode(QTextEditNoWrap) try: self.textedit.setFont(QFont("Courier", parent.font().pointSize()+1)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/FanFicFare-4.9.0/calibre-plugin/fff_plugin.py new/FanFicFare-4.10.0/calibre-plugin/fff_plugin.py --- old/FanFicFare-4.9.0/calibre-plugin/fff_plugin.py 2022-01-11 22:58:22.000000000 +0100 +++ new/FanFicFare-4.10.0/calibre-plugin/fff_plugin.py 2022-02-14 16:39:42.000000000 +0100 @@ -40,10 +40,7 @@ import traceback from collections import defaultdict -try: - from PyQt5.Qt import (QApplication, QMenu, QTimer, QToolButton) -except ImportError as e: - from PyQt4.Qt import (QApplication, QMenu, QTimer, QToolButton) +from PyQt5.Qt import (QApplication, QMenu, QTimer, QToolButton) from calibre.constants import numeric_version as calibre_version diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/FanFicFare-4.9.0/calibre-plugin/inihighlighter.py new/FanFicFare-4.10.0/calibre-plugin/inihighlighter.py --- old/FanFicFare-4.9.0/calibre-plugin/inihighlighter.py 2022-01-11 22:58:22.000000000 +0100 +++ new/FanFicFare-4.10.0/calibre-plugin/inihighlighter.py 2022-02-14 16:39:42.000000000 +0100 @@ -12,10 +12,17 @@ import logging logger = logging.getLogger(__name__) +from PyQt5.Qt import (QApplication, Qt, QColor, QSyntaxHighlighter, + QTextCharFormat, QBrush, QFont) + try: - from PyQt5.Qt import (QApplication, Qt, QColor, QSyntaxHighlighter, QTextCharFormat, QBrush, QFont) -except ImportError as e: - from PyQt4.Qt import (QApplication, Qt, QColor, QSyntaxHighlighter, QTextCharFormat, QBrush, QFont) + # qt6 Calibre v6+ + QFontNormal = QFont.Weight.Normal + QFontBold = QFont.Weight.Bold +except: + # qt5 Calibre v2-5 + QFontNormal = QFont.Normal + QFontBold = QFont.Bold from fanficfare.six import string_types @@ -83,13 +90,13 @@ self.highlightingRules.append( HighlightingRule( r"^(add_to_)?"+rekeywords+r"(_filelist)?\s*[:=]", colors['knownkeywords'] ) ) # *all* sections -- change known later. - self.highlightingRules.append( HighlightingRule( r"^\[[^\]]+\].*?$", colors['errors'], QFont.Bold, blocknum=1 ) ) + self.highlightingRules.append( HighlightingRule( r"^\[[^\]]+\].*?$", colors['errors'], QFontBold, blocknum=1 ) ) if sections: # *known* sections resections = r'('+(r'|'.join(sections))+r')' resections = resections.replace('.','\.') #escape dots. - self.highlightingRules.append( HighlightingRule( r"^\["+resections+r"\]\s*$", colors['knownsections'], QFont.Bold, blocknum=2 ) ) + self.highlightingRules.append( HighlightingRule( r"^\["+resections+r"\]\s*$", colors['knownsections'], QFontBold, blocknum=2 ) ) # test story sections self.teststoryRule = HighlightingRule( r"^\[teststory:([0-9]+|defaults)\]", colors['teststories'], blocknum=3 ) @@ -135,7 +142,7 @@ class HighlightingRule(): def __init__( self, pattern, color, - weight=QFont.Normal, + weight=QFontNormal, style=Qt.SolidPattern, blocknum=0): if isinstance(pattern, string_types): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/FanFicFare-4.9.0/calibre-plugin/plugin-defaults.ini new/FanFicFare-4.10.0/calibre-plugin/plugin-defaults.ini --- old/FanFicFare-4.9.0/calibre-plugin/plugin-defaults.ini 2022-01-11 22:58:22.000000000 +0100 +++ new/FanFicFare-4.10.0/calibre-plugin/plugin-defaults.ini 2022-02-14 16:39:42.000000000 +0100 @@ -570,6 +570,30 @@ ## used. If set to -1, all cached files will be used. browser_cache_age_limit:4.0 +## As a (second) work around for certain sites blocking automated +## downloads, FFF offers the ability to request pages through nsapa's +## fanfictionnet_ff_proxy and FlareSolverr proxy servers. See +## https://github.com/JimmXinu/FanFicFare/wiki/ProxyFeatures for more +## details. + +## FlareSolverr (https://github.com/FlareSolverr/FlareSolverr) is a +## generic proxy that works with several otherwise blocked sites. +## It's recommended to only set use_flaresolverr_proxy:true for +## specific sites. +## FlareSolverr v1 doesn't work with some sites anymore (including +## ffnet), but FlareSolverr v2+ cannot download images. +## use_flaresolverr_proxy:true assumes FSv2 and automatically sets +## include_images:false +## If you want to use FSv1 with images, you can set +## use_flaresolverr_proxy:withimages + +#[www.fanfiction.net] +#use_flaresolverr_proxy:true +## option settings, these are the defaults: +#flaresolverr_proxy_address:localhost +#flaresolverr_proxy_port:8191 +#flaresolverr_proxy_protocol:http + ## Because some adapters can pull chapter URLs from human posts, the ## odds of errors in the chapter URLs can be higher for some ## sites/stories. You can set continue_on_chapter_error:true to @@ -695,10 +719,14 @@ # previous version's include_metadata_pre Can't do on tagsfromtitle # because that's applied to each part after split. tagsfromtitledetect=>^[^\]\)]+$=> +# change ][ and )( to , for [AU][Othertag] etc + tagsfromtitle=>\] *\[=>, + tagsfromtitle=>\) *\(=>, # for QuestionableQuesting NSFW subforum. - tagsfromtitle=>^\[NSFW\].*?(\[([^\]]+)\]|\(([^\)]+)\)).*?$=>NSFW\,\2\,\3 -# remove anything outside () or [] - tagsfromtitle=>^.*?(\[([^\]]+)\]|\(([^\)]+)\)).*?$=>\2\,\3 + tagsfromtitle=>^\[NSFW\].*?((?P<br>\[)|(?P<pr>\())(?P<tag>(?(br)[^\]]|(?(pr)[^\)]))+)(?(br)\]|(?(pr)\))).*?$=>NSFW\,\g<tag> +# remove anything outside () or []. Note \, at the end used to +# prevent looping back so '[Worm(AU)]' becomes 'Worm(AU)' not just 'AU' + tagsfromtitle=>^.*?((?P<br>\[)|(?P<pr>\())(?P<tag>(?(br)[^\]]|(?(pr)[^\)]))+)(?(br)\]|(?(pr)\))).*?$=>\g<tag>\, # remove () [] # tagsfromtitle=>[\(\)\[\]]=> # shield these html entities from the ';' pattern below @@ -1702,19 +1730,6 @@ website_encodings:Windows-1252,utf8 -[fanfic.hu] -## website encoding(s) In theory, each website reports the character -## encoding they use for each page. In practice, some sites report it -## incorrectly. Each adapter has a default list, usually "utf8, -## Windows-1252" or "Windows-1252, utf8", but this will let you -## explicitly set the encoding and order if you need to. The special -## value 'auto' will call chardet and use the encoding it reports if -## it has +90% confidence. 'auto' is not reliable. -website_encodings:ISO-8859-1,auto - -## Site dedicated to these categories/characters/ships -extracategories:Harry Potter - [fanfic.potterheadsanonymous.com] ## Some sites do not require a login, but do require the user to ## confirm they are adult for adult content. In commandline version, @@ -2293,6 +2308,30 @@ website_encodings: utf8:ignore, Windows-1252, iso-8859-1 +[readonlymind.com] +## Some sites do not require a login, but do require the user to +## confirm they are adult for adult content. In commandline version, +## this should go in your personal.ini, not defaults.ini. +## Login on readonlymind.com is optional and not used for adultcheck +#is_adult:true + +## Clear FanFiction from defaults, site is original fiction. +extratags:Erotica + +extra_valid_entries:eroticatags +eroticatags_label:Erotica Tags +extra_titlepage_entries: eroticatags + +## some tags are used as series identification. There is no way to find a +## sequence, but at least the stories will be grouped. Use the tag as written +## without the hash mark. Keep underscores '_' in, they will be replaced by +## spaces in the metadata. +#series_tags:Human_Domestication_Guide + +## If you want underscores replaced in the tags: +#add_to_replace_metadata: +# eroticatags=>_=>\s + [samandjack.net] ## Some sites require login (or login for some rated stories) The ## program can prompt you, or you can save it in config. In @@ -3257,12 +3296,6 @@ # numWords=~.* # size=~.* -[www.hpfanficarchive.com] -## Site dedicated to these categories/characters/ships -extracategories:Harry Potter - -website_encodings:Windows-1252,utf8 - [www.ik-eternal.net] ## Some sites require login (or login for some rated stories) The ## program can prompt you, or you can save it in config. In @@ -3811,4 +3844,3 @@ ## Clear FanFiction from defaults, site is original fiction. extratags: - diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/FanFicFare-4.9.0/fanficfare/adapters/__init__.py new/FanFicFare-4.10.0/fanficfare/adapters/__init__.py --- old/FanFicFare-4.9.0/fanficfare/adapters/__init__.py 2022-01-11 22:58:22.000000000 +0100 +++ new/FanFicFare-4.10.0/fanficfare/adapters/__init__.py 2022-02-14 16:39:42.000000000 +0100 @@ -70,7 +70,6 @@ from . import adapter_pretendercentrecom from . import adapter_darksolaceorg from . import adapter_finestoriescom -from . import adapter_hpfanficarchivecom from . import adapter_hlfictionnet from . import adapter_dracoandginnycom from . import adapter_scarvesandcoffeenet @@ -88,7 +87,6 @@ from . import adapter_voracity2eficcom from . import adapter_spikeluvercom from . import adapter_bloodshedversecom -from . import adapter_fanfichu from . import adapter_fictionmaniatv from . import adapter_themaplebookshelf from . import adapter_sheppardweircom @@ -165,6 +163,8 @@ from . import adapter_worldofxde from . import adapter_psychficcom from . import adapter_deviantartcom +from . import adapter_merengohu +from . import adapter_readonlymindcom ## This bit of complexity allows adapters to be added by just adding ## importing. It eliminates the long if/else clauses we used to need diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/FanFicFare-4.9.0/fanficfare/adapters/adapter_fanfichu.py new/FanFicFare-4.10.0/fanficfare/adapters/adapter_fanfichu.py --- old/FanFicFare-4.9.0/fanficfare/adapters/adapter_fanfichu.py 2022-01-11 22:58:22.000000000 +0100 +++ new/FanFicFare-4.10.0/fanficfare/adapters/adapter_fanfichu.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,187 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright 2014 Fanficdownloader team, 2018 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. -# - -from __future__ import absolute_import -import re -# py2 vs py3 transition -from ..six import ensure_text -from ..six.moves.urllib import parse as urlparse - -from .base_adapter import BaseSiteAdapter, makeDate -from .. import exceptions - - -_SOURCE_CODE_ENCODING = 'utf-8' - - -def getClass(): - return FanficHuAdapter - - -def _get_query_data(url): - components = urlparse.urlparse(url) - query_data = urlparse.parse_qs(components.query) - return dict((key, data[0]) for key, data in query_data.items()) - - -class FanficHuAdapter(BaseSiteAdapter): - SITE_ABBREVIATION = 'ffh' - SITE_DOMAIN = 'fanfic.hu' - SITE_LANGUAGE = 'Hungarian' - - BASE_URL = 'https://' + SITE_DOMAIN + '/merengo/' - VIEW_STORY_URL_TEMPLATE = BASE_URL + 'viewstory.php?sid=%s' - - DATE_FORMAT = '%m/%d/%Y' - - def __init__(self, config, url): - BaseSiteAdapter.__init__(self, config, url) - - query_data = urlparse.parse_qs(self.parsedUrl.query) - story_id = query_data['sid'][0] - - self.story.setMetadata('storyId', story_id) - self._setURL(self.VIEW_STORY_URL_TEMPLATE % story_id) - self.story.setMetadata('siteabbrev', self.SITE_ABBREVIATION) - self.story.setMetadata('language', self.SITE_LANGUAGE) - - @staticmethod - def getSiteDomain(): - return FanficHuAdapter.SITE_DOMAIN - - @classmethod - def getSiteExampleURLs(cls): - return cls.VIEW_STORY_URL_TEMPLATE % 1234 - - def getSiteURLPattern(self): - return re.escape(self.VIEW_STORY_URL_TEMPLATE[:-2]).replace('https','https?') + r'\d+$' - - def extractChapterUrlsAndMetadata(self): - soup = self.make_soup(self.get_request(self.url + '&i=1')) - - if ensure_text(soup.title.string).strip(u' :') == u'??rta': - raise exceptions.StoryDoesNotExist(self.url) - - chapter_options = soup.find('form', action='viewstory.php').select('option') - # Remove redundant "Fejezetek" option - chapter_options.pop(0) - - # If there is still more than one entry remove chapter overview entry - if len(chapter_options) > 1: - chapter_options.pop(0) - - for option in chapter_options: - url = urlparse.urljoin(self.url, option['value']) - self.add_chapter(option.string, url) - - author_url = urlparse.urljoin(self.BASE_URL, soup.find('a', href=lambda href: href and href.startswith('viewuser.php?uid='))['href']) - soup = self.make_soup(self.get_request(author_url)) - - story_id = self.story.getMetadata('storyId') - for table in soup('table', {'class': 'mainnav'}): - title_anchor = table.find('span', {'class': 'storytitle'}).a - href = title_anchor['href'] - if href.startswith('javascript:'): - href = href.rsplit(' ', 1)[1].strip("'") - query_data = _get_query_data(href) - - if query_data['sid'] == story_id: - break - else: - # This should never happen, the story must be found on the author's - # page. - raise exceptions.FailedToDownload(self.url) - - self.story.setMetadata('title', title_anchor.string) - - rows = table('tr') - - anchors = rows[0].div('a') - author_anchor = anchors[1] - query_data = _get_query_data(author_anchor['href']) - self.story.setMetadata('author', author_anchor.string) - self.story.setMetadata('authorId', query_data['uid']) - self.story.setMetadata('authorUrl', urlparse.urljoin(self.BASE_URL, author_anchor['href'])) - self.story.setMetadata('reviews', anchors[3].string) - - if self.getConfig('keep_summary_html'): - self.story.setMetadata('description', self.utf8FromSoup(author_url, rows[1].td)) - else: - self.story.setMetadata('description', ''.join(rows[1].td(text=True))) - - for row in rows[3:]: - index = 0 - cells = row('td') - - while index < len(cells): - cell = cells[index] - key = ensure_text(cell.b.string).strip(u':') - try: - value = ensure_text(cells[index+1].string) - except: - value = None - - if key == u'Kateg??ria': - for anchor in cells[index+1]('a'): - self.story.addToList('category', anchor.string) - - elif key == u'Szerepl??k': - if cells[index+1].string: - for name in cells[index+1].string.split(', '): - self.story.addToList('character', name) - - elif key == u'Korhat??r': - if value != 'nem korhat??ros': - self.story.setMetadata('rating', value) - - elif key == u'Figyelmeztet??sek': - for b_tag in cells[index+1]('b'): - self.story.addToList('warnings', b_tag.string) - - elif key == u'Jellemz??k': - for genre in cells[index+1].string.split(', '): - self.story.addToList('genre', genre) - - elif key == u'Fejezetek': - self.story.setMetadata('numChapters', int(value)) - - elif key == u'Megjelen??s': - self.story.setMetadata('datePublished', makeDate(value, self.DATE_FORMAT)) - - elif key == u'Friss??t??s': - self.story.setMetadata('dateUpdated', makeDate(value, self.DATE_FORMAT)) - - elif key == u'Szavak': - self.story.setMetadata('numWords', value) - - elif key == u'Befejezett': - self.story.setMetadata('status', 'Completed' if value == 'Nem' else 'In-Progress') - - index += 2 - - if self.story.getMetadata('rating') == '18': - if not (self.is_adult or self.getConfig('is_adult')): - raise exceptions.AdultCheckRequired(self.url) - - def getChapterText(self, url): - soup = self.make_soup(self.get_request(url)) - story_cell = soup.find('form', action='viewstory.php').parent.parent - - for div in story_cell('div'): - div.extract() - - return self.utf8FromSoup(url, story_cell) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/FanFicFare-4.9.0/fanficfare/adapters/adapter_fanfictionnet.py new/FanFicFare-4.10.0/fanficfare/adapters/adapter_fanfictionnet.py --- old/FanFicFare-4.9.0/fanficfare/adapters/adapter_fanfictionnet.py 2022-01-11 22:58:22.000000000 +0100 +++ new/FanFicFare-4.10.0/fanficfare/adapters/adapter_fanfictionnet.py 2022-02-14 16:39:42.000000000 +0100 @@ -306,7 +306,7 @@ logger.debug("cover_url:%s"%cover_url) authimg_url = "" - if cover_url and self.getConfig('skip_author_cover'): + if cover_url and self.getConfig('skip_author_cover') and self.getConfig('include_images'): try: authsoup = self.make_soup(self.get_request(self.story.getMetadata('authorUrl'))) try: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/FanFicFare-4.9.0/fanficfare/adapters/adapter_fanfiktionde.py new/FanFicFare-4.10.0/fanficfare/adapters/adapter_fanfiktionde.py --- old/FanFicFare-4.9.0/fanficfare/adapters/adapter_fanfiktionde.py 2022-01-11 22:58:22.000000000 +0100 +++ new/FanFicFare-4.10.0/fanficfare/adapters/adapter_fanfiktionde.py 2022-02-14 16:39:42.000000000 +0100 @@ -119,7 +119,7 @@ raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Auserhalb der Zeit von 23:00 Uhr bis 04:00 Uhr ist diese Geschichte nur nach einer erfolgreichen Altersverifikation zuganglich.") soup = self.make_soup(data) - # print data + # logger.debug(data) ## Title @@ -172,10 +172,12 @@ else: self.story.setMetadata('status', 'In-Progress') - ## Get description from own URL: - ## /?a=v&storyid=46ccbef30000616306614050&s=1 - descsoup = self.make_soup(self.get_request("https://"+self.getSiteDomain()+"/?a=v&storyid="+self.story.getMetadata('storyId')+"&s=1")) - self.setDescription(url,stripHTML(descsoup)) + ## Get description + descdiv = soup.select_one('div#story-summary-inline div') + if descdiv: + if 'center' in descdiv['class']: + del descdiv['class'] + self.setDescription(url,descdiv) # #find metadata on the author's page # asoup = self.make_soup(self.get_request("https://"+self.getSiteDomain()+"?a=q&a1=v&t=nickdetailsstories&lbi=stories&ar=0&nick="+self.story.getMetadata('authorId'))) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/FanFicFare-4.9.0/fanficfare/adapters/adapter_hpfanficarchivecom.py new/FanFicFare-4.10.0/fanficfare/adapters/adapter_hpfanficarchivecom.py --- old/FanFicFare-4.9.0/fanficfare/adapters/adapter_hpfanficarchivecom.py 2022-01-11 22:58:22.000000000 +0100 +++ new/FanFicFare-4.10.0/fanficfare/adapters/adapter_hpfanficarchivecom.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,215 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright 2012 Fanficdownloader team, 2018 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. -# - -# Software: eFiction -from __future__ import absolute_import -import logging -logger = logging.getLogger(__name__) -import re -from bs4.element import Comment -from ..htmlcleanup import stripHTML -from .. import exceptions as exceptions - -# py2 vs py3 transition -from ..six import text_type as unicode - -from .base_adapter import BaseSiteAdapter, makeDate - -def getClass(): - return HPFanficArchiveComAdapter - -# Class name has to be unique. Our convention is camel case the -# sitename with Adapter at the end. www is skipped. -class HPFanficArchiveComAdapter(BaseSiteAdapter): - - def __init__(self, config, url): - BaseSiteAdapter.__init__(self, config, url) - - 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( self.getProtocol() + self.getSiteDomain() + '/stories/viewstory.php?sid='+self.story.getMetadata('storyId')) - - # Each adapter needs to have a unique site abbreviation. - self.story.setMetadata('siteabbrev','hpffa') - - # 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 'hpfanficarchive.com' - - @classmethod - def getProtocol(cls): - # has changed from http to https to http again. - return "http://" - - @classmethod - def getSiteExampleURLs(cls): - return cls.getProtocol()+cls.getSiteDomain()+"/stories/viewstory.php?sid=1234" - - def getSiteURLPattern(self): - return r"https?:"+re.escape("//"+self.getSiteDomain()+"/stories/viewstory.php?sid=")+r"\d+$" - - ## Getting the chapter list and the meta data, plus 'is adult' checking. - def extractChapterUrlsAndMetadata(self): - - # index=1 makes sure we see the story chapter index. Some - # sites skip that for one-chapter stories. - url = self.url - logger.debug("URL: "+url) - - data = self.get_request(url) - - if "Access denied. This story has not been validated by the adminstrators of this site." in data: - raise exceptions.AccessDenied(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") - elif "That story either does not exist on this archive or has not been validated by the adminstrators of this site." in data: - raise exceptions.AccessDenied(self.getSiteDomain() +" says: That story either does not exist on this archive or has not been validated by the adminstrators of this site.") - - soup = self.make_soup(data) - # print data - - - ## Title - a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) - self.story.setMetadata('title',stripHTML(a)) - - # Find authorid and URL from... author url. - a = soup.find('div', id="mainpage").find('a', href=re.compile(r"viewuser.php\?uid=\d+")) - self.story.setMetadata('authorId',a['href'].split('=')[1]) - self.story.setMetadata('authorUrl',self.getProtocol()+self.host+'/stories/'+a['href']) - self.story.setMetadata('author',a.string) - - # Find the chapters: - for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+r"&chapter=\d+$")): - # just in case there's tags, like <i> in chapter titles. - self.add_chapter(chapter,self.getProtocol()+self.host+'/stories/'+chapter['href']) - - - # 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 = soup.findAll('span',{'class':'label'}) - for labelspan in labels: - val = labelspan.nextSibling - value = unicode('') - while val and not 'label' in defaultGetattr(val,'class'): - # print("val:%s"%val) - if not isinstance(val,Comment): - value += unicode(val) - val = val.nextSibling - label = labelspan.string - # print("label:%s\nvalue:%s"%(label,value)) - - if 'Summary' in label: - self.setDescription(url,value) - - if 'Rated' in label: - self.story.setMetadata('rating', stripHTML(value)) - - if 'Word count' in label: - self.story.setMetadata('numWords', stripHTML(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')) # XXX - for genre in genres: - self.story.addToList('genre',genre.string) - - if 'Pairing' in label: - ships = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=4')) - for ship in ships: - self.story.addToList('ships',ship.string) - - if 'Warnings' in label: - warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX - for warning in warnings: - self.story.addToList('warnings',warning.string) - - if 'Completed' in label: - if 'Yes' in stripHTML(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: - self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) - - try: - # Find Series name from series URL. - a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+")) - series_name = a.string - series_url = self.getProtocol()+self.host+'/stories/'+a['href'] - - seriessoup = self.make_soup(self.get_request(series_url)) - # can't use ^viewstory...$ in case of higher rated stories with javascript href. - storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+')) - i=1 - for a in storyas: - # skip 'report this' and 'TOC' links - if 'contact.php' not in a['href'] and 'index' not in a['href']: - if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): - self.setSeries(series_name, i) - self.story.setMetadata('seriesUrl',series_url) - break - i+=1 - - except: - # I find it hard to care if the series parsing fails - pass - - # grab the text for an individual chapter. - def getChapterText(self, url): - - logger.debug('Getting chapter text from: %s' % url) - - soup = self.make_soup(self.get_request(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) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/FanFicFare-4.9.0/fanficfare/adapters/adapter_merengohu.py new/FanFicFare-4.10.0/fanficfare/adapters/adapter_merengohu.py --- old/FanFicFare-4.9.0/fanficfare/adapters/adapter_merengohu.py 1970-01-01 01:00:00.000000000 +0100 +++ new/FanFicFare-4.10.0/fanficfare/adapters/adapter_merengohu.py 2022-02-14 16:39:42.000000000 +0100 @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +# Copyright 2022 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. +# + +# Software: eFiction +from __future__ import absolute_import +from .base_efiction_adapter import BaseEfictionAdapter + +class MerengoHuAdapter(BaseEfictionAdapter): + + @classmethod + def getProtocol(self): + return "https" + + @staticmethod + def getSiteDomain(): + return 'merengo.hu' + + @classmethod + def getSiteAbbrev(self): + return 'merengo' + + @classmethod + def getDateFormat(self): + return "%Y.%m.%d" + + def extractChapterUrlsAndMetadata(self): + ## merengo.hu has a custom 18 consent click through + self.get_request(self.getUrlForPhp('tizennyolc.php')+'?consent=true') + + ## Call super of extractChapterUrlsAndMetadata(). + ## base_efiction leaves the soup in self.html. + return super(MerengoHuAdapter, self).extractChapterUrlsAndMetadata() + +def getClass(): + return MerengoHuAdapter + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/FanFicFare-4.9.0/fanficfare/adapters/adapter_readonlymindcom.py new/FanFicFare-4.10.0/fanficfare/adapters/adapter_readonlymindcom.py --- old/FanFicFare-4.9.0/fanficfare/adapters/adapter_readonlymindcom.py 1970-01-01 01:00:00.000000000 +0100 +++ new/FanFicFare-4.10.0/fanficfare/adapters/adapter_readonlymindcom.py 2022-02-14 16:39:42.000000000 +0100 @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 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. +# +#################################################################################################### +### Based on MCStoriesComSiteAdapter and reworked by Nothorse +### +#################################################################################################### +from __future__ import absolute_import +from __future__ import unicode_literals +import logging +logger = logging.getLogger(__name__) +import re + +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +# py2 vs py3 transition +from ..six import text_type as unicode + +from .base_adapter import BaseSiteAdapter, makeDate + +#################################################################################################### +def getClass(): + return ReadOnlyMindComAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class ReadOnlyMindComAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','rom') + + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # Normalize story URL to the chapter index page (.../index.html) + m = re.match(self.getSiteURLPattern(),url) + if m: + # normalized story URL. + self._setURL("https://"+self.getSiteDomain()+"/@"+m.group('aut')+"/"+m.group('id')+"/") + else: + raise exceptions.InvalidStoryURL(url, + self.getSiteDomain(), + self.getSiteExampleURLs()) + + # get storyId from url + self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2]) + + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%Y-%m-%d" + + + ################################################################################################ + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + return 'readonlymind.com' + + ################################################################################################ + @classmethod + def getAcceptDomains(cls): + + return ['readonlymind.com', 'www.readonlymind.com'] + + ################################################################################################ + @classmethod + def getSiteExampleURLs(self): + return "https://readonlymind.com/@AnAuthor/A_Story_Name/" + + ################################################################################################ + def getSiteURLPattern(self): + return r'https?://readonlymind\.com/@(?P<aut>[a-zA-Z0-9_]+)/(?P<id>[a-zA-Z0-9_]+)' + + ################################################################################################ + def extractChapterUrlsAndMetadata(self): + """ + Chapters are located at /@author/StoryName/#/ + + The story metadata page is at /@author/StoryName/, including a list + of chapters. + """ + if not (self.is_adult or self.getConfig("is_adult")): + raise exceptions.AdultCheckRequired(self.url) + + data1 = self.get_request(self.url) + logger.debug(self.url) + + soup1 = self.make_soup(data1) + #strip comments from soup + baseUrl = "https://" + self.getSiteDomain() + if 'Page Not Found.' in data1: + raise exceptions.StoryDoesNotExist(self.url) + + # Extract metadata + header = soup1.find('header') + title = header.find('h1') + self.story.setMetadata('title', title.text) + + # Author + author = soup1.find('meta', attrs={"name":"author"}) + authorurl = soup1.find('link', rel="author") + self.story.setMetadata('author', author.attrs["content"]) + self.story.setMetadata('authorUrl', baseUrl + authorurl["href"]) + self.story.setMetadata('authorId', author.attrs["content"]) + + # Description + synopsis = soup1.find('meta', attrs={"name":"description"}) + self.story.setMetadata('description', synopsis.attrs["content"]) + + # Tags + # As these are the only tags should they go in categories? + # Also check for series tags in config + # Unfortunately there's no way to get a meaningful volume number + series_tags = self.getConfig('series_tags').split(',') + + for a in soup1.find_all('a', class_="tag-link"): + strippedTag = a.text.strip('#') + if strippedTag in series_tags: + self.setSeries(strippedTag.replace('_', ' '), 0) + seriesUrl = baseUrl + a.attrs['href'] + self.story.setMetadata('seriesUrl', seriesUrl); + else: + self.story.addToList('eroticatags', strippedTag) + + + # Publish and update dates + publishdate = soup1.find('meta', attrs={"name":"created"}) + pDate = makeDate(publishdate.attrs['content'], self.dateformat) + if publishdate is not None: self.story.setMetadata('datePublished', pDate) + + # Get chapter URLs + chapterTable = soup1.find('section', id='chapter-list') + # + if chapterTable is not None: + # Multi-chapter story + chapterRows = chapterTable.find_all('section', class_='story-card-large') + for row in chapterRows: + titleDiv = row.find('div', class_='story-card-title') + chapterCell = titleDiv.a + if chapterCell is not None: + chapterTitle = chapterCell.text + chapterUrl = baseUrl + chapterCell['href'] + self.add_chapter(chapterTitle, chapterUrl) + dateUpdated = row.find('div', class_='story-card-publication-date') + if dateUpdated is not None: + self.story.setMetadata('dateUpdated', makeDate(dateUpdated.text, self.dateformat)) + + else: + # Single chapter + chapterTitle = self.story.getMetadata('title') + chapterUrl = self.url + self.add_chapter(chapterTitle, chapterUrl) + + + logger.debug("Story: <%s>", self.story) + + return + + def getChapterText(self, url): + """ + + All content is in section#chapter-content + """ + logger.debug('Getting chapter text from <%s>' % url) + data1 = self.get_request(url) + soup1 = self.make_soup(data1) + + #strip comments from soup + # [comment.extract() for comment in soup1.find_all(text=lambda text:isinstance(text, Comment))] + + # get story text + story1 = soup1.find('section', id='chapter-content') + + + storytext = self.utf8FromSoup(url, story1) + + return storytext diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/FanFicFare-4.9.0/fanficfare/adapters/adapter_royalroadcom.py new/FanFicFare-4.10.0/fanficfare/adapters/adapter_royalroadcom.py --- old/FanFicFare-4.9.0/fanficfare/adapters/adapter_royalroadcom.py 2022-01-11 22:58:22.000000000 +0100 +++ new/FanFicFare-4.10.0/fanficfare/adapters/adapter_royalroadcom.py 2022-02-14 16:39:42.000000000 +0100 @@ -174,6 +174,10 @@ self.story.setMetadata('status', 'In-Progress') elif 'HIATUS' == label: self.story.setMetadata('status', 'Hiatus') + elif 'STUB' == label: + self.story.setMetadata('status', 'Stub') + elif 'DROPPED' == label: + self.story.setMetadata('status', 'Dropped') elif 'Fan Fiction' == label: self.story.addToList('category', 'FanFiction') elif 'Original' == label: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/FanFicFare-4.9.0/fanficfare/adapters/base_efiction_adapter.py new/FanFicFare-4.10.0/fanficfare/adapters/base_efiction_adapter.py --- old/FanFicFare-4.9.0/fanficfare/adapters/base_efiction_adapter.py 2022-01-11 22:58:22.000000000 +0100 +++ new/FanFicFare-4.10.0/fanficfare/adapters/base_efiction_adapter.py 2022-02-14 16:39:42.000000000 +0100 @@ -320,7 +320,7 @@ elif key == 'Word count': self.story.setMetadata('numWords', value) elif key == 'Completed': - if 'Yes' in value or 'Completed' in value or 'Ja' in value: + if 'Yes' in value or 'Completed' in value or 'Ja' in value or 'Igen' in value: self.story.setMetadata('status', 'Completed') else: self.story.setMetadata('status', 'In-Progress') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/FanFicFare-4.9.0/fanficfare/browsercache/firefoxcache2.py new/FanFicFare-4.10.0/fanficfare/browsercache/firefoxcache2.py --- old/FanFicFare-4.9.0/fanficfare/browsercache/firefoxcache2.py 2022-01-11 22:58:22.000000000 +0100 +++ new/FanFicFare-4.10.0/fanficfare/browsercache/firefoxcache2.py 2022-02-14 16:39:42.000000000 +0100 @@ -97,7 +97,7 @@ self.add_key_mapping(cache_url,path,created) self.count+=1 except Exception as e: - logger.warn("Cache file %s failed to load, skipping."%path) + logger.warning("Cache file %s failed to load, skipping."%path) logger.debug(traceback.format_exc()) # logger.debug(" file time: %s"%datetime.datetime.fromtimestamp(stats.st_mtime)) # logger.debug("created time: %s"%datetime.datetime.fromtimestamp(created)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/FanFicFare-4.9.0/fanficfare/browsercache/simplecache.py new/FanFicFare-4.10.0/fanficfare/browsercache/simplecache.py --- old/FanFicFare-4.9.0/fanficfare/browsercache/simplecache.py 2022-01-11 22:58:22.000000000 +0100 +++ new/FanFicFare-4.10.0/fanficfare/browsercache/simplecache.py 2022-02-14 16:39:42.000000000 +0100 @@ -90,7 +90,7 @@ self.add_key_mapping(cache_url,path,created) self.count+=1 except Exception as e: - logger.warn("Cache file %s failed to load, skipping."%path) + logger.warning("Cache file %s failed to load, skipping."%path) logger.debug(traceback.format_exc()) # key == filename for simple cache diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/FanFicFare-4.9.0/fanficfare/cli.py new/FanFicFare-4.10.0/fanficfare/cli.py --- old/FanFicFare-4.9.0/fanficfare/cli.py 2022-01-11 22:58:22.000000000 +0100 +++ new/FanFicFare-4.10.0/fanficfare/cli.py 2022-02-14 16:39:42.000000000 +0100 @@ -28,7 +28,7 @@ import os, sys, platform -version="4.9.0" +version="4.10.0" os.environ['CURRENT_VERSION_ID']=version global_cache = 'global_cache' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/FanFicFare-4.9.0/fanficfare/configurable.py new/FanFicFare-4.10.0/fanficfare/configurable.py --- old/FanFicFare-4.9.0/fanficfare/configurable.py 2022-01-11 22:58:22.000000000 +0100 +++ new/FanFicFare-4.10.0/fanficfare/configurable.py 2022-02-14 16:39:42.000000000 +0100 @@ -198,7 +198,7 @@ 'use_cloudscraper':(None,None,boollist), 'use_basic_cache':(None,None,boollist), 'use_nsapa_proxy':(None,None,boollist), - 'use_flaresolverr_proxy':(None,None,boollist), + 'use_flaresolverr_proxy':(None,None,boollist+['withimages']), ## currently, browser_cache_path is assumed to be ## shared and only ffnet uses it so far @@ -1004,6 +1004,10 @@ if self.getConfig('use_flaresolverr_proxy',False): logger.debug("use_flaresolverr_proxy:%s"%self.getConfig('use_flaresolverr_proxy')) fetchcls = flaresolverr_proxy.FlareSolverr_ProxyFetcher + if self.getConfig('use_flaresolverr_proxy') != 'withimages': + logger.warning("FlareSolverr v2+ doesn't work with images: include_images automatically set false") + logger.warning("Set use_flaresolverr_proxy:withimages if your are using FlareSolver v1 and want images") + self.set('overrides', 'include_images', 'false') elif self.getConfig('use_nsapa_proxy',False): logger.debug("use_nsapa_proxy:%s"%self.getConfig('use_nsapa_proxy')) fetchcls = nsapa_proxy.NSAPA_ProxyFetcher @@ -1037,7 +1041,7 @@ age_limit=self.getConfig("browser_cache_age_limit")) fetcher.BrowserCacheDecorator(self.browser_cache).decorate_fetcher(self.fetcher) except Exception as e: - logger.warn("Failed to setup BrowserCache(%s)"%e) + logger.warning("Failed to setup BrowserCache(%s)"%e) raise ## cache decorator terminates the chain when found. logger.debug("use_basic_cache:%s"%self.getConfig('use_basic_cache')) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/FanFicFare-4.9.0/fanficfare/defaults.ini new/FanFicFare-4.10.0/fanficfare/defaults.ini --- old/FanFicFare-4.9.0/fanficfare/defaults.ini 2022-01-11 22:58:22.000000000 +0100 +++ new/FanFicFare-4.10.0/fanficfare/defaults.ini 2022-02-14 16:39:42.000000000 +0100 @@ -563,6 +563,30 @@ ## used. If set to -1, all cached files will be used. browser_cache_age_limit:4.0 +## As a (second) work around for certain sites blocking automated +## downloads, FFF offers the ability to request pages through nsapa's +## fanfictionnet_ff_proxy and FlareSolverr proxy servers. See +## https://github.com/JimmXinu/FanFicFare/wiki/ProxyFeatures for more +## details. + +## FlareSolverr (https://github.com/FlareSolverr/FlareSolverr) is a +## generic proxy that works with several otherwise blocked sites. +## It's recommended to only set use_flaresolverr_proxy:true for +## specific sites. +## FlareSolverr v1 doesn't work with some sites anymore (including +## ffnet), but FlareSolverr v2+ cannot download images. +## use_flaresolverr_proxy:true assumes FSv2 and automatically sets +## include_images:false +## If you want to use FSv1 with images, you can set +## use_flaresolverr_proxy:withimages + +#[www.fanfiction.net] +#use_flaresolverr_proxy:true +## option settings, these are the defaults: +#flaresolverr_proxy_address:localhost +#flaresolverr_proxy_port:8191 +#flaresolverr_proxy_protocol:http + ## Because some adapters can pull chapter URLs from human posts, the ## odds of errors in the chapter URLs can be higher for some ## sites/stories. You can set continue_on_chapter_error:true to @@ -713,10 +737,14 @@ # previous version's include_metadata_pre Can't do on tagsfromtitle # because that's applied to each part after split. tagsfromtitledetect=>^[^\]\)]+$=> +# change ][ and )( to , for [AU][Othertag] etc + tagsfromtitle=>\] *\[=>, + tagsfromtitle=>\) *\(=>, # for QuestionableQuesting NSFW subforum. - tagsfromtitle=>^\[NSFW\].*?(\[([^\]]+)\]|\(([^\)]+)\)).*?$=>NSFW\,\2\,\3 -# remove anything outside () or [] - tagsfromtitle=>^.*?(\[([^\]]+)\]|\(([^\)]+)\)).*?$=>\2\,\3 + tagsfromtitle=>^\[NSFW\].*?((?P<br>\[)|(?P<pr>\())(?P<tag>(?(br)[^\]]|(?(pr)[^\)]))+)(?(br)\]|(?(pr)\))).*?$=>NSFW\,\g<tag> +# remove anything outside () or []. Note \, at the end used to +# prevent looping back so '[Worm(AU)]' becomes 'Worm(AU)' not just 'AU' + tagsfromtitle=>^.*?((?P<br>\[)|(?P<pr>\())(?P<tag>(?(br)[^\]]|(?(pr)[^\)]))+)(?(br)\]|(?(pr)\))).*?$=>\g<tag>\, # remove () [] # tagsfromtitle=>[\(\)\[\]]=> # shield these html entities from the ';' pattern below @@ -1724,19 +1752,6 @@ website_encodings:Windows-1252,utf8 -[fanfic.hu] -## website encoding(s) In theory, each website reports the character -## encoding they use for each page. In practice, some sites report it -## incorrectly. Each adapter has a default list, usually "utf8, -## Windows-1252" or "Windows-1252, utf8", but this will let you -## explicitly set the encoding and order if you need to. The special -## value 'auto' will call chardet and use the encoding it reports if -## it has +90% confidence. 'auto' is not reliable. -website_encodings:ISO-8859-1,auto - -## Site dedicated to these categories/characters/ships -extracategories:Harry Potter - [fanfic.potterheadsanonymous.com] ## Some sites do not require a login, but do require the user to ## confirm they are adult for adult content. In commandline version, @@ -2315,6 +2330,30 @@ website_encodings: utf8:ignore, Windows-1252, iso-8859-1 +[readonlymind.com] +## Some sites do not require a login, but do require the user to +## confirm they are adult for adult content. In commandline version, +## this should go in your personal.ini, not defaults.ini. +## Login on readonlymind.com is optional and not used for adultcheck +#is_adult:true + +## Clear FanFiction from defaults, site is original fiction. +extratags:Erotica + +extra_valid_entries:eroticatags +eroticatags_label:Erotica Tags +extra_titlepage_entries: eroticatags + +## some tags are used as series identification. There is no way to find a +## sequence, but at least the stories will be grouped. Use the tag as written +## without the hash mark. Keep underscores '_' in, they will be replaced by +## spaces in the metadata. +#series_tags:Human_Domestication_Guide + +## If you want underscores replaced in the tags: +#add_to_replace_metadata: +# eroticatags=>_=>\s + [samandjack.net] ## Some sites require login (or login for some rated stories) The ## program can prompt you, or you can save it in config. In @@ -3261,12 +3300,6 @@ # numWords=~.* # size=~.* -[www.hpfanficarchive.com] -## Site dedicated to these categories/characters/ships -extracategories:Harry Potter - -website_encodings:Windows-1252,utf8 - [www.ik-eternal.net] ## Some sites require login (or login for some rated stories) The ## program can prompt you, or you can save it in config. In @@ -3815,4 +3848,3 @@ ## Clear FanFiction from defaults, site is original fiction. extratags: - diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/FanFicFare-4.9.0/fanficfare/flaresolverr_proxy.py new/FanFicFare-4.10.0/fanficfare/flaresolverr_proxy.py --- old/FanFicFare-4.9.0/fanficfare/flaresolverr_proxy.py 2022-01-11 22:58:22.000000000 +0100 +++ new/FanFicFare-4.10.0/fanficfare/flaresolverr_proxy.py 2022-02-14 16:39:42.000000000 +0100 @@ -69,12 +69,15 @@ 'url':url, #'userAgent': 'Mozilla/5.0', 'maxTimeout': 30000, - 'download': True, # download:True causes response to be base64 encoded # which makes images work. 'cookies':cookiejar_to_jsonable(self.get_cookiejar()), 'postData':encode_params(parameters), } + if self.getConfig('use_flaresolverr_proxy') == 'withimages': + # download param removed in FlareSolverr v2+, but optional + # for FFF users still on FlareSolver v1. + fs_data['download'] = True if self.fs_session: fs_data['session']=self.fs_session @@ -111,22 +114,33 @@ url = resp.json['solution']['url'] for c in cookiejson_to_jarable(resp.json['solution']['cookies']): self.get_cookiejar().set_cookie(c) - if resp.json.get('version','').startswith('v2.'): - # FlareSolverr v2 detected, don't need base64 decode, - # and image downloads won't work. + data = None + ## FSv2 check removed in favor of + ## use_flaresolverr_proxy:withimages in the hope one day + ## FS will have download option again. + if self.getConfig('use_flaresolverr_proxy') == 'withimages': + try: + # v1 flaresolverr has 'download' option. + data = base64.b64decode(resp.json['solution']['response']) + except Exception as e: + logger.warning("Base64 decode of FlareSolverr response failed. FSv2 doesn't work with use_flaresolverr_proxy:withimages.") + ## Allows for user misconfiguration, IE, + ## use_flaresolverr_proxy:withimages with FSv2. Warning + ## instead of error out--until they hit an image and crash + ## FSv2.2 at least. But hopefully that will be fixed. + if data is None: + # Without download (or with FlareSolverr v2), don't + # need base64 decode, and image downloads won't work. if 'image' in resp.json['solution']['headers']['content-type']: raise exceptions.HTTPErrorFFF( url, 428, # 404 & 410 trip StoryDoesNotExist # 428 ('Precondition Required') gets the # error_msg through to the user. - "FlareSolverr v2 doesn't support image download.",# error_msg + "FlareSolverr v2 doesn't support image download (or use_flaresolverr_proxy!=withimages)",# error_msg None # data ) data = resp.json['solution']['response'] - else: - # v1 flaresolverr has 'download' option. - data = base64.b64decode(resp.json['solution']['response']) else: logger.debug("flaresolverr error resp:") logger.debug(json.dumps(resp.json, sort_keys=True, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/FanFicFare-4.9.0/fanficfare/geturls.py new/FanFicFare-4.10.0/fanficfare/geturls.py --- old/FanFicFare-4.9.0/fanficfare/geturls.py 2022-01-11 22:58:22.000000000 +0100 +++ new/FanFicFare-4.10.0/fanficfare/geturls.py 2022-02-14 16:39:42.000000000 +0100 @@ -179,7 +179,7 @@ href = adapter.get_request_redirected(href)[1] href = href.replace('&index=1','') except Exception as e: - logger.warn("Skipping royalroad email URL %s, got HTTP error %s"%(href,e)) + logger.warning("Skipping royalroad email URL %s, got HTTP error %s"%(href,e)) return href def get_urls_from_imap(srv,user,passwd,folder,markread=True): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/FanFicFare-4.9.0/setup.py new/FanFicFare-4.10.0/setup.py --- old/FanFicFare-4.9.0/setup.py 2022-01-11 22:58:22.000000000 +0100 +++ new/FanFicFare-4.10.0/setup.py 2022-02-14 16:39:42.000000000 +0100 @@ -26,7 +26,7 @@ name=package_name, # Versions should comply with PEP440. - version="4.9.0", + version="4.10.0", description='A tool for downloading fanfiction to eBook formats', long_description=long_description,