From e9190b9a12732769d39a58f67102da6eaf175338 Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Wed, 10 Aug 2016 14:09:02 -0500 Subject: [PATCH] New sites from GComyn, including adult-fanfiction.org. --- calibre-plugin/plugin-defaults.ini | 122 ++++-- fanficfare/adapters/__init__.py | 5 + .../adapters/adapter_adultfanfictionorg.py | 372 ++++++++++++++++++ .../adapters/adapter_deepinmysoulnet.py | 300 ++++++++++++++ .../adapters/adapter_fanfictionlucifaelcom.py | 36 ++ .../adapters/adapter_haremlucifaelcom.py | 36 ++ .../adapter_kiarepositorymujajinet.py | 300 ++++++++++++++ fanficfare/defaults.ini | 122 ++++-- 8 files changed, 1227 insertions(+), 66 deletions(-) create mode 100644 fanficfare/adapters/adapter_adultfanfictionorg.py create mode 100644 fanficfare/adapters/adapter_deepinmysoulnet.py create mode 100644 fanficfare/adapters/adapter_fanfictionlucifaelcom.py create mode 100644 fanficfare/adapters/adapter_haremlucifaelcom.py create mode 100644 fanficfare/adapters/adapter_kiarepositorymujajinet.py diff --git a/calibre-plugin/plugin-defaults.ini b/calibre-plugin/plugin-defaults.ini index f99055e3..07b9d526 100644 --- a/calibre-plugin/plugin-defaults.ini +++ b/calibre-plugin/plugin-defaults.ini @@ -25,7 +25,7 @@ ## titlepage_entries: category,genre, status,dateUpdated,rating ## [epub] ## # overrides defaults & site section -## titlepage_entries: category,genre, status,datePublished,dateUpdated,dateCreated +## titlepage_entries: category,genre,status,datePublished,dateUpdated,dateCreated ## [www.whofic.com:epub] ## # overrides defaults, site section & format section ## titlepage_entries: category,genre, status,datePublished @@ -748,6 +748,19 @@ extratags: FanFiction,Testing,Text [test1.com:html] extratags: FanFiction,Testing,HTML +[adult-fanfiction.org] +extra_valid_entries:eroticatags,disclaimer +eroticatags_label:Erotica Tags +disclaimer_label:Disclaimer +extra_titlepage_entries:eroticatags,disclaimer + +## Some sites require login (or login for some rated stories) The +## program can prompt you, or you can save it in config. In +## commandline version, this should go in your personal.ini, not +## defaults.ini. +#username:YourName +#password:yourpassword + [archive.skyehawke.com] [archiveofourown.org] @@ -1145,14 +1158,6 @@ romance_label: Romance ## this should go in your personal.ini, not defaults.ini. #is_adult:true -[ficwad.com] -## Some sites require login (or login for some rated stories) The -## program can prompt you, or you can save it in config. In -## commandline version, this should go in your personal.ini, not -## defaults.ini. -#username:YourName -#password:yourpassword - [fictionmania.tv] ## website encoding(s) In theory, each website reports the character ## encoding they use for each page. In practice, some sites report it @@ -1209,6 +1214,14 @@ views_label:Views likes_label:Likes dislikes_label:Dislikes +[ficwad.com] +## Some sites require login (or login for some rated stories) The +## program can prompt you, or you can save it in config. In +## commandline version, this should go in your personal.ini, not +## defaults.ini. +#username:YourName +#password:yourpassword + [finestories.com] ## Some sites require login (or login for some rated stories) The ## program can prompt you, or you can save it in config. In @@ -1250,12 +1263,35 @@ universe_as_series: true ## cover image. This lets you exclude them. cover_exclusion_regexp:/css/bir.png +[forum.questionablequesting.com] +## see [base_xenforoforum] + +## Some sites require login (or login for some rated stories) The +## program can prompt you, or you can save it in config. In +## commandline version, this should go in your personal.ini, not +## defaults.ini. +#username:YourName +#password:yourpassword + [forums.spacebattles.com] ## see [base_xenforoforum] [forums.sufficientvelocity.com] ## see [base_xenforoforum] +[harem.lucifael.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. +#is_adult:true + +## Some sites require login (or login for some rated stories) The +## program can prompt you, or you can save it in config. In +## commandline version, this should go in your personal.ini, not +## defaults.ini. +#username:YourName +#password:yourpassword + [hlfiction.net] ## Site dedicated to these categories/characters/ships extracategories:Highlander @@ -1368,6 +1404,19 @@ extra_titlepage_entries: eroticatags ## Site dedicated to these categories/characters/ships extracategories:Merlin +[mujaji.net] +## Some sites require login (or login for some rated stories) The +## program can prompt you, or you can save it in config. In +## commandline version, this should go in your personal.ini, not +## defaults.ini. +#username:YourName +#password:yourpassword + +## Some sites also require the user to confirm they are adult for +## adult content. In commandline version, this should go in your +## personal.ini, not defaults.ini. +#is_adult:true + [national-library.net] ## Site dedicated to these categories/characters/ships extracategories:West Wing @@ -1469,15 +1518,16 @@ extracategories:My Little Pony: Friendship is Magic ## Site dedicated to these categories/characters/ships extracategories:The Pretender -[forum.questionablequesting.com] -## see [base_xenforoforum] +[quotev.com] +extra_valid_entries:pages,readers,reads,favorites,searchtags,comments +pages_label:Pages +readers_label:Readers +reads_label:Reads +favorites_label:Favorites +searchtags_label:Search Tags +comments_label:Comments -## Some sites require login (or login for some rated stories) The -## program can prompt you, or you can save it in config. In -## commandline version, this should go in your personal.ini, not -## defaults.ini. -#username:YourName -#password:yourpassword +include_in_category:category,searchtags [samandjack.net] ## Some sites require login (or login for some rated stories) The @@ -1711,6 +1761,12 @@ extraships:Draco Malfoy/Ginny Weasley ## Site dedicated to these categories/characters/ships extracategories:Stargate: SG-1 +[www.deepinmysoul.net] +## 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. +#is_adult:true + [www.destinysgateway.com] ## Some sites do not require a login, but do require the user to ## confirm they are adult for adult content. In commandline version, @@ -2143,22 +2199,6 @@ extracategories:Lord of the Rings #username:YourName #password:yourpassword -[www.twcslibrary.net] -## Some sites require login (or login for some rated stories) The -## program can prompt you, or you can save it in config. In -## commandline version, this should go in your personal.ini, not -## defaults.ini. -#username:YourName -#password:yourpassword - -## Some sites also require the user to confirm they are adult for -## adult content. In commandline version, this should go in your -## personal.ini, not defaults.ini. -#is_adult:true - -## twcslibrary.net (ab)uses series as personal reading lists. -collect_series: false - [www.tthfanfic.org] user_agent: slow_down_sleep_time:2 @@ -2196,6 +2236,22 @@ pairingcat_to_characters_ships:true ## instead be added to characters Buffy, Spike and ships Buffy/Spike romancecat_to_characters_ships:true +[www.twcslibrary.net] +## Some sites require login (or login for some rated stories) The +## program can prompt you, or you can save it in config. In +## commandline version, this should go in your personal.ini, not +## defaults.ini. +#username:YourName +#password:yourpassword + +## Some sites also require the user to confirm they are adult for +## adult content. In commandline version, this should go in your +## personal.ini, not defaults.ini. +#is_adult:true + +## twcslibrary.net (ab)uses series as personal reading lists. +collect_series: false + [www.twilightarchives.com] ## Site dedicated to these categories/characters/ships extracategories:Twilight diff --git a/fanficfare/adapters/__init__.py b/fanficfare/adapters/__init__.py index 4717ac6c..b0239fbd 100644 --- a/fanficfare/adapters/__init__.py +++ b/fanficfare/adapters/__init__.py @@ -138,6 +138,11 @@ import adapter_buffygilescom import adapter_andromedawebcom import adapter_artemisfowlcom import adapter_naiceanilmenet +import adapter_deepinmysoulnet +import adapter_haremlucifaelcom +import adapter_kiarepositorymujajinet +import adapter_fanfictionlucifaelcom +import adapter_adultfanfictionorg ## 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 --git a/fanficfare/adapters/adapter_adultfanfictionorg.py b/fanficfare/adapters/adapter_adultfanfictionorg.py new file mode 100644 index 00000000..6925d9fc --- /dev/null +++ b/fanficfare/adapters/adapter_adultfanfictionorg.py @@ -0,0 +1,372 @@ +# -*- coding: utf-8 -*- + +# Copyright 2013 Fanficdownloader team, 2015 FanFicFare team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ +### Written by GComyn +################################################################################ +import time +import logging +logger = logging.getLogger(__name__) +import re +import sys + +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +################################################################################ + +def getClass(): + return AdultFanFictionOrgAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class AdultFanFictionOrgAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + logger.debug("AdultFanFictionOrgAdapter.__init__ - url='{0}'".format(url)) + + self.decode = ["utf8", + "Windows-1252"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + #Setting the 'Zone' for each "Site" + self.zone = self.parsedUrl.netloc.split('.')[0] + + # normalized story URL. + if self.zone in ('hp','anime2','anime','bleach','books','buffy','cartoon','celeb','comics','games','inu','lotr', + 'manga','naruto','ne','original','tv','xmen','ygo','yuyu'): + self._setURL('http://' + self.zone + '.' + self.getSiteDomain() + '/story.php?no='+self.story.getMetadata('storyId')) + else: + raise exceptions.StoryDoesNotExist('{0}.{1} is not a valid URL for this site. Examples: {2}'.format(self.zone,self.getSiteDomain(),str(self.getSiteExampleURLs()))) + #I know the above get's captured by previous code, but I didn't want it to continue if the url was incorrect, and actually got here. + + # Each adapter needs to have a unique site abbreviation. + #self.story.setMetadata('siteabbrev',self.getSiteAbbrev()) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev',self.zone+'aff') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%Y-%m-%d" + + + ##This method will be moved to the sub-adapters + @classmethod + def getSiteAbbrev(self): + return self.zone+'aff' + + ##This method will be moved to the sub-adapters + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'adult-fanfiction.org' + + @classmethod + def getAcceptDomains(cls): + # mobile.fimifction.com isn't actually a valid domain, but we can still get the story id from URLs anyway + return ['anime.adult-fanfiction.org', + 'anime2.adult-fanfiction.org', + 'bleach.adult-fanfiction.org', + 'books.adult-fanfiction.org', + 'buffy.adult-fanfiction.org', + 'cartoon.adult-fanfiction.org', + 'celeb.adult-fanfiction.org', + 'comics.adult-fanfiction.org', + 'ff.adult-fanfiction.org', + 'games.adult-fanfiction.org', + 'hp.adult-fanfiction.org', + 'inu.adult-fanfiction.org', + 'lotr.adult-fanfiction.org', + 'manga.adult-fanfiction.org', + 'movies.adult-fanfiction.org', + 'naruto.adult-fanfiction.org', + 'ne.adult-fanfiction.org', + 'original.adult-fanfiction.org', + 'tv.adult-fanfiction.org', + 'xmen.adult-fanfiction.org', + 'ygo.adult-fanfiction.org', + 'yuyu.adult-fanfiction.org'] + + + @classmethod + def getSiteExampleURLs(self): + return ("http://anime.adult-fanfiction.org/story.php?no=123456789 " + + "http://anime2.adult-fanfiction.org/story.php?no=123456789 " + + "http://bleach.adult-fanfiction.org/story.php?no=123456789 " + + "http://books.adult-fanfiction.org/story.php?no=123456789 " + + "http://buffy.adult-fanfiction.org/story.php?no=123456789 " + + "http://cartoon.adult-fanfiction.org/story.php?no=123456789 " + + "http://celeb.adult-fanfiction.org/story.php?no=123456789 " + + "http://comics.adult-fanfiction.org/story.php?no=123456789 " + + "http://ff.adult-fanfiction.org/story.php?no=123456789 " + + "http://games.adult-fanfiction.org/story.php?no=123456789 " + + "http://hp.adult-fanfiction.org/story.php?no=123456789 " + + "http://inu.adult-fanfiction.org/story.php?no=123456789 " + + "http://lotr.adult-fanfiction.org/story.php?no=123456789 " + + "http://manga.adult-fanfiction.org/story.php?no=123456789 " + + "http://movies.adult-fanfiction.org/story.php?no=123456789 " + + "http://naruto.adult-fanfiction.org/story.php?no=123456789 " + + "http://ne.adult-fanfiction.org/story.php?no=123456789 " + + "http://original.adult-fanfiction.org/story.php?no=123456789 " + + "http://tv.adult-fanfiction.org/story.php?no=123456789 " + + "http://xmen.adult-fanfiction.org/story.php?no=123456789 " + + "http://ygo.adult-fanfiction.org/story.php?no=123456789 " + + "http://yuyu.adult-fanfiction.org/story.php?no=123456789") + + def getSiteURLPattern(self): + return r'http?://(hp|anime|anime2|bleach|books|buffy|cartoon|celeb|comics|ff|games|hp|inu|lotr|manga|naruto|ne|original|tv|xmen|ygo|yuyu)\.adult-fanfiction\.org/story\.php\?no=\d+$' + + ##This is not working right now, so I'm commenting it out, but leaving it for future testing + ## Login seems to be reasonably standard across eFiction sites. + #def needToLoginCheck(self, data): + ##This adapter will always require a login + # return True + +#
+#
E-mail: +# +# Email is required.Invalid E-mail.
+#
Password: +# +# password is required.Minimum 8 characters8.Exceeded 32 characters.
+#

+# +#
+#
+ + + ##This is not working right now, so I'm commenting it out, but leaving it for future testing + #def performLogin(self, url, soup): + # params = {} + + # if self.password: + # params['email'] = self.username + # params['pass1'] = self.password + # else: + # params['email'] = self.getConfig("username") + # params['pass1'] = self.getConfig("password") + # params['submit'] = 'Login' + + # # copy all hidden input tags to pick up appropriate tokens. + # for tag in soup.findAll('input',{'type':'hidden'}): + # params[tag['name']] = tag['value'] + + # logger.debug("Will now login to URL {0} as {1} with password: {2}".format(url, params['email'],params['pass1'])) + + # d = self._postUrl(url, params, usecache=False) + # d = self._fetchUrl(url, params, usecache=False) + # soup = self.make_soup(d) + + #if not (soup.find('form', {'name' : 'login'}) == None): + # logger.info("Failed to login to URL %s as %s" % (url, params['email'])) + # raise exceptions.FailedToLogin(url,params['email']) + # return False + #else: + # return True + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def doExtractChapterUrlsAndMetadata(self, get_cover=True): + + ## You need to have your is_adult set to true to get this story + if not (self.is_adult or self.getConfig("is_adult")): + raise exceptions.AdultCheckRequired(self.url) + + url = self.url + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code in 404: + raise exceptions.StoryDoesNotExist("Code: 404. %s"%self.url) + elif e.code == 410: + raise exceptions.StoryDoesNotExist("Code: 410. %s"%self.url) + elif e.code == 401: + self.needToLogin = True + data = '' + else: + raise e + + if "The dragons running the back end of the site can not seem to find the story you are looking for." in data: + raise exceptions.StoryDoesNotExist(self.zone+'.'+self.getSiteDomain() + +" says: The dragons running the back end of the site can not seem to find the story you are looking for.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = self.make_soup(data) + + ##This is not working right now, so I'm commenting it out, but leaving it for future testing + #self.performLogin(url, soup) + + # Now go hunting for all the meta data and the chapter list. + + ## Title + ## Some of the titles have a backslash on the story page, but not on the Author's page + ## So I am removing it from the title, so it can be found on the Author's page further in the code. + ## Also, some titles may have extra spaces ' ', and the search on the Author's page removes them, + ## so I have to here as well. I used multiple replaces to make sure, since I did the same below. + a = soup.find('a', href=re.compile(r'story.php\?no='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a).replace('\\','').replace(' ',' ').replace(' ',' ').replace(' ',' ').strip()) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"profile.php\?no=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl',a['href']) + self.story.setMetadata('author',stripHTML(a)) + + # Find the chapters: + chapters = soup.find('div',{'id':'snav'}) + for i, chapter in enumerate(chapters.findAll('a')): + self.chapterUrls.append((stripHTML(chapter),self.url+'&chapter='+str(i+1))) + + self.story.setMetadata('numChapters', len(self.chapterUrls)) + + ##The story page does not give much Metadata, so we go to the Author's page + + ##Get the first Author page to see if there are multiple pages. + ##AFF doesn't care if the page number is larger than the actual pages, + ##it will continue to show the last page even if the variable is larger than the actual page + author_Url = self.story.getMetadata('authorUrl')+'&view=story&zone='+self.zone+'&page=1' + + ##I'm resetting the author page to the zone for this story + self.story.setMetadata('authorUrl',author_Url) + + logger.debug('Getting the author page: {0}'.format(author_Url)) + try: + adata = self._fetchUrl(author_Url) + except urllib2.HTTPError, e: + if e.code in 404: + raise exceptions.StoryDoesNotExist("Author Page: Code: 404. %s"%author_Url) + elif e.code == 410: + raise exceptions.StoryDoesNotExist("Author Page: Code: 410. %s"%author_Url) + else: + raise e + + if "The member you are looking for does not exist." in adata: + raise exceptions.StoryDoesNotExist(self.zone+'.'+self.getSiteDomain() +" says: The member you are looking for does not exist.") + + asoup = self.make_soup(adata) + + ##Getting the number of pages + pages=asoup.find('div',{'class' : 'pagination'}).findAll('li')[-1].find('a') + if not pages == None: + pages = pages['href'].split('=')[-1] + else: + pages = 0 + logger.info(pages) + ##If there is only 1 page of stories, check it to get the Metadata, + if pages == 0: + a = asoup.findAll('li') + for lc2 in a: + if lc2.find('a', href=re.compile(r'story.php\?no='+self.story.getMetadata('storyId')+"$")): + break + ## otherwise go through the pages + else: + page=1 + i=0 + while i == 0: + ##We already have the first page, so if this is the first time through, skip getting the page + if page != 1: + author_Url = self.story.getMetadata('authorUrl')+'&view=story&zone='+self.zone+'&page='+str(page) + logger.debug('Getting the author page: {0}'.format(author_Url)) + try: + adata = self._fetchUrl(author_Url) + except urllib2.HTTPError, e: + if e.code in 404: + raise exceptions.StoryDoesNotExist("Author Page: Code: 404. %s"%author_Url) + elif e.code == 410: + raise exceptions.StoryDoesNotExist("Author Page: Code: 410. %s"%author_Url) + else: + raise e + ##This will probably never be needed, since AFF doesn't seem to care what number you put as + ## the page number, it will default to the last page, even if you use 1000, for an author + ## that only hase 5 pages of stories, but I'm keeping it in to appease Saint Justin Case (just in case). + if "The member you are looking for does not exist." in adata: + raise exceptions.StoryDoesNotExist(self.zone+'.'+self.getSiteDomain() +" says: The member you are looking for does not exist.") + + asoup = self.make_soup(adata) + + a = asoup.findAll('li') + for lc2 in a: + if lc2.find('a', href=re.compile(r'story.php\?no='+self.story.getMetadata('storyId')+"$")): + i=1 + break + page = page + 1 + if page > pages: + break + + ##Split the Metadata up into a list + ##We have to change the soup type to a string, then remove the newlines, and double spaces, + ##then changes the
to '-:-', which seperates the different elemeents. + ##Then we strip the HTML elements from the string. + ##There is also a double
, so we have to fix that, then remove the leading and trailing '-:-'. + ##They are always in the same order. + liMetadata = stripHTML(str(lc2).replace('\n','').replace('\r','').replace('\t',' ').replace(' ',' ').replace(' ',' ').replace(' ',' ').replace(r'
','-:-')) + liMetadata = liMetadata.replace(r'-:--:-','-:-').strip('-:-').strip('-:-') + + for i, value in enumerate(liMetadata.split('-:-')): + ##The item 6 is the reviews... We are disregarding them. + ##The item 7 is the 'Dragon Prints'... not sure what they are, so disregarding them. + ##The 0 item is the title + if i == 0: + if value <> self.story.getMetadata('title'): + raise exceptions.StoryDoesNotExist('Did not find story in author story list: {0}'.format(author_Url)) + elif i == 1: + ##Get the description + self.story.setMetadata('description',stripHTML(value.strip())) + elif i == 2: + ##The Get the Category + self.story.setMetadata('category',value.replace(r'>',r'>').replace(r'Located :',r'').strip()) + elif i == 3: + ##Get the Erotic Tags + value = stripHTML(value.replace(r'Content Tags :',r'')).strip() + for code in re.split(r'\s',value): + self.story.addToList('eroticatags',code) + elif i == 4: + ##Get the Posted Date + value = value.replace(r'Posted :',r'').strip() + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + elif i == 5: + ##Get the 'Updated' Edited date + ##AFF has the time for the Updated date, and we only want the date, + ##so we take the first 10 characters only + value = value.replace(r'Edited :',r'').strip()[0:10] + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + # grab the text for an individual chapter. + def getChapterText(self, url): + #Since each chapter is on 1 page, we don't need to do anything special, just get the content of the page. + logger.debug('Getting chapter text from: %s' % url) + + soup = self.make_soup(self._fetchUrl(url)) + chaptertag = soup.find('div',{'class' : 'pagination'}).parent.findNext('td') + + if None == chaptertag: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,chaptertag) diff --git a/fanficfare/adapters/adapter_deepinmysoulnet.py b/fanficfare/adapters/adapter_deepinmysoulnet.py new file mode 100644 index 00000000..954d952e --- /dev/null +++ b/fanficfare/adapters/adapter_deepinmysoulnet.py @@ -0,0 +1,300 @@ +# -*- coding: utf-8 -*- + +# Copyright 2013 Fanficdownloader team, 2015 FanFicFare team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + + +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return DeepInMySoulNetAdapter ## XXX + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class DeepInMySoulNetAdapter(BaseSiteAdapter): # XXX + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + # XXX Most sites don't have the /fiction part. Replace all to remove it usually. + self._setURL('http://' + self.getSiteDomain() + '/fiction/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','dimsn') ## XXX + + # 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 'www.deepinmysoul.net' # XXX + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/fiction/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/fiction/viewstory.php?sid=")+r"\d+$" + + ## Login seems to be reasonably standard across eFiction sites. + def needToLoginCheck(self, data): + if 'Registered Users Only' in data \ + or 'There is no such account on our website' in data \ + or "That password doesn't match the one in our database" in data: + return True + else: + return False + + def performLogin(self, url): + params = {} + + if self.password: + params['penname'] = self.username + params['password'] = self.password + else: + params['penname'] = self.getConfig("username") + params['password'] = self.getConfig("password") + params['cookiecheck'] = '1' + params['submit'] = 'Submit' + + loginUrl = 'http://' + self.getSiteDomain() + '/fiction/user.php?action=login' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['penname'])) + + d = self._fetchUrl(loginUrl, params) + + if "Member Account" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['penname'])) + raise exceptions.FailedToLogin(url,params['penname']) + return False + else: + return True + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + # Weirdly, different sites use different warning numbers. + # If the title search below fails, there's a good chance + # you need a different number. print data at that point + # and see what the 'click here to continue' url says. + addurl = "&warning=4" + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url) + + # Since the warning text can change by warning level, let's + # look for the warning pass url. ksarchive uses + # &warning= -- actually, so do other sites. Must be an + # eFiction book. + + # fiction/viewstory.php?sid=1882&warning=4 + # fiction/viewstory.php?sid=1654&ageconsent=ok&warning=5 + #print data + m = re.search(r"'fiction/viewstory.php\?sid=29(&warning=4)'",data) + m = re.search(r"'fiction/viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = self.make_soup(data) + + # Now go hunting for all the meta data and the chapter list. + + pagetitle = soup.find('div',{'id':'pagecontent'}) + + ## Title + a = pagetitle.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 = pagetitle.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+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')+"&chapter=\d+$")): + # just in case there's tags, like in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/fiction/'+chapter['href']+addurl)) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + # 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 "" + + # Rated: NC-17
etc + labels = soup.findAll('span',{'class':'label'}) + for labelspan in labels: + value = labelspan.nextSibling + label = labelspan.string + + if 'Summary' in label: + ## Everything until the next span class='label' + svalue = "" + while 'label' not in defaultGetattr(value,'class'): + svalue += unicode(value) + value = value.nextSibling + self.setDescription(url,svalue) + #self.story.setMetadata('description',stripHTML(svalue)) + + if 'Rated' in label: + self.story.setMetadata('rating', value) + + if 'Word count' in label: + self.story.setMetadata('numWords', value) + + if 'Categories' in label: + cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories')) + for cat in cats: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + for char in chars: + self.story.addToList('characters',char.string) + + if 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) + for genre in genres: + self.story.addToList('genre',genre.string) + + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=3')) + for warning in warnings: + self.story.addToList('warnings',warning.string) + + if 'Completed' in label: + if 'Yes' in value: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label: + # there's a stray [ at the end. + #value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + try: + # Find Series name from series URL. + a = soup.find('a', href=re.compile(r"fiction/viewseries.php\?seriesid=\d+")) + series_name = a.string + series_url = 'http://'+self.host+'/'+a['href'] + + # use BeautifulSoup HTML parser to make everything easier to find. + seriessoup = self.make_soup(self._fetchUrl(series_url)) + storyas = seriessoup.findAll('a', href=re.compile(r'^fiction/viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('fiction/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._fetchUrl(url)) + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) + diff --git a/fanficfare/adapters/adapter_fanfictionlucifaelcom.py b/fanficfare/adapters/adapter_fanfictionlucifaelcom.py new file mode 100644 index 00000000..95081539 --- /dev/null +++ b/fanficfare/adapters/adapter_fanfictionlucifaelcom.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- + +# Copyright 2015 FanFicFare team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +from base_efiction_adapter import BaseEfictionAdapter + +class FanfictionLucifaelComAdapter(BaseEfictionAdapter): + + @staticmethod + def getSiteDomain(): + return 'fanfiction.lucifael.com' + + @classmethod + def getSiteAbbrev(self): + return 'luci' + + @classmethod + def getDateFormat(self): + return "%d/%m/%Y" + +def getClass(): + return FanfictionLucifaelComAdapter diff --git a/fanficfare/adapters/adapter_haremlucifaelcom.py b/fanficfare/adapters/adapter_haremlucifaelcom.py new file mode 100644 index 00000000..4e91d72b --- /dev/null +++ b/fanficfare/adapters/adapter_haremlucifaelcom.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- + +# Copyright 2015 FanFicFare team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +from base_efiction_adapter import BaseEfictionAdapter + +class HaremLucifaelComAdapter(BaseEfictionAdapter): + + @staticmethod + def getSiteDomain(): + return 'harem.lucifael.com' + + @classmethod + def getSiteAbbrev(self): + return 'seraglio' + + @classmethod + def getDateFormat(self): + return "%d/%m/%Y" + +def getClass(): + return HaremLucifaelComAdapter diff --git a/fanficfare/adapters/adapter_kiarepositorymujajinet.py b/fanficfare/adapters/adapter_kiarepositorymujajinet.py new file mode 100644 index 00000000..bc209509 --- /dev/null +++ b/fanficfare/adapters/adapter_kiarepositorymujajinet.py @@ -0,0 +1,300 @@ +# -*- coding: utf-8 -*- + +# Copyright 2013 Fanficdownloader team, 2015 FanFicFare team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + + +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return KiaRepositoryMujajiNetAdapter ## XXX + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class KiaRepositoryMujajiNetAdapter(BaseSiteAdapter): # XXX + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + # XXX Most sites don't have the /fiction part. Replace all to remove it usually. + self._setURL('http://' + self.getSiteDomain() + '/repository/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','kia') ## XXX + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%d %b %Y" ## XXX + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'mujaji.net' # XXX + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/repository/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/repository/viewstory.php?sid=")+r"\d+$" + + ## Login seems to be reasonably standard across eFiction sites. + def needToLoginCheck(self, data): + if 'Registered Users Only' in data \ + or 'There is no such account on our website' in data \ + or "That password doesn't match the one in our database" in data: + return True + else: + return False + + def performLogin(self, url): + params = {} + + if self.password: + params['penname'] = self.username + params['password'] = self.password + else: + params['penname'] = self.getConfig("username") + params['password'] = self.getConfig("password") + params['cookiecheck'] = '1' + params['submit'] = 'Submit' + + loginUrl = 'http://' + self.getSiteDomain() + '/repository/user.php?action=login' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['penname'])) + + d = self._fetchUrl(loginUrl, params) + + if "Member Account" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['penname'])) + raise exceptions.FailedToLogin(url,params['penname']) + return False + else: + return True + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + # Weirdly, different sites use different warning numbers. + # If the title search below fails, there's a good chance + # you need a different number. print data at that point + # and see what the 'click here to continue' url says. + addurl = "&warning=4" + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url) + + # Since the warning text can change by warning level, let's + # look for the warning pass url. ksarchive uses + # &warning= -- actually, so do other sites. Must be an + # eFiction book. + + # fiction/viewstory.php?sid=1882&warning=4 + # fiction/viewstory.php?sid=1654&ageconsent=ok&warning=5 + #print data + m = re.search(r"'repository/viewstory.php\?sid=29(&warning=4)'",data) + m = re.search(r"'repository/viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = self.make_soup(data) + + # Now go hunting for all the meta data and the chapter list. + + pagetitle = soup.find('div',{'id':'pagetitle'}) + + ## Title + a = pagetitle.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 = pagetitle.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+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')+"&chapter=\d+$")): + # just in case there's tags, like in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/repository/'+chapter['href']+addurl)) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + # 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 "" + + # Rated: NC-17
etc + labels = soup.findAll('span',{'class':'label'}) + for labelspan in labels: + value = labelspan.nextSibling + label = labelspan.string + + if 'Summary' in label: + ## Everything until the next span class='label' + svalue = "" + while 'label' not in defaultGetattr(value,'class'): + svalue += unicode(value) + value = value.nextSibling + self.setDescription(url,svalue) + #self.story.setMetadata('description',stripHTML(svalue)) + + if 'Rated' in label: + self.story.setMetadata('rating', value) + + if 'Word count' in label: + self.story.setMetadata('numWords', value) + + if 'Categories' in label: + cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories')) + for cat in cats: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + for char in chars: + self.story.addToList('characters',char.string) + + if 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) + for genre in genres: + self.story.addToList('genre',genre.string) + + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=3')) + for warning in warnings: + self.story.addToList('warnings',warning.string) + + if 'Completed' in label: + if 'Yes' in value: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label: + # there's a stray [ at the end. + #value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + try: + # Find Series name from series URL. + a = soup.find('a', href=re.compile(r"repository/viewseries.php\?seriesid=\d+")) + series_name = a.string + series_url = 'http://'+self.host+'/'+a['href'] + + # use BeautifulSoup HTML parser to make everything easier to find. + seriessoup = self.make_soup(self._fetchUrl(series_url)) + storyas = seriessoup.findAll('a', href=re.compile(r'^repository/viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('repository/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._fetchUrl(url)) + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) + diff --git a/fanficfare/defaults.ini b/fanficfare/defaults.ini index e08d2e85..c4ed1215 100644 --- a/fanficfare/defaults.ini +++ b/fanficfare/defaults.ini @@ -25,7 +25,7 @@ ## titlepage_entries: category,genre, status,dateUpdated,rating ## [epub] ## # overrides defaults & site section -## titlepage_entries: category,genre, status,datePublished,dateUpdated,dateCreated +## titlepage_entries: category,genre,status,datePublished,dateUpdated,dateCreated ## [www.whofic.com:epub] ## # overrides defaults, site section & format section ## titlepage_entries: category,genre, status,datePublished @@ -754,6 +754,19 @@ extratags: FanFiction,Testing,Text [test1.com:html] extratags: FanFiction,Testing,HTML +[adult-fanfiction.org] +extra_valid_entries:eroticatags,disclaimer +eroticatags_label:Erotica Tags +disclaimer_label:Disclaimer +extra_titlepage_entries:eroticatags,disclaimer + +## Some sites require login (or login for some rated stories) The +## program can prompt you, or you can save it in config. In +## commandline version, this should go in your personal.ini, not +## defaults.ini. +#username:YourName +#password:yourpassword + [archive.skyehawke.com] [archiveofourown.org] @@ -1124,14 +1137,6 @@ romance_label: Romance ## this should go in your personal.ini, not defaults.ini. #is_adult:true -[ficwad.com] -## Some sites require login (or login for some rated stories) The -## program can prompt you, or you can save it in config. In -## commandline version, this should go in your personal.ini, not -## defaults.ini. -#username:YourName -#password:yourpassword - [fictionmania.tv] ## website encoding(s) In theory, each website reports the character ## encoding they use for each page. In practice, some sites report it @@ -1188,6 +1193,14 @@ views_label:Views likes_label:Likes dislikes_label:Dislikes +[ficwad.com] +## Some sites require login (or login for some rated stories) The +## program can prompt you, or you can save it in config. In +## commandline version, this should go in your personal.ini, not +## defaults.ini. +#username:YourName +#password:yourpassword + [finestories.com] ## Some sites require login (or login for some rated stories) The ## program can prompt you, or you can save it in config. In @@ -1229,12 +1242,35 @@ universe_as_series: true ## cover image. This lets you exclude them. cover_exclusion_regexp:/css/bir.png +[forum.questionablequesting.com] +## see [base_xenforoforum] + +## Some sites require login (or login for some rated stories) The +## program can prompt you, or you can save it in config. In +## commandline version, this should go in your personal.ini, not +## defaults.ini. +#username:YourName +#password:yourpassword + [forums.spacebattles.com] ## see [base_xenforoforum] [forums.sufficientvelocity.com] ## see [base_xenforoforum] +[harem.lucifael.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. +#is_adult:true + +## Some sites require login (or login for some rated stories) The +## program can prompt you, or you can save it in config. In +## commandline version, this should go in your personal.ini, not +## defaults.ini. +#username:YourName +#password:yourpassword + [hlfiction.net] ## Site dedicated to these categories/characters/ships extracategories:Highlander @@ -1347,6 +1383,19 @@ extra_titlepage_entries: eroticatags ## Site dedicated to these categories/characters/ships extracategories:Merlin +[mujaji.net] +## Some sites require login (or login for some rated stories) The +## program can prompt you, or you can save it in config. In +## commandline version, this should go in your personal.ini, not +## defaults.ini. +#username:YourName +#password:yourpassword + +## Some sites also require the user to confirm they are adult for +## adult content. In commandline version, this should go in your +## personal.ini, not defaults.ini. +#is_adult:true + [national-library.net] ## Site dedicated to these categories/characters/ships extracategories:West Wing @@ -1448,15 +1497,16 @@ extracategories:My Little Pony: Friendship is Magic ## Site dedicated to these categories/characters/ships extracategories:The Pretender -[forum.questionablequesting.com] -## see [base_xenforoforum] +[quotev.com] +extra_valid_entries:pages,readers,reads,favorites,searchtags,comments +pages_label:Pages +readers_label:Readers +reads_label:Reads +favorites_label:Favorites +searchtags_label:Search Tags +comments_label:Comments -## Some sites require login (or login for some rated stories) The -## program can prompt you, or you can save it in config. In -## commandline version, this should go in your personal.ini, not -## defaults.ini. -#username:YourName -#password:yourpassword +include_in_category:category,searchtags [samandjack.net] ## Some sites require login (or login for some rated stories) The @@ -1690,6 +1740,12 @@ extraships:Draco Malfoy/Ginny Weasley ## Site dedicated to these categories/characters/ships extracategories:Stargate: SG-1 +[www.deepinmysoul.net] +## 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. +#is_adult:true + [www.destinysgateway.com] ## Some sites do not require a login, but do require the user to ## confirm they are adult for adult content. In commandline version, @@ -2116,22 +2172,6 @@ extracategories:Lord of the Rings #username:YourName #password:yourpassword -[www.twcslibrary.net] -## Some sites require login (or login for some rated stories) The -## program can prompt you, or you can save it in config. In -## commandline version, this should go in your personal.ini, not -## defaults.ini. -#username:YourName -#password:yourpassword - -## Some sites also require the user to confirm they are adult for -## adult content. In commandline version, this should go in your -## personal.ini, not defaults.ini. -#is_adult:true - -## twcslibrary.net (ab)uses series as personal reading lists. -collect_series: false - [www.tthfanfic.org] user_agent: slow_down_sleep_time:2 @@ -2169,6 +2209,22 @@ pairingcat_to_characters_ships:true ## instead be added to characters Buffy, Spike and ships Buffy/Spike romancecat_to_characters_ships:true +[www.twcslibrary.net] +## Some sites require login (or login for some rated stories) The +## program can prompt you, or you can save it in config. In +## commandline version, this should go in your personal.ini, not +## defaults.ini. +#username:YourName +#password:yourpassword + +## Some sites also require the user to confirm they are adult for +## adult content. In commandline version, this should go in your +## personal.ini, not defaults.ini. +#is_adult:true + +## twcslibrary.net (ab)uses series as personal reading lists. +collect_series: false + [www.twilightarchives.com] ## Site dedicated to these categories/characters/ships extracategories:Twilight