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
+
+#
+
+
+ ##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