Just Off The Platform II by DrT
- authora=soup.find('h1',{'class':'title'}).find('a') - self.story.setMetadata('author',authora.string) - self.story.setMetadata('authorUrl',authora['href']) - - if len(chapterlinklist) == 1: - self.chapterUrls.append((self.story.getMetadata('title'),chapterlinklist[0]['href'])) - else: - # Find the chapters: - for chapter in chapterlinklist: - # just in case there's tags, like in chapter titles. - self.chapterUrls.append((stripHTML(chapter),chapter['href'])) - - self.story.setMetadata('numChapters',len(self.chapterUrls)) - - ## Go scrape the rest of the metadata from the author's page. - data = self._fetchUrl(self.story.getMetadata('authorUrl')) - soup = bs.BeautifulSoup(data) - - #- - # [Rid] The Magical Hottiez by Aafro Man Ziegod - #
- Rating: PG-13 - Spoilers: PS/SS, CoS, PoA, GoF, QTTA, FB - 4264 hits - 5060 words
- # Genre: Humor, Romance - Main character(s): None - Ships: None - Era: Multiple Eras
- # Chaos ensues after Witch Weekly, seeking to increase readers, decides to create a boyband out of five seemingly talentless wizards: Harry Potter, Draco Malfoy, Ron Weasley, Neville Longbottom, and Oliver "Toss Your Knickers Here" Wood.
- # Published: June 3, 2002 (between Goblet of Fire and Order of Phoenix) - Updated: June 3, 2002 - #
Just Off The Platform II by DrT
+ authora=soup.find('h1',{'class':'title'}).find('a') + self.story.setMetadata('author',authora.string) + self.story.setMetadata('authorUrl',authora['href']) + + if len(chapterlinklist) == 1: + self.chapterUrls.append((self.story.getMetadata('title'),chapterlinklist[0]['href'])) + else: + # Find the chapters: + for chapter in chapterlinklist: + # just in case there's tags, like in chapter titles. + self.chapterUrls.append((stripHTML(chapter),chapter['href'])) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + ## Go scrape the rest of the metadata from the author's page. + data = self._fetchUrl(self.story.getMetadata('authorUrl')) + soup = bs.BeautifulSoup(data) + + #+ # Genre: Humor, Romance - Main character(s): None - Ships: None - Era: Multiple Eras
+ # Chaos ensues after Witch Weekly, seeking to increase readers, decides to create a boyband out of five seemingly talentless wizards: Harry Potter, Draco Malfoy, Ron Weasley, Neville Longbottom, and Oliver "Toss Your Knickers Here" Wood.
+ # Published: June 3, 2002 (between Goblet of Fire and Order of Phoenix) - Updated: June 3, 2002 + #
tag, probably taken 1:1
- # from the source text file. A simple replacement of all newline
- # characters with a break line tag should take care of formatting.
-
- # While wrapping in paragraphs would be possible, it's too much work,
- # I'd rather display the story 1:1 like it was found in the pre tag.
- content = unicode(element)
- content = content.replace('\n', '
')
-
- if self.getConfig('non_breaking_spaces'):
- return content.replace(' ', ' ')
-
- return content
+import re
+import urllib2
+import urlparse
+
+from base_adapter import BaseSiteAdapter, makeDate
+
+
+def getClass():
+ return FictionManiaTVAdapter
+
+
+def _get_query_data(url):
+ components = urlparse.urlparse(url)
+ query_data = urlparse.parse_qs(components.query)
+ return dict((key, data[0]) for key, data in query_data.items())
+
+
+class FictionManiaTVAdapter(BaseSiteAdapter):
+ SITE_ABBREVIATION = 'fmt'
+ SITE_DOMAIN = 'fictionmania.tv'
+
+ BASE_URL = 'http://' + SITE_DOMAIN + '/stories/'
+ READ_TEXT_STORY_URL_TEMPLATE = BASE_URL + 'readtextstory.html?storyID=%s'
+ DETAILS_URL_TEMPLATE = BASE_URL + 'details.html?storyID=%s'
+
+ DATETIME_FORMAT = '%m/%d/%Y'
+ ALTERNATIVE_DATETIME_FORMAT = '%m/%d/%y'
+
+ def __init__(self, config, url):
+ BaseSiteAdapter.__init__(self, config, url)
+
+ query_data = urlparse.parse_qs(self.parsedUrl.query)
+ story_id = query_data['storyID'][0]
+
+ self.story.setMetadata('storyId', story_id)
+ self._setURL(self.READ_TEXT_STORY_URL_TEMPLATE % story_id)
+ self.story.setMetadata('siteabbrev', self.SITE_ABBREVIATION)
+
+ # Always single chapters, probably should use the Anthology feature to
+ # merge chapters of a story
+ self.story.setMetadata('numChapters', 1)
+
+ def _customized_fetch_url(self, url, exception=None, parameters=None):
+ if exception:
+ try:
+ data = self._fetchUrl(url, parameters)
+ except urllib2.HTTPError:
+ raise exception(self.url)
+ # Just let self._fetchUrl throw the exception, don't catch and
+ # customize it.
+ else:
+ data = self._fetchUrl(url, parameters)
+
+ return self.make_soup(data)
+
+ @staticmethod
+ def getSiteDomain():
+ return FictionManiaTVAdapter.SITE_DOMAIN
+
+ @classmethod
+ def getSiteExampleURLs(cls):
+ return cls.READ_TEXT_STORY_URL_TEMPLATE % 1234
+
+ def getSiteURLPattern(self):
+ return 'https?' + re.escape(self.BASE_URL[len('http'):]) + '(readtextstory|readxstory|details)\.html\?storyID=\d+$'
+
+ def extractChapterUrlsAndMetadata(self):
+ url = self.DETAILS_URL_TEMPLATE % self.story.getMetadata('storyId')
+ soup = self._customized_fetch_url(url)
+
+ keep_summary_html = self.getConfig('keep_summary_html')
+ for row in soup.find('table')('tr'):
+ cells = row('td')
+ key = cells[0].b.string.strip(':')
+ try:
+ value = cells[1].string
+ except AttributeError:
+ value = None
+
+ if key == 'Title':
+ self.story.setMetadata('title', value)
+ self.chapterUrls.append((value, self.url))
+
+ elif key == 'File Name':
+ self.story.setMetadata('fileName', value)
+
+ elif key == 'File Size':
+ self.story.setMetadata('fileSize', value)
+
+ elif key == 'Author':
+ element = cells[1].a
+ self.story.setMetadata('author', element.string)
+ query_data = _get_query_data(element['href'])
+ self.story.setMetadata('authorId', query_data['word'])
+ self.story.setMetadata('authorUrl', urlparse.urljoin(url, element['href']))
+
+ elif key == 'Date Added':
+ try:
+ date = makeDate(value, self.DATETIME_FORMAT)
+ except ValueError:
+ date = makeDate(value, self.ALTERNATIVE_DATETIME_FORMAT)
+ self.story.setMetadata('datePublished', date)
+
+ elif key == 'Old Name':
+ self.story.setMetadata('oldName', value)
+
+ elif key == 'New Name':
+ self.story.setMetadata('newName', value)
+
+ elif key == 'Other Names':
+ for name in value.split(', '):
+ self.story.addToList('characters', name)
+
+ # I have no clue how the rating system works, if you are reading
+ # transgender fanfiction, you are probably an adult.
+ elif key == 'Rating':
+ self.story.setMetadata('rating', value)
+
+ elif key == 'Complete':
+ self.story.setMetadata('status', 'Complete' if value == 'Complete' else 'In-Progress')
+
+ elif key == 'Categories':
+ for element in cells[1]('a'):
+ self.story.addToList('category', element.string)
+
+ elif key == 'Key Words':
+ for element in cells[1]('a'):
+ self.story.addToList('keyWords', element.string)
+
+ elif key == 'Age':
+ element = cells[1].a
+ self.story.setMetadata('mainCharactersAge', element.string)
+
+ elif key == 'Synopsis':
+ element = cells[1]
+
+ # Replace td with div to avoid possible strange formatting in
+ # the ebook later on
+ element.name = 'div'
+
+ if keep_summary_html:
+ self.story.setMetadata('description', unicode(element))
+ else:
+ self.story.setMetadata('description', element.get_text(strip=True))
+
+ elif key == 'Reads':
+ self.story.setMetadata('readings', value)
+
+ def getChapterText(self, url):
+ soup = self._customized_fetch_url(url)
+ element = soup.find('pre')
+ element.name = 'div'
+
+ # The story's content is contained in a tag, probably taken 1:1
+ # from the source text file. A simple replacement of all newline
+ # characters with a break line tag should take care of formatting.
+
+ # While wrapping in paragraphs would be possible, it's too much work,
+ # I'd rather display the story 1:1 like it was found in the pre tag.
+ content = unicode(element)
+ content = content.replace('\n', '
')
+
+ if self.getConfig('non_breaking_spaces'):
+ return content.replace(' ', ' ')
+
+ return content
diff --git a/fff_internals/adapters/adapter_fictionpadcom.py b/fanficfare/adapters/adapter_fictionpadcom.py
similarity index 97%
rename from fff_internals/adapters/adapter_fictionpadcom.py
rename to fanficfare/adapters/adapter_fictionpadcom.py
index 36f82bf..a890276 100644
--- a/fff_internals/adapters/adapter_fictionpadcom.py
+++ b/fanficfare/adapters/adapter_fictionpadcom.py
@@ -1,194 +1,194 @@
-# -*- coding: utf-8 -*-
-
-# Copyright 2013 Fanficdownloader team
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-import time
-import logging
-logger = logging.getLogger(__name__)
-import re
-import urllib2
-import time
-import json
-
-from .. import BeautifulSoup as bs
-#from ..htmlcleanup import stripHTML
-from .. import exceptions as exceptions
-
-from base_adapter import BaseSiteAdapter, makeDate
-
-class FictionPadSiteAdapter(BaseSiteAdapter):
-
- def __init__(self, config, url):
- BaseSiteAdapter.__init__(self, config, url)
- self.story.setMetadata('siteabbrev','fpad')
- self.dateformat = "%Y-%m-%dT%H:%M:%SZ"
- self.is_adult=False
- self.username = None
- self.password = None
- # get storyId from url--url validation guarantees query correct
- m = re.match(self.getSiteURLPattern(),url)
- if m:
- self.story.setMetadata('storyId',m.group('id'))
-
- # normalized story URL.
- self._setURL("https://"+self.getSiteDomain()
- +"/author/"+m.group('author')
- +"/stories/"+self.story.getMetadata('storyId'))
- else:
- raise exceptions.InvalidStoryURL(url,
- self.getSiteDomain(),
- self.getSiteExampleURLs())
-
- @staticmethod
- def getSiteDomain():
- return 'fictionpad.com'
-
- @classmethod
- def getSiteExampleURLs(cls):
- return "https://fictionpad.com/author/Author/stories/1234/Some-Title"
-
- def getSiteURLPattern(self):
- # http://fictionpad.com/author/Serdd/stories/4275
- return r"http(s)?://(www\.)?fictionpad\.com/author/(?P[^/]+)/stories/(?P\d+)"
-
-#
- def performLogin(self):
- params = {}
-
- if self.password:
- params['login'] = self.username
- params['password'] = self.password
- else:
- params['login'] = self.getConfig("username")
- params['password'] = self.getConfig("password")
- params['remember'] = '1'
-
- loginUrl = 'http://' + self.getSiteDomain() + '/signin'
- logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
- params['login']))
-
- ## need to pull empty login page first to get authenticity_token
- soup = bs.BeautifulSoup(self._fetchUrl(loginUrl))
- params['authenticity_token']=soup.find('input', {'name':'authenticity_token'})['value']
-
- data = self._postUrl(loginUrl, params)
-
- if "Invalid email/pseudonym and password combination." in data:
- logger.info("Failed to login to URL %s as %s" % (loginUrl,
- params['login']))
- raise exceptions.FailedToLogin(loginUrl,params['login'])
-
-
- def extractChapterUrlsAndMetadata(self):
- # fetch the chapter. From that we will get almost all the
- # metadata and chapter list
-
- url=self.url
- logger.debug("URL: "+url)
-
- try:
- data = self._fetchUrl(url)
- if "This is a mature story. Please sign in to read it." in data:
- self.performLogin()
- data = self._fetchUrl(url)
-
- find = "wordyarn.config.page = "
- data = data[data.index(find)+len(find):]
- data = data[:data.index("")]
- data = data[:data.rindex(";")]
- data = data.replace('tables:','"tables":')
- tables = json.loads(data)['tables']
- except urllib2.HTTPError, e:
- if e.code == 404:
- raise exceptions.StoryDoesNotExist(url)
- else:
- raise e
-
- # looks like only one author per story allowed.
- author = tables['users'][0]
- story = tables['stories'][0]
- story_ver = tables['story_versions'][0]
- logger.debug("story:%s"%story)
-
- self.story.setMetadata('authorId',author['id'])
- self.story.setMetadata('author',author['display_name'])
- self.story.setMetadata('authorUrl','https://'+self.host+'/author/'+author['display_name']+'/stories')
-
- self.story.setMetadata('title',story_ver['title'])
- self.setDescription(url,story_ver['description'])
-
- if not ('assets/story_versions/covers' in story_ver['profile_image_url@2x']):
- self.setCoverImage(url,story_ver['profile_image_url@2x'])
-
- self.story.setMetadata('datePublished',makeDate(story['published_at'], self.dateformat))
- self.story.setMetadata('dateUpdated',makeDate(story['published_at'], self.dateformat))
-
- self.story.setMetadata('followers',story['followers_count'])
- self.story.setMetadata('comments',story['comments_count'])
- self.story.setMetadata('views',story['views_count'])
- self.story.setMetadata('likes',int(story['likes'])) # no idea why they floated these.
- if 'dislikes' in story:
- self.story.setMetadata('dislikes',int(story['dislikes']))
-
- if story_ver['is_complete']:
- self.story.setMetadata('status', 'Completed')
- else:
- self.story.setMetadata('status', 'In-Progress')
-
- self.story.setMetadata('rating', story_ver['maturity_level'])
- self.story.setMetadata('numWords', unicode(story_ver['word_count']))
-
- for i in tables['fandoms']:
- self.story.addToList('category',i['name'])
-
- for i in tables['genres']:
- self.story.addToList('genre',i['name'])
-
- for i in tables['characters']:
- self.story.addToList('characters',i['name'])
-
- for c in tables['chapters']:
- chtitle = "Chapter %d"%c['number']
- if c['title']:
- chtitle += " - %s"%c['title']
- self.chapterUrls.append((chtitle,c['body_url']))
-
- self.story.setMetadata('numChapters',len(self.chapterUrls))
-
- def getChapterText(self, url):
- logger.debug('Getting chapter text from: %s' % url)
- if not url:
- data = u"This chapter has no text."
- else:
- data = self._fetchUrl(url)
- soup = bs.BeautifulSoup(u""+data+u"")
- return self.utf8FromSoup(url,soup)
-
-def getClass():
- return FictionPadSiteAdapter
-
+# -*- coding: utf-8 -*-
+
+# Copyright 2013 Fanficdownloader team
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import time
+import logging
+logger = logging.getLogger(__name__)
+import re
+import urllib2
+import time
+import json
+
+from .. import BeautifulSoup as bs
+#from ..htmlcleanup import stripHTML
+from .. import exceptions as exceptions
+
+from base_adapter import BaseSiteAdapter, makeDate
+
+class FictionPadSiteAdapter(BaseSiteAdapter):
+
+ def __init__(self, config, url):
+ BaseSiteAdapter.__init__(self, config, url)
+ self.story.setMetadata('siteabbrev','fpad')
+ self.dateformat = "%Y-%m-%dT%H:%M:%SZ"
+ self.is_adult=False
+ self.username = None
+ self.password = None
+ # get storyId from url--url validation guarantees query correct
+ m = re.match(self.getSiteURLPattern(),url)
+ if m:
+ self.story.setMetadata('storyId',m.group('id'))
+
+ # normalized story URL.
+ self._setURL("https://"+self.getSiteDomain()
+ +"/author/"+m.group('author')
+ +"/stories/"+self.story.getMetadata('storyId'))
+ else:
+ raise exceptions.InvalidStoryURL(url,
+ self.getSiteDomain(),
+ self.getSiteExampleURLs())
+
+ @staticmethod
+ def getSiteDomain():
+ return 'fictionpad.com'
+
+ @classmethod
+ def getSiteExampleURLs(cls):
+ return "https://fictionpad.com/author/Author/stories/1234/Some-Title"
+
+ def getSiteURLPattern(self):
+ # http://fictionpad.com/author/Serdd/stories/4275
+ return r"http(s)?://(www\.)?fictionpad\.com/author/(?P[^/]+)/stories/(?P\d+)"
+
+#
+ def performLogin(self):
+ params = {}
+
+ if self.password:
+ params['login'] = self.username
+ params['password'] = self.password
+ else:
+ params['login'] = self.getConfig("username")
+ params['password'] = self.getConfig("password")
+ params['remember'] = '1'
+
+ loginUrl = 'http://' + self.getSiteDomain() + '/signin'
+ logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
+ params['login']))
+
+ ## need to pull empty login page first to get authenticity_token
+ soup = bs.BeautifulSoup(self._fetchUrl(loginUrl))
+ params['authenticity_token']=soup.find('input', {'name':'authenticity_token'})['value']
+
+ data = self._postUrl(loginUrl, params)
+
+ if "Invalid email/pseudonym and password combination." in data:
+ logger.info("Failed to login to URL %s as %s" % (loginUrl,
+ params['login']))
+ raise exceptions.FailedToLogin(loginUrl,params['login'])
+
+
+ def extractChapterUrlsAndMetadata(self):
+ # fetch the chapter. From that we will get almost all the
+ # metadata and chapter list
+
+ url=self.url
+ logger.debug("URL: "+url)
+
+ try:
+ data = self._fetchUrl(url)
+ if "This is a mature story. Please sign in to read it." in data:
+ self.performLogin()
+ data = self._fetchUrl(url)
+
+ find = "wordyarn.config.page = "
+ data = data[data.index(find)+len(find):]
+ data = data[:data.index("")]
+ data = data[:data.rindex(";")]
+ data = data.replace('tables:','"tables":')
+ tables = json.loads(data)['tables']
+ except urllib2.HTTPError, e:
+ if e.code == 404:
+ raise exceptions.StoryDoesNotExist(url)
+ else:
+ raise e
+
+ # looks like only one author per story allowed.
+ author = tables['users'][0]
+ story = tables['stories'][0]
+ story_ver = tables['story_versions'][0]
+ logger.debug("story:%s"%story)
+
+ self.story.setMetadata('authorId',author['id'])
+ self.story.setMetadata('author',author['display_name'])
+ self.story.setMetadata('authorUrl','https://'+self.host+'/author/'+author['display_name']+'/stories')
+
+ self.story.setMetadata('title',story_ver['title'])
+ self.setDescription(url,story_ver['description'])
+
+ if not ('assets/story_versions/covers' in story_ver['profile_image_url@2x']):
+ self.setCoverImage(url,story_ver['profile_image_url@2x'])
+
+ self.story.setMetadata('datePublished',makeDate(story['published_at'], self.dateformat))
+ self.story.setMetadata('dateUpdated',makeDate(story['published_at'], self.dateformat))
+
+ self.story.setMetadata('followers',story['followers_count'])
+ self.story.setMetadata('comments',story['comments_count'])
+ self.story.setMetadata('views',story['views_count'])
+ self.story.setMetadata('likes',int(story['likes'])) # no idea why they floated these.
+ if 'dislikes' in story:
+ self.story.setMetadata('dislikes',int(story['dislikes']))
+
+ if story_ver['is_complete']:
+ self.story.setMetadata('status', 'Completed')
+ else:
+ self.story.setMetadata('status', 'In-Progress')
+
+ self.story.setMetadata('rating', story_ver['maturity_level'])
+ self.story.setMetadata('numWords', unicode(story_ver['word_count']))
+
+ for i in tables['fandoms']:
+ self.story.addToList('category',i['name'])
+
+ for i in tables['genres']:
+ self.story.addToList('genre',i['name'])
+
+ for i in tables['characters']:
+ self.story.addToList('characters',i['name'])
+
+ for c in tables['chapters']:
+ chtitle = "Chapter %d"%c['number']
+ if c['title']:
+ chtitle += " - %s"%c['title']
+ self.chapterUrls.append((chtitle,c['body_url']))
+
+ self.story.setMetadata('numChapters',len(self.chapterUrls))
+
+ def getChapterText(self, url):
+ logger.debug('Getting chapter text from: %s' % url)
+ if not url:
+ data = u"This chapter has no text."
+ else:
+ data = self._fetchUrl(url)
+ soup = bs.BeautifulSoup(u""+data+u"")
+ return self.utf8FromSoup(url,soup)
+
+def getClass():
+ return FictionPadSiteAdapter
+
diff --git a/fff_internals/adapters/adapter_fictionpresscom.py b/fanficfare/adapters/adapter_fictionpresscom.py
similarity index 97%
rename from fff_internals/adapters/adapter_fictionpresscom.py
rename to fanficfare/adapters/adapter_fictionpresscom.py
index 795ff94..e7f973e 100644
--- a/fff_internals/adapters/adapter_fictionpresscom.py
+++ b/fanficfare/adapters/adapter_fictionpresscom.py
@@ -1,51 +1,51 @@
-# -*- coding: utf-8 -*-
-
-# Copyright 2011 Fanficdownloader team
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-import time
-import logging
-logger = logging.getLogger(__name__)
-import re
-import urllib2
-import time
-
-## They're from the same people and pretty much identical.
-from adapter_fanfictionnet import FanFictionNetSiteAdapter
-
-class FictionPressComSiteAdapter(FanFictionNetSiteAdapter):
-
- def __init__(self, config, url):
- FanFictionNetSiteAdapter.__init__(self, config, url)
- self.story.setMetadata('siteabbrev','fpcom')
-
- @staticmethod
- def getSiteDomain():
- return 'www.fictionpress.com'
-
- @classmethod
- def getAcceptDomains(cls):
- return ['www.fictionpress.com','m.fictionpress.com']
-
- @classmethod
- def getSiteExampleURLs(cls):
- return "https://www.fictionpress.com/s/1234/1/ https://www.fictionpress.com/s/1234/12/ http://www.fictionpress.com/s/1234/1/Story_Title http://m.fictionpress.com/s/1234/1/"
-
- def getSiteURLPattern(self):
- return r"https?://(www|m)?\.fictionpress\.com/s/\d+(/\d+)?(/|/[a-zA-Z0-9_-]+)?/?$"
-
-def getClass():
- return FictionPressComSiteAdapter
-
+# -*- coding: utf-8 -*-
+
+# Copyright 2011 Fanficdownloader team
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import time
+import logging
+logger = logging.getLogger(__name__)
+import re
+import urllib2
+import time
+
+## They're from the same people and pretty much identical.
+from adapter_fanfictionnet import FanFictionNetSiteAdapter
+
+class FictionPressComSiteAdapter(FanFictionNetSiteAdapter):
+
+ def __init__(self, config, url):
+ FanFictionNetSiteAdapter.__init__(self, config, url)
+ self.story.setMetadata('siteabbrev','fpcom')
+
+ @staticmethod
+ def getSiteDomain():
+ return 'www.fictionpress.com'
+
+ @classmethod
+ def getAcceptDomains(cls):
+ return ['www.fictionpress.com','m.fictionpress.com']
+
+ @classmethod
+ def getSiteExampleURLs(cls):
+ return "https://www.fictionpress.com/s/1234/1/ https://www.fictionpress.com/s/1234/12/ http://www.fictionpress.com/s/1234/1/Story_Title http://m.fictionpress.com/s/1234/1/"
+
+ def getSiteURLPattern(self):
+ return r"https?://(www|m)?\.fictionpress\.com/s/\d+(/\d+)?(/|/[a-zA-Z0-9_-]+)?/?$"
+
+def getClass():
+ return FictionPressComSiteAdapter
+
diff --git a/fff_internals/adapters/adapter_ficwadcom.py b/fanficfare/adapters/adapter_ficwadcom.py
similarity index 97%
rename from fff_internals/adapters/adapter_ficwadcom.py
rename to fanficfare/adapters/adapter_ficwadcom.py
index 7934a13..ff0012e 100644
--- a/fff_internals/adapters/adapter_ficwadcom.py
+++ b/fanficfare/adapters/adapter_ficwadcom.py
@@ -1,233 +1,233 @@
-# -*- coding: utf-8 -*-
-
-# Copyright 2011 Fanficdownloader team
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-import time
-import logging
-logger = logging.getLogger(__name__)
-import re
-import urllib2
-import time
-import httplib, urllib
-
-from .. import exceptions as exceptions
-from ..htmlcleanup import stripHTML
-
-from base_adapter import BaseSiteAdapter, makeDate
-
-class FicwadComSiteAdapter(BaseSiteAdapter):
-
- def __init__(self, config, url):
- BaseSiteAdapter.__init__(self, config, url)
- self.story.setMetadata('siteabbrev','fw')
-
- # get storyId from url--url validation guarantees second part is storyId
- self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
-
- self.username = "NoneGiven"
- self.password = ""
-
- @staticmethod
- def getSiteDomain():
- return 'ficwad.com'
-
- @classmethod
- def getSiteExampleURLs(cls):
- return "http://ficwad.com/story/1234"
-
- def getSiteURLPattern(self):
- return re.escape(r"http://"+self.getSiteDomain())+"/story/\d+?$"
-
- def performLogin(self,url):
- params = {}
-
- if self.password:
- params['username'] = self.username
- params['password'] = self.password
- else:
- params['username'] = self.getConfig("username")
- params['password'] = self.getConfig("password")
-
- loginUrl = 'http://' + self.getSiteDomain() + '/account/login'
- logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
- params['username']))
- d = self._postUrl(loginUrl,params,usecache=False)
-
- if "Login attempt failed..." in d:
- logger.info("Failed to login to URL %s as %s" % (loginUrl,
- params['username']))
- raise exceptions.FailedToLogin(url,params['username'])
- return False
- else:
- return True
-
- def use_pagecache(self):
- '''
- adapters that will work with the page cache need to implement
- this and change it to True.
- '''
- return True
-
- def extractChapterUrlsAndMetadata(self):
-
- # fetch the chapter. From that we will get almost all the
- # metadata and chapter list
-
- url = self.url
- logger.debug("URL: "+url)
-
- # use BeautifulSoup HTML parser to make everything easier to find.
- try:
- data = self._fetchUrl(url)
- # non-existent/removed story urls get thrown to the front page.
- if "Welcome to FicWad
" in data:
- raise exceptions.StoryDoesNotExist(self.url)
- soup = self.make_soup(data)
- except urllib2.HTTPError, e:
- if e.code == 404:
- raise exceptions.StoryDoesNotExist(self.url)
- else:
- raise e
-
- # if blocked, attempt login.
- if soup.find("div",{"class":"blocked"}) or soup.find("li",{"class":"blocked"}):
- if self.performLogin(url): # performLogin raises
- # FailedToLogin if it fails.
- soup = self.make_soup(self._fetchUrl(url,usecache=False))
-
- divstory = soup.find('div',id='story')
- storya = divstory.find('a',href=re.compile("^/story/\d+$"))
- if storya : # if there's a story link in the divstory header, this is a chapter page.
- # normalize story URL on chapter list.
- self.story.setMetadata('storyId',storya['href'].split('/',)[2])
- url = "http://"+self.getSiteDomain()+storya['href']
- logger.debug("Normalizing to URL: "+url)
- self._setURL(url)
- try:
- soup = self.make_soup(self._fetchUrl(url))
- except urllib2.HTTPError, e:
- if e.code == 404:
- raise exceptions.StoryDoesNotExist(self.url)
- else:
- raise e
-
- # if blocked, attempt login.
- if soup.find("div",{"class":"blocked"}) or soup.find("li",{"class":"blocked"}):
- if self.performLogin(url): # performLogin raises
- # FailedToLogin if it fails.
- soup = self.make_soup(self._fetchUrl(url,usecache=False))
-
- # title - first h4 tag will be title.
- titleh4 = soup.find('div',{'class':'storylist'}).find('h4')
- self.story.setMetadata('title', stripHTML(titleh4.a))
-
- # Find authorid and URL from... author url.
- a = soup.find('span',{'class':'author'}).find('a', href=re.compile(r"^/author/\d+"))
- self.story.setMetadata('authorId',a['href'].split('/')[2])
- self.story.setMetadata('authorUrl','http://'+self.host+a['href'])
- self.story.setMetadata('author',a.string)
-
- # description
- storydiv = soup.find("div",{"id":"story"})
- self.setDescription(url,storydiv.find("blockquote",{'class':'summary'}).p)
- #self.story.setMetadata('description', storydiv.find("blockquote",{'class':'summary'}).p.string)
-
- # most of the meta data is here:
- metap = storydiv.find("p",{"class":"meta"})
- self.story.addToList('category',metap.find("a",href=re.compile(r"^/category/\d+")).string)
-
- # warnings
- # [!!] [R] [V] [Y]
- spanreq = metap.find("span",{"class":"story-warnings"})
- if spanreq: # can be no warnings.
- for a in spanreq.findAll("a"):
- self.story.addToList('warnings',a['title'])
-
- ## perhaps not the most efficient way to parse this, using
- ## regexps for each rather than something more complex, but
- ## IMO, it's more readable and amenable to change.
- metastr = stripHTML(str(metap)).replace('\n',' ').replace('\t',' ').replace(u'\u00a0',' ')
-
- m = re.match(r".*?Rating: (.+?) -.*?",metastr)
- if m:
- self.story.setMetadata('rating', m.group(1))
-
- m = re.match(r".*?Genres: (.+?) -.*?",metastr)
- if m:
- for g in m.group(1).split(','):
- self.story.addToList('genre',g)
-
- m = re.match(r".*?Characters: (.*?) -.*?",metastr)
- if m:
- for g in m.group(1).split(','):
- if g:
- self.story.addToList('characters',g)
-
- m = re.match(r".*?Published: ([0-9-]+?) -.*?",metastr)
- if m:
- self.story.setMetadata('datePublished',makeDate(m.group(1), "%Y-%m-%d"))
-
- # Updated can have more than one space after it.
- m = re.match(r".*?Updated: ([0-9-]+?) +-.*?",metastr)
- if m:
- self.story.setMetadata('dateUpdated',makeDate(m.group(1), "%Y-%m-%d"))
-
- m = re.match(r".*? - ([0-9,]+?) words.*?",metastr)
- if m:
- self.story.setMetadata('numWords',m.group(1))
-
- if metastr.endswith("Complete"):
- self.story.setMetadata('status', 'Completed')
- else:
- self.story.setMetadata('status', 'In-Progress')
-
- # get the chapter list first this time because that's how we
- # detect the need to login.
- storylistul = soup.find('ul',{'class':'storylist'})
- if not storylistul:
- # no list found, so it's a one-chapter story.
- self.chapterUrls.append((self.story.getMetadata('title'),url))
- else:
- chapterlistlis = storylistul.findAll('li')
- for chapterli in chapterlistlis:
- if "blocked" in chapterli['class']:
- # paranoia check. We should already be logged in by now.
- raise exceptions.FailedToLogin(url,self.username)
- else:
- #print "chapterli.h4.a (%s)"%chapterli.h4.a
- self.chapterUrls.append((chapterli.h4.a.string,
- u'http://%s%s'%(self.getSiteDomain(),
- chapterli.h4.a['href'])))
- #print "self.chapterUrls:%s"%self.chapterUrls
- self.story.setMetadata('numChapters',len(self.chapterUrls))
-
- return
-
-
- def getChapterText(self, url):
- logger.debug('Getting chapter text from: %s' % url)
- soup = self.make_soup(self._fetchUrl(url))
-
- span = soup.find('div', {'id' : 'storytext'})
-
- if None == span:
- raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
-
- return self.utf8FromSoup(url,span)
-
-def getClass():
- return FicwadComSiteAdapter
-
+# -*- coding: utf-8 -*-
+
+# Copyright 2011 Fanficdownloader team
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import time
+import logging
+logger = logging.getLogger(__name__)
+import re
+import urllib2
+import time
+import httplib, urllib
+
+from .. import exceptions as exceptions
+from ..htmlcleanup import stripHTML
+
+from base_adapter import BaseSiteAdapter, makeDate
+
+class FicwadComSiteAdapter(BaseSiteAdapter):
+
+ def __init__(self, config, url):
+ BaseSiteAdapter.__init__(self, config, url)
+ self.story.setMetadata('siteabbrev','fw')
+
+ # get storyId from url--url validation guarantees second part is storyId
+ self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
+
+ self.username = "NoneGiven"
+ self.password = ""
+
+ @staticmethod
+ def getSiteDomain():
+ return 'ficwad.com'
+
+ @classmethod
+ def getSiteExampleURLs(cls):
+ return "http://ficwad.com/story/1234"
+
+ def getSiteURLPattern(self):
+ return re.escape(r"http://"+self.getSiteDomain())+"/story/\d+?$"
+
+ def performLogin(self,url):
+ params = {}
+
+ if self.password:
+ params['username'] = self.username
+ params['password'] = self.password
+ else:
+ params['username'] = self.getConfig("username")
+ params['password'] = self.getConfig("password")
+
+ loginUrl = 'http://' + self.getSiteDomain() + '/account/login'
+ logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
+ params['username']))
+ d = self._postUrl(loginUrl,params,usecache=False)
+
+ if "Login attempt failed..." in d:
+ logger.info("Failed to login to URL %s as %s" % (loginUrl,
+ params['username']))
+ raise exceptions.FailedToLogin(url,params['username'])
+ return False
+ else:
+ return True
+
+ def use_pagecache(self):
+ '''
+ adapters that will work with the page cache need to implement
+ this and change it to True.
+ '''
+ return True
+
+ def extractChapterUrlsAndMetadata(self):
+
+ # fetch the chapter. From that we will get almost all the
+ # metadata and chapter list
+
+ url = self.url
+ logger.debug("URL: "+url)
+
+ # use BeautifulSoup HTML parser to make everything easier to find.
+ try:
+ data = self._fetchUrl(url)
+ # non-existent/removed story urls get thrown to the front page.
+ if "Welcome to FicWad
" in data:
+ raise exceptions.StoryDoesNotExist(self.url)
+ soup = self.make_soup(data)
+ except urllib2.HTTPError, e:
+ if e.code == 404:
+ raise exceptions.StoryDoesNotExist(self.url)
+ else:
+ raise e
+
+ # if blocked, attempt login.
+ if soup.find("div",{"class":"blocked"}) or soup.find("li",{"class":"blocked"}):
+ if self.performLogin(url): # performLogin raises
+ # FailedToLogin if it fails.
+ soup = self.make_soup(self._fetchUrl(url,usecache=False))
+
+ divstory = soup.find('div',id='story')
+ storya = divstory.find('a',href=re.compile("^/story/\d+$"))
+ if storya : # if there's a story link in the divstory header, this is a chapter page.
+ # normalize story URL on chapter list.
+ self.story.setMetadata('storyId',storya['href'].split('/',)[2])
+ url = "http://"+self.getSiteDomain()+storya['href']
+ logger.debug("Normalizing to URL: "+url)
+ self._setURL(url)
+ try:
+ soup = self.make_soup(self._fetchUrl(url))
+ except urllib2.HTTPError, e:
+ if e.code == 404:
+ raise exceptions.StoryDoesNotExist(self.url)
+ else:
+ raise e
+
+ # if blocked, attempt login.
+ if soup.find("div",{"class":"blocked"}) or soup.find("li",{"class":"blocked"}):
+ if self.performLogin(url): # performLogin raises
+ # FailedToLogin if it fails.
+ soup = self.make_soup(self._fetchUrl(url,usecache=False))
+
+ # title - first h4 tag will be title.
+ titleh4 = soup.find('div',{'class':'storylist'}).find('h4')
+ self.story.setMetadata('title', stripHTML(titleh4.a))
+
+ # Find authorid and URL from... author url.
+ a = soup.find('span',{'class':'author'}).find('a', href=re.compile(r"^/author/\d+"))
+ self.story.setMetadata('authorId',a['href'].split('/')[2])
+ self.story.setMetadata('authorUrl','http://'+self.host+a['href'])
+ self.story.setMetadata('author',a.string)
+
+ # description
+ storydiv = soup.find("div",{"id":"story"})
+ self.setDescription(url,storydiv.find("blockquote",{'class':'summary'}).p)
+ #self.story.setMetadata('description', storydiv.find("blockquote",{'class':'summary'}).p.string)
+
+ # most of the meta data is here:
+ metap = storydiv.find("p",{"class":"meta"})
+ self.story.addToList('category',metap.find("a",href=re.compile(r"^/category/\d+")).string)
+
+ # warnings
+ # [!!] [R] [V] [Y]
+ spanreq = metap.find("span",{"class":"story-warnings"})
+ if spanreq: # can be no warnings.
+ for a in spanreq.findAll("a"):
+ self.story.addToList('warnings',a['title'])
+
+ ## perhaps not the most efficient way to parse this, using
+ ## regexps for each rather than something more complex, but
+ ## IMO, it's more readable and amenable to change.
+ metastr = stripHTML(str(metap)).replace('\n',' ').replace('\t',' ').replace(u'\u00a0',' ')
+
+ m = re.match(r".*?Rating: (.+?) -.*?",metastr)
+ if m:
+ self.story.setMetadata('rating', m.group(1))
+
+ m = re.match(r".*?Genres: (.+?) -.*?",metastr)
+ if m:
+ for g in m.group(1).split(','):
+ self.story.addToList('genre',g)
+
+ m = re.match(r".*?Characters: (.*?) -.*?",metastr)
+ if m:
+ for g in m.group(1).split(','):
+ if g:
+ self.story.addToList('characters',g)
+
+ m = re.match(r".*?Published: ([0-9-]+?) -.*?",metastr)
+ if m:
+ self.story.setMetadata('datePublished',makeDate(m.group(1), "%Y-%m-%d"))
+
+ # Updated can have more than one space after it.
+ m = re.match(r".*?Updated: ([0-9-]+?) +-.*?",metastr)
+ if m:
+ self.story.setMetadata('dateUpdated',makeDate(m.group(1), "%Y-%m-%d"))
+
+ m = re.match(r".*? - ([0-9,]+?) words.*?",metastr)
+ if m:
+ self.story.setMetadata('numWords',m.group(1))
+
+ if metastr.endswith("Complete"):
+ self.story.setMetadata('status', 'Completed')
+ else:
+ self.story.setMetadata('status', 'In-Progress')
+
+ # get the chapter list first this time because that's how we
+ # detect the need to login.
+ storylistul = soup.find('ul',{'class':'storylist'})
+ if not storylistul:
+ # no list found, so it's a one-chapter story.
+ self.chapterUrls.append((self.story.getMetadata('title'),url))
+ else:
+ chapterlistlis = storylistul.findAll('li')
+ for chapterli in chapterlistlis:
+ if "blocked" in chapterli['class']:
+ # paranoia check. We should already be logged in by now.
+ raise exceptions.FailedToLogin(url,self.username)
+ else:
+ #print "chapterli.h4.a (%s)"%chapterli.h4.a
+ self.chapterUrls.append((chapterli.h4.a.string,
+ u'http://%s%s'%(self.getSiteDomain(),
+ chapterli.h4.a['href'])))
+ #print "self.chapterUrls:%s"%self.chapterUrls
+ self.story.setMetadata('numChapters',len(self.chapterUrls))
+
+ return
+
+
+ def getChapterText(self, url):
+ logger.debug('Getting chapter text from: %s' % url)
+ soup = self.make_soup(self._fetchUrl(url))
+
+ span = soup.find('div', {'id' : 'storytext'})
+
+ if None == span:
+ raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
+
+ return self.utf8FromSoup(url,span)
+
+def getClass():
+ return FicwadComSiteAdapter
+
diff --git a/fff_internals/adapters/adapter_fimfictionnet.py b/fanficfare/adapters/adapter_fimfictionnet.py
similarity index 98%
rename from fff_internals/adapters/adapter_fimfictionnet.py
rename to fanficfare/adapters/adapter_fimfictionnet.py
index 469e931..74c29c5 100644
--- a/fff_internals/adapters/adapter_fimfictionnet.py
+++ b/fanficfare/adapters/adapter_fimfictionnet.py
@@ -1,357 +1,357 @@
-# -*- coding: utf-8 -*-
-
-# Copyright 2011 Fanficdownloader team
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-import time
-from datetime import date
-from datetime import timedelta
-import logging
-logger = logging.getLogger(__name__)
-import re
-import urllib2
-import cookielib as cl
-import json
-
-from ..htmlcleanup import stripHTML
-from .. import exceptions as exceptions
-
-from base_adapter import BaseSiteAdapter, makeDate
-
-def getClass():
- return FimFictionNetSiteAdapter
-
-class FimFictionNetSiteAdapter(BaseSiteAdapter):
-
- def __init__(self, config, url):
- BaseSiteAdapter.__init__(self, config, url)
- self.story.setMetadata('siteabbrev','fimficnet')
- self.story.setMetadata('storyId', self.parsedUrl.path.split('/',)[2])
- self._setURL("http://"+self.getSiteDomain()+"/story/"+self.story.getMetadata('storyId')+"/")
- self.is_adult = False
-
- # The date format will vary from site to site.
- # http://docs.python.org/library/datetime.html#strftime-strptime-behavior
- self.dateformat = "%d %b %Y"
-
- @staticmethod
- def getSiteDomain():
- return 'www.fimfiction.net'
-
- @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 ['www.fimfiction.net','mobile.fimfiction.net', 'www.fimfiction.com', 'mobile.fimfiction.com']
-
- @classmethod
- def getSiteExampleURLs(cls):
- return "http://www.fimfiction.net/story/1234/story-title-here http://www.fimfiction.net/story/1234/ http://www.fimfiction.com/story/1234/1/ http://mobile.fimfiction.net/story/1234/1/story-title-here/chapter-title-here"
-
- def getSiteURLPattern(self):
- return r"https?://(www|mobile)\.fimfiction\.(net|com)/story/\d+/?.*"
-
- def use_pagecache(self):
- '''
- adapters that will work with the page cache need to implement
- this and change it to True.
- '''
- return True
-
- def doExtractChapterUrlsAndMetadata(self,get_cover=True):
-
- if self.is_adult or self.getConfig("is_adult"):
- cookie = cl.Cookie(version=0, name='view_mature', value='true',
- port=None, port_specified=False,
- domain=self.getSiteDomain(), domain_specified=False, domain_initial_dot=False,
- path='/story', path_specified=True,
- secure=False,
- expires=time.time()+10000,
- discard=False,
- comment=None,
- comment_url=None,
- rest={'HttpOnly': None},
- rfc2109=False)
- self.cookiejar.set_cookie(cookie)
-
- ##---------------------------------------------------------------------------------------------------
- ## Get the story's title page. Check if it exists.
-
- try:
- # don't use cache if manual is_adult--should only happen
- # if it's an adult story and they don't have is_adult in ini.
- data = self.do_fix_blockquotes(self._fetchUrl(self.url,
- usecache=(not self.is_adult)))
- soup = self.make_soup(data)
- except urllib2.HTTPError, e:
- if e.code == 404:
- raise exceptions.StoryDoesNotExist(self.url)
- else:
- raise e
-
- if "Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result resource" in data:
- raise exceptions.StoryDoesNotExist(self.url)
-
- if "This story has been marked as having adult content. Please click below to confirm you are of legal age to view adult material in your country." in data:
- raise exceptions.AdultCheckRequired(self.url)
-
- if self.password:
- params = {}
- params['password'] = self.password
- data = self._postUrl(self.url, params)
- soup = self.make_soup(data)
-
- if not (soup.find('form', {'id' : 'password_form'}) == None):
- if self.getConfig('fail_on_password'):
- raise exceptions.FailedToDownload("%s requires story password and fail_on_password is true."%self.url)
- else:
- raise exceptions.FailedToLogin(self.url,"Story requires individual password",passwdonly=True)
-
- ##----------------------------------------------------------------------------------------------------
- ## Extract metadata
-
- storyContentBox = soup.find('div', {'class':'story_content_box'})
-
- # Title
- title = storyContentBox.find('a', {'class':re.compile(r'.*\bstory_name\b.*')})
- self.story.setMetadata('title',stripHTML(title))
-
- # Author
- author = storyContentBox.find('div', {'class':'author'}).find('a')
- self.story.setMetadata("author", stripHTML(author))
- #No longer seems to be a way to access Fimfiction's internal author ID
- self.story.setMetadata("authorId", self.story.getMetadata("author"))
- self.story.setMetadata("authorUrl", "http://%s/user/%s" % (self.getSiteDomain(), stripHTML(author)))
-
- #Rating text is replaced with full words for historical compatibility after the site changed
- #on 2014-10-27
- rating = stripHTML(storyContentBox.find('a', {'class':re.compile(r'.*\bcontent-rating-.*')}))
- rating = rating.replace("E", "Everyone").replace("T", "Teen").replace("M", "Mature")
- self.story.setMetadata("rating", rating)
-
- # Chapters
- for chapter in storyContentBox.find_all('a',{'class':'chapter_link'}):
- self.chapterUrls.append((stripHTML(chapter), 'http://'+self.host+chapter['href']))
-
- self.story.setMetadata('numChapters',len(self.chapterUrls))
-
- # Status
- # In the case of Fimfiction, possible statuses are 'Completed', 'Incomplete', 'On Hiatus' and 'Cancelled'
- # For the sake of bringing it in line with the other adapters, 'Incomplete' becomes 'In-Progress'
- # and 'Complete' becomes 'Completed'. 'Cancelled' and 'On Hiatus' are passed through, it's easy now for users
- # to change/remove if they want with replace_metadata
- status = stripHTML(storyContentBox.find('span', {'class':re.compile(r'.*\bcompleted-status-.*')}))
- status = status.replace("Incomplete", "In-Progress").replace("Complete", "Completed")
- self.story.setMetadata("status", status)
-
- # Genres and Warnings
- # warnings were folded into general categories in the 2014-10-27 site update
- categories = storyContentBox.find_all('a', {'class':re.compile(r'.*\bstory_category\b.*')})
- for category in categories:
- category = stripHTML(category)
- if category == "Gore" or category == "Sex":
- self.story.addToList('warnings', category)
- else:
- self.story.addToList('genre', category)
-
- # Word count
- wordCountText = stripHTML(storyContentBox.find('li', {'class':'bottom'}).find('div', {'class':'word_count'}))
- self.story.setMetadata("numWords", re.sub(r'[^0-9]', '', wordCountText))
-
- # Cover image
- storyImage = storyContentBox.find('div', {'class':'story_image'})
- if storyImage:
- coverurl = storyImage.find('a')['href']
- if coverurl.startswith('//'): # fix for img urls missing 'http:'
- coverurl = "http:"+coverurl
- if get_cover:
- self.setCoverImage(self.url,coverurl)
-
- coverSource = storyImage.find('a', {'class':'source'})
- if coverSource:
- self.story.setMetadata('coverSourceUrl', coverSource['href'])
- #There's no text associated with the cover source link, so just
- #reuse the URL. Makes it clear it's an external link leading
- #outside of the fanfic site, at least.
- self.story.setMetadata('coverSource', coverSource['href'])
-
- # fimf has started including extra stuff inside the description div.
- descdivstr = u"%s"%storyContentBox.find("div", {"class":"description"})
- hrstr=u"
"
- descdivstr = u''+descdivstr[descdivstr.index(hrstr)+len(hrstr):]
- self.setDescription(self.url,descdivstr)
-
- # Find the newest and oldest chapter dates
- storyData = storyContentBox.find('div', {'class':'story_data'})
- oldestChapter = None
- newestChapter = None
- self.newestChapterNum = None # save for comparing during update.
- # Scan all chapters to find the oldest and newest, on
- # FiMFiction it's possible for authors to insert new chapters
- # out-of-order or change the dates of earlier ones by editing
- # them--That WILL break epub update.
- for index, chapterDate in enumerate(storyData.find_all('span', {'class':'date'})):
- chapterDate = self.ordinal_date_string_to_date(chapterDate.contents[1])
- if oldestChapter == None or chapterDate < oldestChapter:
- oldestChapter = chapterDate
- if newestChapter == None or chapterDate > newestChapter:
- newestChapter = chapterDate
- self.newestChapterNum = index
-
- if newestChapter is None:
- #this will only be true when updating metadata for stories that have 0 chapters
- #there is a "last modified" date given on the page, extract it and use that.
- moddatetag = storyContentBox.find('span', {'class':'last_modified'})
- if not moddatetag is None:
- newestChapter = self.ordinal_date_string_to_date(moddatetag('span')[1].text)
-
- # Date updated
- self.story.setMetadata("dateUpdated", newestChapter)
-
- # Date published
- # falls back to oldest chapter date for stories that haven't been officially published yet
- pubdatetag = storyContentBox.find('span', {'class':'date_approved'})
- if pubdatetag is None:
- if oldestChapter is None:
- #this will only be true when updating metadata for stories that have 0 chapters
- #and that have never been officially published - a rare occurrence. Fall back to last
- #modified date as the publication date, it's all that we've got.
- self.story.setMetadata("datePublished", newestChapter)
- else:
- self.story.setMetadata("datePublished", oldestChapter)
- else:
- pubDate = self.ordinal_date_string_to_date(pubdatetag('span')[1].text)
- self.story.setMetadata("datePublished", pubDate)
-
- # Characters
- chars = storyContentBox.find("div", {"class":"extra_story_data"})
- for character in chars.find_all("a", {"class":"character_icon"}):
- self.story.addToList("characters", character['title'])
-
- # Likes and dislikes
- storyToolbar = soup.find('div', {'class':'story-toolbar'})
- likes = storyToolbar.find('span', {'class':'likes'})
- if not likes is None:
- self.story.setMetadata("likes", stripHTML(likes))
- dislikes = storyToolbar.find('span', {'class':'dislikes'})
- if not dislikes is None:
- self.story.setMetadata("dislikes", stripHTML(dislikes))
-
- # Highest view for a chapter and total views
- viewSpan = storyToolbar.find('span', {'title':re.compile(r'.*\btotal views\b.*')})
- self.story.setMetadata("views", re.sub(r'[^0-9]', '', stripHTML(viewSpan)))
- self.story.setMetadata("total_views", re.sub(r'[^0-9]', '', viewSpan['title']))
-
- # Comment count
- commentSpan = storyToolbar.find('span', {'title':re.compile(r'.*\bcomments\b.*')})
- self.story.setMetadata("comment_count", re.sub(r'[^0-9]', '', stripHTML(commentSpan)))
-
- # Short description
- descriptionMeta = soup.find('meta', {'property':'og:description'})
- self.story.setMetadata("short_description", stripHTML(descriptionMeta['content']))
-
- #groups
- if soup.find('button', {'id':'button-view-all-groups'}):
- groupResponse = self._fetchUrl("http://www.fimfiction.net/ajax/groups/story_groups_list.php?story=%s" % (self.story.getMetadata("storyId")))
- groupData = json.loads(groupResponse)
- groupList = self.make_soup(groupData["content"])
- else:
- groupList = soup.find('ul', {'id':'story-groups-list'})
-
- if not (groupList == None):
- for groupName in groupList.find_all('a'):
- self.story.addToList("groupsUrl", 'http://'+self.host+groupName["href"])
- self.story.addToList("groups",stripHTML(groupName).replace(',', ';'))
-
- #sequels
- for header in soup.find_all('h1', {'class':'header-stories'}):
- # I don't know why using text=re.compile with find() wouldn't work, but it didn't.
- if header.text.startswith('Sequels'):
- sequelContainer = header.parent
- for sequel in sequelContainer.find_all('a', {'class':'story_link'}):
- self.story.addToList("sequelsUrl", 'http://'+self.host+sequel["href"])
- self.story.addToList("sequels", stripHTML(sequel).replace(',', ';'))
-
- #author last login
- userPageHeader = soup.find('div', {'class':re.compile(r'\buser-page-header\b')})
- if not userPageHeader == None:
- infoContainer = userPageHeader.find('div', {'class':re.compile(r'\binfo-container\b')})
- listItems = infoContainer.find_all('li')
- lastLoginString = stripHTML(listItems[1])
- lastLogin = None
- if "online" in lastLoginString:
- lastLogin = date.today()
- elif "offline" in lastLoginString:
- #this regex extracts the number of weeks and the number of days from the last login string.
- #durations under a day are ignored.
- #group 1 is weeks, group 2 is days
- durationGroups = re.match(r"(?:[^0-9]*(\d+?)w)?[^0-9]*(?:(\d+?)d)?", lastLoginString)
- lastLogin = date.today() - timedelta(days=int(durationGroups.group(2) or 0), weeks=int(durationGroups.group(1) or 0))
- self.story.setMetadata("authorLastLogin", lastLogin)
-
- #The link to the prequel is embedded in the description text, so erring
- #on the side of caution and wrapping this whole thing in a try block.
- #If anything goes wrong this probably wasn't a valid prequel link.
- try:
- description = soup.find('div', {'class':'description'})
- firstHR = description.find("hr")
- nextSib = firstHR.nextSibling
- if "This story is a sequel to" in nextSib.string:
- link = nextSib.nextSibling
- if link.name == "a":
- self.story.setMetadata("prequelUrl", 'http://'+self.host+link["href"])
- self.story.setMetadata("prequel", stripHTML(link))
- except:
- pass
-
- def ordinal_date_string_to_date(self, datestring):
- datestripped=re.sub(r"(\d+)(st|nd|rd|th)", r"\1", datestring.strip())
- return makeDate(datestripped, self.dateformat)
-
- def hookForUpdates(self,chaptercount):
- if self.oldchapters and len(self.oldchapters) > self.newestChapterNum:
- logger.info("Existing epub has %s chapters\nNewest chapter is %s. Discarding old chapters from there on."%(len(self.oldchapters), self.newestChapterNum+1))
- self.oldchapters = self.oldchapters[:self.newestChapterNum]
- return len(self.oldchapters)
-
- def do_fix_blockquotes(self,data):
- if self.getConfig('fix_fimf_blockquotes'):
- #
- #
- # include > in re groups so there's always something in the group.
- data = re.sub(r']*>\s*)
]*>)',r'\s*)',r'',data)
- return data
-
- def getChapterText(self, url):
- logger.debug('Getting chapter text from: %s' % url)
-
- data = self._fetchUrl(url)
-
- soup = self.make_soup(data)
- if not (soup.find('form', {'id' : 'password_form'}) == None):
- if self.password:
- params = {}
- params['password'] = self.password
- data = self._postUrl(url, params)
- else:
- logger.error("Chapter %s needed password but no password was present" % url)
-
- data = self.do_fix_blockquotes(data)
-
- soup = self.make_soup(data).find('div', {'class' : 'chapter_content'})
- if soup == None:
- raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
-
- return self.utf8FromSoup(url,soup)
+# -*- coding: utf-8 -*-
+
+# Copyright 2011 Fanficdownloader team
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import time
+from datetime import date
+from datetime import timedelta
+import logging
+logger = logging.getLogger(__name__)
+import re
+import urllib2
+import cookielib as cl
+import json
+
+from ..htmlcleanup import stripHTML
+from .. import exceptions as exceptions
+
+from base_adapter import BaseSiteAdapter, makeDate
+
+def getClass():
+ return FimFictionNetSiteAdapter
+
+class FimFictionNetSiteAdapter(BaseSiteAdapter):
+
+ def __init__(self, config, url):
+ BaseSiteAdapter.__init__(self, config, url)
+ self.story.setMetadata('siteabbrev','fimficnet')
+ self.story.setMetadata('storyId', self.parsedUrl.path.split('/',)[2])
+ self._setURL("http://"+self.getSiteDomain()+"/story/"+self.story.getMetadata('storyId')+"/")
+ self.is_adult = False
+
+ # The date format will vary from site to site.
+ # http://docs.python.org/library/datetime.html#strftime-strptime-behavior
+ self.dateformat = "%d %b %Y"
+
+ @staticmethod
+ def getSiteDomain():
+ return 'www.fimfiction.net'
+
+ @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 ['www.fimfiction.net','mobile.fimfiction.net', 'www.fimfiction.com', 'mobile.fimfiction.com']
+
+ @classmethod
+ def getSiteExampleURLs(cls):
+ return "http://www.fimfiction.net/story/1234/story-title-here http://www.fimfiction.net/story/1234/ http://www.fimfiction.com/story/1234/1/ http://mobile.fimfiction.net/story/1234/1/story-title-here/chapter-title-here"
+
+ def getSiteURLPattern(self):
+ return r"https?://(www|mobile)\.fimfiction\.(net|com)/story/\d+/?.*"
+
+ def use_pagecache(self):
+ '''
+ adapters that will work with the page cache need to implement
+ this and change it to True.
+ '''
+ return True
+
+ def doExtractChapterUrlsAndMetadata(self,get_cover=True):
+
+ if self.is_adult or self.getConfig("is_adult"):
+ cookie = cl.Cookie(version=0, name='view_mature', value='true',
+ port=None, port_specified=False,
+ domain=self.getSiteDomain(), domain_specified=False, domain_initial_dot=False,
+ path='/story', path_specified=True,
+ secure=False,
+ expires=time.time()+10000,
+ discard=False,
+ comment=None,
+ comment_url=None,
+ rest={'HttpOnly': None},
+ rfc2109=False)
+ self.cookiejar.set_cookie(cookie)
+
+ ##---------------------------------------------------------------------------------------------------
+ ## Get the story's title page. Check if it exists.
+
+ try:
+ # don't use cache if manual is_adult--should only happen
+ # if it's an adult story and they don't have is_adult in ini.
+ data = self.do_fix_blockquotes(self._fetchUrl(self.url,
+ usecache=(not self.is_adult)))
+ soup = self.make_soup(data)
+ except urllib2.HTTPError, e:
+ if e.code == 404:
+ raise exceptions.StoryDoesNotExist(self.url)
+ else:
+ raise e
+
+ if "Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result resource" in data:
+ raise exceptions.StoryDoesNotExist(self.url)
+
+ if "This story has been marked as having adult content. Please click below to confirm you are of legal age to view adult material in your country." in data:
+ raise exceptions.AdultCheckRequired(self.url)
+
+ if self.password:
+ params = {}
+ params['password'] = self.password
+ data = self._postUrl(self.url, params)
+ soup = self.make_soup(data)
+
+ if not (soup.find('form', {'id' : 'password_form'}) == None):
+ if self.getConfig('fail_on_password'):
+ raise exceptions.FailedToDownload("%s requires story password and fail_on_password is true."%self.url)
+ else:
+ raise exceptions.FailedToLogin(self.url,"Story requires individual password",passwdonly=True)
+
+ ##----------------------------------------------------------------------------------------------------
+ ## Extract metadata
+
+ storyContentBox = soup.find('div', {'class':'story_content_box'})
+
+ # Title
+ title = storyContentBox.find('a', {'class':re.compile(r'.*\bstory_name\b.*')})
+ self.story.setMetadata('title',stripHTML(title))
+
+ # Author
+ author = storyContentBox.find('div', {'class':'author'}).find('a')
+ self.story.setMetadata("author", stripHTML(author))
+ #No longer seems to be a way to access Fimfiction's internal author ID
+ self.story.setMetadata("authorId", self.story.getMetadata("author"))
+ self.story.setMetadata("authorUrl", "http://%s/user/%s" % (self.getSiteDomain(), stripHTML(author)))
+
+ #Rating text is replaced with full words for historical compatibility after the site changed
+ #on 2014-10-27
+ rating = stripHTML(storyContentBox.find('a', {'class':re.compile(r'.*\bcontent-rating-.*')}))
+ rating = rating.replace("E", "Everyone").replace("T", "Teen").replace("M", "Mature")
+ self.story.setMetadata("rating", rating)
+
+ # Chapters
+ for chapter in storyContentBox.find_all('a',{'class':'chapter_link'}):
+ self.chapterUrls.append((stripHTML(chapter), 'http://'+self.host+chapter['href']))
+
+ self.story.setMetadata('numChapters',len(self.chapterUrls))
+
+ # Status
+ # In the case of Fimfiction, possible statuses are 'Completed', 'Incomplete', 'On Hiatus' and 'Cancelled'
+ # For the sake of bringing it in line with the other adapters, 'Incomplete' becomes 'In-Progress'
+ # and 'Complete' becomes 'Completed'. 'Cancelled' and 'On Hiatus' are passed through, it's easy now for users
+ # to change/remove if they want with replace_metadata
+ status = stripHTML(storyContentBox.find('span', {'class':re.compile(r'.*\bcompleted-status-.*')}))
+ status = status.replace("Incomplete", "In-Progress").replace("Complete", "Completed")
+ self.story.setMetadata("status", status)
+
+ # Genres and Warnings
+ # warnings were folded into general categories in the 2014-10-27 site update
+ categories = storyContentBox.find_all('a', {'class':re.compile(r'.*\bstory_category\b.*')})
+ for category in categories:
+ category = stripHTML(category)
+ if category == "Gore" or category == "Sex":
+ self.story.addToList('warnings', category)
+ else:
+ self.story.addToList('genre', category)
+
+ # Word count
+ wordCountText = stripHTML(storyContentBox.find('li', {'class':'bottom'}).find('div', {'class':'word_count'}))
+ self.story.setMetadata("numWords", re.sub(r'[^0-9]', '', wordCountText))
+
+ # Cover image
+ storyImage = storyContentBox.find('div', {'class':'story_image'})
+ if storyImage:
+ coverurl = storyImage.find('a')['href']
+ if coverurl.startswith('//'): # fix for img urls missing 'http:'
+ coverurl = "http:"+coverurl
+ if get_cover:
+ self.setCoverImage(self.url,coverurl)
+
+ coverSource = storyImage.find('a', {'class':'source'})
+ if coverSource:
+ self.story.setMetadata('coverSourceUrl', coverSource['href'])
+ #There's no text associated with the cover source link, so just
+ #reuse the URL. Makes it clear it's an external link leading
+ #outside of the fanfic site, at least.
+ self.story.setMetadata('coverSource', coverSource['href'])
+
+ # fimf has started including extra stuff inside the description div.
+ descdivstr = u"%s"%storyContentBox.find("div", {"class":"description"})
+ hrstr=u"
"
+ descdivstr = u''+descdivstr[descdivstr.index(hrstr)+len(hrstr):]
+ self.setDescription(self.url,descdivstr)
+
+ # Find the newest and oldest chapter dates
+ storyData = storyContentBox.find('div', {'class':'story_data'})
+ oldestChapter = None
+ newestChapter = None
+ self.newestChapterNum = None # save for comparing during update.
+ # Scan all chapters to find the oldest and newest, on
+ # FiMFiction it's possible for authors to insert new chapters
+ # out-of-order or change the dates of earlier ones by editing
+ # them--That WILL break epub update.
+ for index, chapterDate in enumerate(storyData.find_all('span', {'class':'date'})):
+ chapterDate = self.ordinal_date_string_to_date(chapterDate.contents[1])
+ if oldestChapter == None or chapterDate < oldestChapter:
+ oldestChapter = chapterDate
+ if newestChapter == None or chapterDate > newestChapter:
+ newestChapter = chapterDate
+ self.newestChapterNum = index
+
+ if newestChapter is None:
+ #this will only be true when updating metadata for stories that have 0 chapters
+ #there is a "last modified" date given on the page, extract it and use that.
+ moddatetag = storyContentBox.find('span', {'class':'last_modified'})
+ if not moddatetag is None:
+ newestChapter = self.ordinal_date_string_to_date(moddatetag('span')[1].text)
+
+ # Date updated
+ self.story.setMetadata("dateUpdated", newestChapter)
+
+ # Date published
+ # falls back to oldest chapter date for stories that haven't been officially published yet
+ pubdatetag = storyContentBox.find('span', {'class':'date_approved'})
+ if pubdatetag is None:
+ if oldestChapter is None:
+ #this will only be true when updating metadata for stories that have 0 chapters
+ #and that have never been officially published - a rare occurrence. Fall back to last
+ #modified date as the publication date, it's all that we've got.
+ self.story.setMetadata("datePublished", newestChapter)
+ else:
+ self.story.setMetadata("datePublished", oldestChapter)
+ else:
+ pubDate = self.ordinal_date_string_to_date(pubdatetag('span')[1].text)
+ self.story.setMetadata("datePublished", pubDate)
+
+ # Characters
+ chars = storyContentBox.find("div", {"class":"extra_story_data"})
+ for character in chars.find_all("a", {"class":"character_icon"}):
+ self.story.addToList("characters", character['title'])
+
+ # Likes and dislikes
+ storyToolbar = soup.find('div', {'class':'story-toolbar'})
+ likes = storyToolbar.find('span', {'class':'likes'})
+ if not likes is None:
+ self.story.setMetadata("likes", stripHTML(likes))
+ dislikes = storyToolbar.find('span', {'class':'dislikes'})
+ if not dislikes is None:
+ self.story.setMetadata("dislikes", stripHTML(dislikes))
+
+ # Highest view for a chapter and total views
+ viewSpan = storyToolbar.find('span', {'title':re.compile(r'.*\btotal views\b.*')})
+ self.story.setMetadata("views", re.sub(r'[^0-9]', '', stripHTML(viewSpan)))
+ self.story.setMetadata("total_views", re.sub(r'[^0-9]', '', viewSpan['title']))
+
+ # Comment count
+ commentSpan = storyToolbar.find('span', {'title':re.compile(r'.*\bcomments\b.*')})
+ self.story.setMetadata("comment_count", re.sub(r'[^0-9]', '', stripHTML(commentSpan)))
+
+ # Short description
+ descriptionMeta = soup.find('meta', {'property':'og:description'})
+ self.story.setMetadata("short_description", stripHTML(descriptionMeta['content']))
+
+ #groups
+ if soup.find('button', {'id':'button-view-all-groups'}):
+ groupResponse = self._fetchUrl("http://www.fimfiction.net/ajax/groups/story_groups_list.php?story=%s" % (self.story.getMetadata("storyId")))
+ groupData = json.loads(groupResponse)
+ groupList = self.make_soup(groupData["content"])
+ else:
+ groupList = soup.find('ul', {'id':'story-groups-list'})
+
+ if not (groupList == None):
+ for groupName in groupList.find_all('a'):
+ self.story.addToList("groupsUrl", 'http://'+self.host+groupName["href"])
+ self.story.addToList("groups",stripHTML(groupName).replace(',', ';'))
+
+ #sequels
+ for header in soup.find_all('h1', {'class':'header-stories'}):
+ # I don't know why using text=re.compile with find() wouldn't work, but it didn't.
+ if header.text.startswith('Sequels'):
+ sequelContainer = header.parent
+ for sequel in sequelContainer.find_all('a', {'class':'story_link'}):
+ self.story.addToList("sequelsUrl", 'http://'+self.host+sequel["href"])
+ self.story.addToList("sequels", stripHTML(sequel).replace(',', ';'))
+
+ #author last login
+ userPageHeader = soup.find('div', {'class':re.compile(r'\buser-page-header\b')})
+ if not userPageHeader == None:
+ infoContainer = userPageHeader.find('div', {'class':re.compile(r'\binfo-container\b')})
+ listItems = infoContainer.find_all('li')
+ lastLoginString = stripHTML(listItems[1])
+ lastLogin = None
+ if "online" in lastLoginString:
+ lastLogin = date.today()
+ elif "offline" in lastLoginString:
+ #this regex extracts the number of weeks and the number of days from the last login string.
+ #durations under a day are ignored.
+ #group 1 is weeks, group 2 is days
+ durationGroups = re.match(r"(?:[^0-9]*(\d+?)w)?[^0-9]*(?:(\d+?)d)?", lastLoginString)
+ lastLogin = date.today() - timedelta(days=int(durationGroups.group(2) or 0), weeks=int(durationGroups.group(1) or 0))
+ self.story.setMetadata("authorLastLogin", lastLogin)
+
+ #The link to the prequel is embedded in the description text, so erring
+ #on the side of caution and wrapping this whole thing in a try block.
+ #If anything goes wrong this probably wasn't a valid prequel link.
+ try:
+ description = soup.find('div', {'class':'description'})
+ firstHR = description.find("hr")
+ nextSib = firstHR.nextSibling
+ if "This story is a sequel to" in nextSib.string:
+ link = nextSib.nextSibling
+ if link.name == "a":
+ self.story.setMetadata("prequelUrl", 'http://'+self.host+link["href"])
+ self.story.setMetadata("prequel", stripHTML(link))
+ except:
+ pass
+
+ def ordinal_date_string_to_date(self, datestring):
+ datestripped=re.sub(r"(\d+)(st|nd|rd|th)", r"\1", datestring.strip())
+ return makeDate(datestripped, self.dateformat)
+
+ def hookForUpdates(self,chaptercount):
+ if self.oldchapters and len(self.oldchapters) > self.newestChapterNum:
+ logger.info("Existing epub has %s chapters\nNewest chapter is %s. Discarding old chapters from there on."%(len(self.oldchapters), self.newestChapterNum+1))
+ self.oldchapters = self.oldchapters[:self.newestChapterNum]
+ return len(self.oldchapters)
+
+ def do_fix_blockquotes(self,data):
+ if self.getConfig('fix_fimf_blockquotes'):
+ #
+ #
+ # include > in re groups so there's always something in the group.
+ data = re.sub(r']*>\s*)
]*>)',r'\s*)',r'',data)
+ return data
+
+ def getChapterText(self, url):
+ logger.debug('Getting chapter text from: %s' % url)
+
+ data = self._fetchUrl(url)
+
+ soup = self.make_soup(data)
+ if not (soup.find('form', {'id' : 'password_form'}) == None):
+ if self.password:
+ params = {}
+ params['password'] = self.password
+ data = self._postUrl(url, params)
+ else:
+ logger.error("Chapter %s needed password but no password was present" % url)
+
+ data = self.do_fix_blockquotes(data)
+
+ soup = self.make_soup(data).find('div', {'class' : 'chapter_content'})
+ if soup == None:
+ raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
+
+ return self.utf8FromSoup(url,soup)
diff --git a/fff_internals/adapters/adapter_finestoriescom.py b/fanficfare/adapters/adapter_finestoriescom.py
similarity index 97%
rename from fff_internals/adapters/adapter_finestoriescom.py
rename to fanficfare/adapters/adapter_finestoriescom.py
index 6c99f9d..1902494 100644
--- a/fff_internals/adapters/adapter_finestoriescom.py
+++ b/fanficfare/adapters/adapter_finestoriescom.py
@@ -1,288 +1,288 @@
-# -*- coding: utf-8 -*-
-
-# Copyright 2013 Fanficdownloader team
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-import time
-import logging
-logger = logging.getLogger(__name__)
-import re
-import urllib2
-
-from .. import BeautifulSoup as bs
-from ..htmlcleanup import stripHTML
-from .. import exceptions as exceptions
-
-from base_adapter import BaseSiteAdapter, makeDate
-
-def getClass():
- return FineStoriesComAdapter
-
-# Class name has to be unique. Our convention is camel case the
-# sitename with Adapter at the end. www is skipped.
-class FineStoriesComAdapter(BaseSiteAdapter):
-
- def __init__(self, config, url):
- BaseSiteAdapter.__init__(self, config, url)
-
- self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
- self.password = ""
- self.is_adult=False
-
- # get storyId from url
- self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2].split(':')[0])
- if 'storyInfo' in self.story.getMetadata('storyId'):
- self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
-
- # normalized story URL.
- self._setURL('http://' + self.getSiteDomain() + '/s/storyInfo.php?id='+self.story.getMetadata('storyId'))
-
- # Each adapter needs to have a unique site abbreviation.
- self.story.setMetadata('siteabbrev','fnst')
-
- # The date format will vary from site to site.
- # http://docs.python.org/library/datetime.html#strftime-strptime-behavior
- self.dateformat = "%Y-%m-%d"
-
- @staticmethod # must be @staticmethod, don't remove it.
- def getSiteDomain():
- # The site domain. Does have www here, if it uses it.
- return 'finestories.com'
-
- @classmethod
- def getSiteExampleURLs(cls):
- return "http://"+cls.getSiteDomain()+"/s/1234 http://"+cls.getSiteDomain()+"/s/1234:4010 http://"+cls.getSiteDomain()+"/library/storyInfo.php?id=1234"
-
- def getSiteURLPattern(self):
- return re.escape("http://"+self.getSiteDomain())+r"/(s|library)?/(storyInfo.php\?id=)?\d+(:\d+)?(;\d+)?$"
-
- ## Login seems to be reasonably standard across eFiction sites.
- def needToLoginCheck(self, data):
- if 'Free Registration' in data \
- or "Invalid Password!" in data \
- or "Invalid User Name!" in data:
- return True
- else:
- return False
-
- def performLogin(self, url):
- params = {}
-
- if self.password:
- params['theusername'] = self.username
- params['thepassword'] = self.password
- else:
- params['theusername'] = self.getConfig("username")
- params['thepassword'] = self.getConfig("password")
- params['rememberMe'] = '1'
- params['page'] = 'http://'+self.getSiteDomain()+'/'
- params['submit'] = 'Login'
-
- loginUrl = 'http://' + self.getSiteDomain() + '/login.php'
- logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
- params['theusername']))
-
- d = self._fetchUrl(loginUrl, params)
-
- if "My Account" not in d : #Member Account
- logger.info("Failed to login to URL %s as %s" % (loginUrl,
- params['theusername']))
- raise exceptions.FailedToLogin(url,params['theusername'])
- return False
- else:
- return True
-
- ## Getting the chapter list and the meta data, plus 'is adult' checking.
- def extractChapterUrlsAndMetadata(self):
-
- # index=1 makes sure we see the story chapter index. Some
- # sites skip that for one-chapter stories.
- url = self.url
- logger.debug("URL: "+url)
-
- 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)
-
- if "Access denied. This story has not been validated by the adminstrators of this site." in data:
- raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
-
- # use BeautifulSoup HTML parser to make everything easier to find.
- soup = bs.BeautifulSoup(data)
- # print data
-
- # Now go hunting for all the meta data and the chapter list.
-
- ## Title
- a = soup.find('a', href=re.compile(r'/s/'+self.story.getMetadata('storyId')+"$"))
- self.story.setMetadata('title',stripHTML(a))
-
- # Find authorid and URL from... author url.
- a = soup.find('a', href=re.compile(r"/a/\w+"))
- self.story.setMetadata('authorId',a['href'].split('/')[2])
- self.story.setMetadata('authorUrl','http://'+self.host+a['href'])
- self.story.setMetadata('author',a.text)
-
- # Find the chapters:
- chapters = soup.findAll('a', href=re.compile(r'/s/'+self.story.getMetadata('storyId')+":\d+$"))
- if len(chapters) != 0:
- for chapter in chapters:
- # just in case there's tags, like in chapter titles.
- self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+chapter['href']))
- else:
- self.chapterUrls.append((self.story.getMetadata('title'),'http://'+self.host+'/s/'+self.story.getMetadata('storyId')))
-
- self.story.setMetadata('numChapters',len(self.chapterUrls))
-
- # surprisingly, the detailed page does not give enough details, so go to author's page
-
- skip=0
- i=0
- while i == 0:
- asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')+"&skip="+str(skip)))
-
- a = asoup.findAll('td', {'class' : 'lc2'})
- for lc2 in a:
- if lc2.find('a')['href'] == '/s/'+self.story.getMetadata('storyId'):
- i=1
- break
- if a[len(a)-1] == lc2:
- skip=skip+10
-
- for cat in lc2.findAll('div', {'class' : 'typediv'}):
- self.story.addToList('category',cat.text)
-
- self.story.setMetadata('numWords', lc2.findNext('td', {'class' : 'num'}).text)
-
- lc4 = lc2.findNext('td', {'class' : 'lc4'})
-
-
- try:
- a = lc4.find('a', href=re.compile(r"/library/show_series.php\?id=\d+"))
- i = a.parent.text.split('(')[1].split(')')[0]
- self.setSeries(a.text, i)
- self.story.setMetadata('seriesUrl','http://'+self.host+a['href'])
- except:
- pass
- try:
- a = lc4.find('a', href=re.compile(r"/library/universe.php\?id=\d+"))
- self.story.addToList("category",a.text)
- except:
- pass
-
- for a in lc4.findAll('span', {'class' : 'help'}):
- a.extract()
-
- self.setDescription('http://'+self.host+'/s/'+self.story.getMetadata('storyId'),lc4.text.split('[More Info')[0])
-
- for b in lc4.findAll('b'):
- label = b.text
- value = b.nextSibling
-
- if 'For Age' in label:
- self.story.setMetadata('rating', value)
-
- if 'Tags' in label:
- for genre in value.split(', '):
- self.story.addToList('genre',genre)
-
- if 'Posted' in label:
- self.story.setMetadata('datePublished', makeDate(stripHTML(value.split('/ (')[0]), self.dateformat))
- self.story.setMetadata('dateUpdated', makeDate(stripHTML(value.split('/ (')[0]), self.dateformat))
-
- if 'Concluded' in label:
- self.story.setMetadata('dateUpdated', makeDate(stripHTML(value.split('/ (')[0]), self.dateformat))
-
- if 'Updated' in label:
- self.story.setMetadata('dateUpdated', makeDate(stripHTML(value.split('/ (')[0]), self.dateformat))
-
- status = lc4.find('span', {'class' : 'ab'})
- if status != None:
- self.story.setMetadata('status', 'In-Progress')
- if "Last Activity" in status.text:
- self.story.setMetadata('dateUpdated', makeDate(status.text.split('Activity: ')[1].split(')')[0], self.dateformat))
- else:
- self.story.setMetadata('status', 'Completed')
-
-
- # grab the text for an individual chapter.
- def getChapterText(self, url):
-
- logger.debug('Getting chapter text from: %s' % url)
-
- soup = bs.BeautifulSoup(self._fetchUrl(url),
- selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
-
- div = soup.find('div', {'id' : 'story'})
-
- # some big chapters are split over several pages
- pager = div.find('span', {'class' : 'pager'})
- if pager != None:
- urls=pager.findAll('a')
- urls=urls[:len(urls)-1]
-
-
- for ur in urls:
- soup = bs.BeautifulSoup(self._fetchUrl("http://"+self.getSiteDomain()+ur['href']),
- selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
-
- div1 = soup.find('div', {'id' : 'story'})
-
- # appending next section
- last=div.findAll('p')
- next=div1.find('span', {'class' : 'conTag'}).nextSibling
-
- last[len(last)-1]=last[len(last)-1].append(next)
- div.append(div1)
-
- # removing all the left-over stuff
- for a in div.findAll('span'):
- a.extract()
-
- for a in div.findAll('h1'):
- a.extract()
- for a in div.findAll('h2'):
- a.extract()
- for a in div.findAll('h3'):
- a.extract()
- for a in div.findAll('h4'):
- a.extract()
- for a in div.findAll('br'):
- a.extract()
- for a in div.findAll('div', {'class' : 'date'}):
- a.extract()
-
- a = div.find('form')
- if a != None:
- b = a.nextSibling
- while b != None:
- a.extract()
- a=b
- b=b.nextSibling
-
-
- if None == div:
- raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
-
- return self.utf8FromSoup(url,div)
+# -*- coding: utf-8 -*-
+
+# Copyright 2013 Fanficdownloader team
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import time
+import logging
+logger = logging.getLogger(__name__)
+import re
+import urllib2
+
+from .. import BeautifulSoup as bs
+from ..htmlcleanup import stripHTML
+from .. import exceptions as exceptions
+
+from base_adapter import BaseSiteAdapter, makeDate
+
+def getClass():
+ return FineStoriesComAdapter
+
+# Class name has to be unique. Our convention is camel case the
+# sitename with Adapter at the end. www is skipped.
+class FineStoriesComAdapter(BaseSiteAdapter):
+
+ def __init__(self, config, url):
+ BaseSiteAdapter.__init__(self, config, url)
+
+ self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
+ self.password = ""
+ self.is_adult=False
+
+ # get storyId from url
+ self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2].split(':')[0])
+ if 'storyInfo' in self.story.getMetadata('storyId'):
+ self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
+
+ # normalized story URL.
+ self._setURL('http://' + self.getSiteDomain() + '/s/storyInfo.php?id='+self.story.getMetadata('storyId'))
+
+ # Each adapter needs to have a unique site abbreviation.
+ self.story.setMetadata('siteabbrev','fnst')
+
+ # The date format will vary from site to site.
+ # http://docs.python.org/library/datetime.html#strftime-strptime-behavior
+ self.dateformat = "%Y-%m-%d"
+
+ @staticmethod # must be @staticmethod, don't remove it.
+ def getSiteDomain():
+ # The site domain. Does have www here, if it uses it.
+ return 'finestories.com'
+
+ @classmethod
+ def getSiteExampleURLs(cls):
+ return "http://"+cls.getSiteDomain()+"/s/1234 http://"+cls.getSiteDomain()+"/s/1234:4010 http://"+cls.getSiteDomain()+"/library/storyInfo.php?id=1234"
+
+ def getSiteURLPattern(self):
+ return re.escape("http://"+self.getSiteDomain())+r"/(s|library)?/(storyInfo.php\?id=)?\d+(:\d+)?(;\d+)?$"
+
+ ## Login seems to be reasonably standard across eFiction sites.
+ def needToLoginCheck(self, data):
+ if 'Free Registration' in data \
+ or "Invalid Password!" in data \
+ or "Invalid User Name!" in data:
+ return True
+ else:
+ return False
+
+ def performLogin(self, url):
+ params = {}
+
+ if self.password:
+ params['theusername'] = self.username
+ params['thepassword'] = self.password
+ else:
+ params['theusername'] = self.getConfig("username")
+ params['thepassword'] = self.getConfig("password")
+ params['rememberMe'] = '1'
+ params['page'] = 'http://'+self.getSiteDomain()+'/'
+ params['submit'] = 'Login'
+
+ loginUrl = 'http://' + self.getSiteDomain() + '/login.php'
+ logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
+ params['theusername']))
+
+ d = self._fetchUrl(loginUrl, params)
+
+ if "My Account" not in d : #Member Account
+ logger.info("Failed to login to URL %s as %s" % (loginUrl,
+ params['theusername']))
+ raise exceptions.FailedToLogin(url,params['theusername'])
+ return False
+ else:
+ return True
+
+ ## Getting the chapter list and the meta data, plus 'is adult' checking.
+ def extractChapterUrlsAndMetadata(self):
+
+ # index=1 makes sure we see the story chapter index. Some
+ # sites skip that for one-chapter stories.
+ url = self.url
+ logger.debug("URL: "+url)
+
+ 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)
+
+ if "Access denied. This story has not been validated by the adminstrators of this site." in data:
+ raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
+
+ # use BeautifulSoup HTML parser to make everything easier to find.
+ soup = bs.BeautifulSoup(data)
+ # print data
+
+ # Now go hunting for all the meta data and the chapter list.
+
+ ## Title
+ a = soup.find('a', href=re.compile(r'/s/'+self.story.getMetadata('storyId')+"$"))
+ self.story.setMetadata('title',stripHTML(a))
+
+ # Find authorid and URL from... author url.
+ a = soup.find('a', href=re.compile(r"/a/\w+"))
+ self.story.setMetadata('authorId',a['href'].split('/')[2])
+ self.story.setMetadata('authorUrl','http://'+self.host+a['href'])
+ self.story.setMetadata('author',a.text)
+
+ # Find the chapters:
+ chapters = soup.findAll('a', href=re.compile(r'/s/'+self.story.getMetadata('storyId')+":\d+$"))
+ if len(chapters) != 0:
+ for chapter in chapters:
+ # just in case there's tags, like in chapter titles.
+ self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+chapter['href']))
+ else:
+ self.chapterUrls.append((self.story.getMetadata('title'),'http://'+self.host+'/s/'+self.story.getMetadata('storyId')))
+
+ self.story.setMetadata('numChapters',len(self.chapterUrls))
+
+ # surprisingly, the detailed page does not give enough details, so go to author's page
+
+ skip=0
+ i=0
+ while i == 0:
+ asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')+"&skip="+str(skip)))
+
+ a = asoup.findAll('td', {'class' : 'lc2'})
+ for lc2 in a:
+ if lc2.find('a')['href'] == '/s/'+self.story.getMetadata('storyId'):
+ i=1
+ break
+ if a[len(a)-1] == lc2:
+ skip=skip+10
+
+ for cat in lc2.findAll('div', {'class' : 'typediv'}):
+ self.story.addToList('category',cat.text)
+
+ self.story.setMetadata('numWords', lc2.findNext('td', {'class' : 'num'}).text)
+
+ lc4 = lc2.findNext('td', {'class' : 'lc4'})
+
+
+ try:
+ a = lc4.find('a', href=re.compile(r"/library/show_series.php\?id=\d+"))
+ i = a.parent.text.split('(')[1].split(')')[0]
+ self.setSeries(a.text, i)
+ self.story.setMetadata('seriesUrl','http://'+self.host+a['href'])
+ except:
+ pass
+ try:
+ a = lc4.find('a', href=re.compile(r"/library/universe.php\?id=\d+"))
+ self.story.addToList("category",a.text)
+ except:
+ pass
+
+ for a in lc4.findAll('span', {'class' : 'help'}):
+ a.extract()
+
+ self.setDescription('http://'+self.host+'/s/'+self.story.getMetadata('storyId'),lc4.text.split('[More Info')[0])
+
+ for b in lc4.findAll('b'):
+ label = b.text
+ value = b.nextSibling
+
+ if 'For Age' in label:
+ self.story.setMetadata('rating', value)
+
+ if 'Tags' in label:
+ for genre in value.split(', '):
+ self.story.addToList('genre',genre)
+
+ if 'Posted' in label:
+ self.story.setMetadata('datePublished', makeDate(stripHTML(value.split('/ (')[0]), self.dateformat))
+ self.story.setMetadata('dateUpdated', makeDate(stripHTML(value.split('/ (')[0]), self.dateformat))
+
+ if 'Concluded' in label:
+ self.story.setMetadata('dateUpdated', makeDate(stripHTML(value.split('/ (')[0]), self.dateformat))
+
+ if 'Updated' in label:
+ self.story.setMetadata('dateUpdated', makeDate(stripHTML(value.split('/ (')[0]), self.dateformat))
+
+ status = lc4.find('span', {'class' : 'ab'})
+ if status != None:
+ self.story.setMetadata('status', 'In-Progress')
+ if "Last Activity" in status.text:
+ self.story.setMetadata('dateUpdated', makeDate(status.text.split('Activity: ')[1].split(')')[0], self.dateformat))
+ else:
+ self.story.setMetadata('status', 'Completed')
+
+
+ # grab the text for an individual chapter.
+ def getChapterText(self, url):
+
+ logger.debug('Getting chapter text from: %s' % url)
+
+ soup = bs.BeautifulSoup(self._fetchUrl(url),
+ selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
+
+ div = soup.find('div', {'id' : 'story'})
+
+ # some big chapters are split over several pages
+ pager = div.find('span', {'class' : 'pager'})
+ if pager != None:
+ urls=pager.findAll('a')
+ urls=urls[:len(urls)-1]
+
+
+ for ur in urls:
+ soup = bs.BeautifulSoup(self._fetchUrl("http://"+self.getSiteDomain()+ur['href']),
+ selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
+
+ div1 = soup.find('div', {'id' : 'story'})
+
+ # appending next section
+ last=div.findAll('p')
+ next=div1.find('span', {'class' : 'conTag'}).nextSibling
+
+ last[len(last)-1]=last[len(last)-1].append(next)
+ div.append(div1)
+
+ # removing all the left-over stuff
+ for a in div.findAll('span'):
+ a.extract()
+
+ for a in div.findAll('h1'):
+ a.extract()
+ for a in div.findAll('h2'):
+ a.extract()
+ for a in div.findAll('h3'):
+ a.extract()
+ for a in div.findAll('h4'):
+ a.extract()
+ for a in div.findAll('br'):
+ a.extract()
+ for a in div.findAll('div', {'class' : 'date'}):
+ a.extract()
+
+ a = div.find('form')
+ if a != None:
+ b = a.nextSibling
+ while b != None:
+ a.extract()
+ a=b
+ b=b.nextSibling
+
+
+ if None == div:
+ raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
+
+ return self.utf8FromSoup(url,div)
diff --git a/fff_internals/adapters/adapter_grangerenchantedcom.py b/fanficfare/adapters/adapter_grangerenchantedcom.py
similarity index 97%
rename from fff_internals/adapters/adapter_grangerenchantedcom.py
rename to fanficfare/adapters/adapter_grangerenchantedcom.py
index 831f08f..c73cceb 100644
--- a/fff_internals/adapters/adapter_grangerenchantedcom.py
+++ b/fanficfare/adapters/adapter_grangerenchantedcom.py
@@ -1,311 +1,311 @@
-# -*- coding: utf-8 -*-
-
-# Copyright 2012 Fanficdownloader 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 .. import BeautifulSoup as bs
-from ..htmlcleanup import stripHTML
-from .. import exceptions as exceptions
-
-from base_adapter import BaseSiteAdapter, makeDate
-
-def getClass():
- return GrangerEnchantedCom
-
-# Class name has to be unique. Our convention is camel case the
-# sitename with Adapter at the end. www is skipped.
-class GrangerEnchantedCom(BaseSiteAdapter):
-
- def __init__(self, config, url):
- BaseSiteAdapter.__init__(self, config, url)
-
- self.decode = ["Windows-1252",
- "utf8"] # 1252 is a superset of iso-8859-1.
- # Most sites that claim to be
- # iso-8859-1 (and some that claim to be
- # utf8) are really windows-1252.
- self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
- self.password = ""
- self.is_adult=False
-
- # get storyId from url--url validation guarantees query is only sid=1234
- self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
-
- self.section=self.parsedUrl.path.split('/',)[1]
-
- # normalized story URL.
- if "malfoymanor" in self.parsedUrl.netloc:
- self._setURL('http://malfoymanor.' + self.getSiteDomain() + '/themanor/viewstory.php?sid='+self.story.getMetadata('storyId'))
- self.story.addToList("category","The Manor")
- else:
- self._setURL('http://' + self.getSiteDomain() + '/enchant/viewstory.php?sid='+self.story.getMetadata('storyId'))
-
- # Each adapter needs to have a unique site abbreviation.
- self.story.setMetadata('siteabbrev','gech')
-
- # The date format will vary from site to site.
- # http://docs.python.org/library/datetime.html#strftime-strptime-behavior
- self.dateformat = "%d/%b/%Y"
-
- @staticmethod # must be @staticmethod, don't remove it.
- def getSiteDomain():
- # The site domain. Does have www here, if it uses it.
- return 'grangerenchanted.com'
-
- @classmethod
- def getAcceptDomains(cls):
- return ['grangerenchanted.com','malfoymanor.grangerenchanted.com']
-
- @classmethod
- def getSiteExampleURLs(cls):
- return "http://grangerenchanted.com/enchant/viewstory.php?sid=1234 http://malfoymanor.grangerenchanted.com/themanor/viewstory.php?sid=1234"
-
- def getSiteURLPattern(self):
- return r"http://(malfoymanor.)?grangerenchanted.com/(enchant|themanor)?/viewstory.php\?sid=\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'
-
- if "enchant" in self.section:
- loginUrl = 'http://grangerenchanted.com/enchant/user.php?action=login'
- else:
- loginUrl = 'http://malfoymanor.grangerenchanted.com/themanor/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 = "&ageconsent=ok&warning=1"
- else:
- addurl=""
-
- # index=1 makes sure we see the story chapter index. Some
- # sites skip that for one-chapter stories.
- url = self.url+addurl
- logger.debug("URL: "+url)
-
- try:
- data = self._fetchUrl(url)
- except urllib2.HTTPError, e:
- if e.code == 404:
- raise exceptions.StoryDoesNotExist(self.url)
- else:
- raise e
-
- if self.needToLoginCheck(data):
- # need to log in for this one.
- self.performLogin(url)
- data = self._fetchUrl(url)
-
- m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data)
- if m != None:
- if self.is_adult or self.getConfig("is_adult"):
- # We tried the default and still got a warning, so
- # let's pull the warning number from the 'continue'
- # link and reload data.
- addurl = m.group(1)
- # correct stupid & error in url.
- addurl = addurl.replace("&","&")
- url = self.url+'&index=1'+addurl
- logger.debug("URL 2nd try: "+url)
-
- try:
- data = self._fetchUrl(url)
- except urllib2.HTTPError, e:
- if e.code == 404:
- raise exceptions.StoryDoesNotExist(self.url)
- else:
- raise e
- else:
- raise exceptions.AdultCheckRequired(self.url)
-
- if "Access denied. This story has not been validated by the adminstrators of this site." in data:
- raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
-
- # use BeautifulSoup HTML parser to make everything easier to find.
- soup = bs.BeautifulSoup(data)
- # print data
-
- # Now go hunting for all the meta data and the chapter list.
-
- ## Title
- a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
- self.story.setMetadata('title',stripHTML(a))
-
- # Find authorid and URL from... author url.
- a = soup.find('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+'/'+self.section+'/'+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 not defaultGetattr(value,'class') == 'label':
- svalue += str(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 'Read' in label:
- self.story.setMetadata('read', 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=4'))
- for genre in genres:
- self.story.addToList('genre',genre.string)
-
- if 'Warnings' in label:
- warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2'))
- for warning in warnings:
- self.story.addToList('warnings',warning.string)
-
- if 'Completed' in label:
- if 'Yes' in value:
- self.story.setMetadata('status', 'Completed')
- else:
- self.story.setMetadata('status', 'In-Progress')
-
- if 'Published' in label:
- self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
-
- if 'Updated' in label:
- self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
-
- try:
- # Find Series name from series URL.
- a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+"))
- series_name = a.string
- series_url = 'http://'+self.host+'/'+self.section+'/'+a['href']
-
- # use BeautifulSoup HTML parser to make everything easier to find.
- seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url))
- # can't use ^viewstory...$ in case of higher rated stories with javascript href.
- storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+'))
- i=1
- for a in storyas:
- # skip 'report this' and 'TOC' links
- if 'contact.php' not in a['href'] and 'index' not in a['href']:
- if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
- self.setSeries(series_name, i)
- self.story.setMetadata('seriesUrl',series_url)
- break
- i+=1
- except:
- # I find it hard to care if the series parsing fails
- pass
-
- try:
- self.story.setMetadata('reviews',
- stripHTML(soup.find('div',{'id':'sort'}).
- findAll('a', href=re.compile(r'^reviews.php'))[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 = bs.BeautifulSoup(self._fetchUrl(url),
- selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
-
- div = soup.find('div', {'id' : 'story1'})
-
- if None == div:
- raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
-
- return self.utf8FromSoup(url,div)
+# -*- coding: utf-8 -*-
+
+# Copyright 2012 Fanficdownloader 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 .. import BeautifulSoup as bs
+from ..htmlcleanup import stripHTML
+from .. import exceptions as exceptions
+
+from base_adapter import BaseSiteAdapter, makeDate
+
+def getClass():
+ return GrangerEnchantedCom
+
+# Class name has to be unique. Our convention is camel case the
+# sitename with Adapter at the end. www is skipped.
+class GrangerEnchantedCom(BaseSiteAdapter):
+
+ def __init__(self, config, url):
+ BaseSiteAdapter.__init__(self, config, url)
+
+ self.decode = ["Windows-1252",
+ "utf8"] # 1252 is a superset of iso-8859-1.
+ # Most sites that claim to be
+ # iso-8859-1 (and some that claim to be
+ # utf8) are really windows-1252.
+ self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
+ self.password = ""
+ self.is_adult=False
+
+ # get storyId from url--url validation guarantees query is only sid=1234
+ self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
+
+ self.section=self.parsedUrl.path.split('/',)[1]
+
+ # normalized story URL.
+ if "malfoymanor" in self.parsedUrl.netloc:
+ self._setURL('http://malfoymanor.' + self.getSiteDomain() + '/themanor/viewstory.php?sid='+self.story.getMetadata('storyId'))
+ self.story.addToList("category","The Manor")
+ else:
+ self._setURL('http://' + self.getSiteDomain() + '/enchant/viewstory.php?sid='+self.story.getMetadata('storyId'))
+
+ # Each adapter needs to have a unique site abbreviation.
+ self.story.setMetadata('siteabbrev','gech')
+
+ # The date format will vary from site to site.
+ # http://docs.python.org/library/datetime.html#strftime-strptime-behavior
+ self.dateformat = "%d/%b/%Y"
+
+ @staticmethod # must be @staticmethod, don't remove it.
+ def getSiteDomain():
+ # The site domain. Does have www here, if it uses it.
+ return 'grangerenchanted.com'
+
+ @classmethod
+ def getAcceptDomains(cls):
+ return ['grangerenchanted.com','malfoymanor.grangerenchanted.com']
+
+ @classmethod
+ def getSiteExampleURLs(cls):
+ return "http://grangerenchanted.com/enchant/viewstory.php?sid=1234 http://malfoymanor.grangerenchanted.com/themanor/viewstory.php?sid=1234"
+
+ def getSiteURLPattern(self):
+ return r"http://(malfoymanor.)?grangerenchanted.com/(enchant|themanor)?/viewstory.php\?sid=\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'
+
+ if "enchant" in self.section:
+ loginUrl = 'http://grangerenchanted.com/enchant/user.php?action=login'
+ else:
+ loginUrl = 'http://malfoymanor.grangerenchanted.com/themanor/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 = "&ageconsent=ok&warning=1"
+ else:
+ addurl=""
+
+ # index=1 makes sure we see the story chapter index. Some
+ # sites skip that for one-chapter stories.
+ url = self.url+addurl
+ logger.debug("URL: "+url)
+
+ try:
+ data = self._fetchUrl(url)
+ except urllib2.HTTPError, e:
+ if e.code == 404:
+ raise exceptions.StoryDoesNotExist(self.url)
+ else:
+ raise e
+
+ if self.needToLoginCheck(data):
+ # need to log in for this one.
+ self.performLogin(url)
+ data = self._fetchUrl(url)
+
+ m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data)
+ if m != None:
+ if self.is_adult or self.getConfig("is_adult"):
+ # We tried the default and still got a warning, so
+ # let's pull the warning number from the 'continue'
+ # link and reload data.
+ addurl = m.group(1)
+ # correct stupid & error in url.
+ addurl = addurl.replace("&","&")
+ url = self.url+'&index=1'+addurl
+ logger.debug("URL 2nd try: "+url)
+
+ try:
+ data = self._fetchUrl(url)
+ except urllib2.HTTPError, e:
+ if e.code == 404:
+ raise exceptions.StoryDoesNotExist(self.url)
+ else:
+ raise e
+ else:
+ raise exceptions.AdultCheckRequired(self.url)
+
+ if "Access denied. This story has not been validated by the adminstrators of this site." in data:
+ raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
+
+ # use BeautifulSoup HTML parser to make everything easier to find.
+ soup = bs.BeautifulSoup(data)
+ # print data
+
+ # Now go hunting for all the meta data and the chapter list.
+
+ ## Title
+ a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
+ self.story.setMetadata('title',stripHTML(a))
+
+ # Find authorid and URL from... author url.
+ a = soup.find('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+'/'+self.section+'/'+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 not defaultGetattr(value,'class') == 'label':
+ svalue += str(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 'Read' in label:
+ self.story.setMetadata('read', 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=4'))
+ for genre in genres:
+ self.story.addToList('genre',genre.string)
+
+ if 'Warnings' in label:
+ warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2'))
+ for warning in warnings:
+ self.story.addToList('warnings',warning.string)
+
+ if 'Completed' in label:
+ if 'Yes' in value:
+ self.story.setMetadata('status', 'Completed')
+ else:
+ self.story.setMetadata('status', 'In-Progress')
+
+ if 'Published' in label:
+ self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
+
+ if 'Updated' in label:
+ self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
+
+ try:
+ # Find Series name from series URL.
+ a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+"))
+ series_name = a.string
+ series_url = 'http://'+self.host+'/'+self.section+'/'+a['href']
+
+ # use BeautifulSoup HTML parser to make everything easier to find.
+ seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url))
+ # can't use ^viewstory...$ in case of higher rated stories with javascript href.
+ storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+'))
+ i=1
+ for a in storyas:
+ # skip 'report this' and 'TOC' links
+ if 'contact.php' not in a['href'] and 'index' not in a['href']:
+ if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')):
+ self.setSeries(series_name, i)
+ self.story.setMetadata('seriesUrl',series_url)
+ break
+ i+=1
+ except:
+ # I find it hard to care if the series parsing fails
+ pass
+
+ try:
+ self.story.setMetadata('reviews',
+ stripHTML(soup.find('div',{'id':'sort'}).
+ findAll('a', href=re.compile(r'^reviews.php'))[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 = bs.BeautifulSoup(self._fetchUrl(url),
+ selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
+
+ div = soup.find('div', {'id' : 'story1'})
+
+ if None == div:
+ raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
+
+ return self.utf8FromSoup(url,div)
diff --git a/fff_internals/adapters/adapter_harrypotterfanfictioncom.py b/fanficfare/adapters/adapter_harrypotterfanfictioncom.py
similarity index 97%
rename from fff_internals/adapters/adapter_harrypotterfanfictioncom.py
rename to fanficfare/adapters/adapter_harrypotterfanfictioncom.py
index bddf4b6..1453bd3 100644
--- a/fff_internals/adapters/adapter_harrypotterfanfictioncom.py
+++ b/fanficfare/adapters/adapter_harrypotterfanfictioncom.py
@@ -1,203 +1,203 @@
-# -*- coding: utf-8 -*-
-
-# Copyright 2011 Fanficdownloader 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 urllib
-import urllib2
-
-from .. import BeautifulSoup as bs
-from ..htmlcleanup import stripHTML
-from .. import exceptions as exceptions
-
-from base_adapter import BaseSiteAdapter, makeDate
-
-class HarryPotterFanFictionComSiteAdapter(BaseSiteAdapter):
-
- def __init__(self, config, url):
- BaseSiteAdapter.__init__(self, config, url)
- self.story.setMetadata('siteabbrev','hp')
- 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.is_adult=False
-
- # get storyId from url--url validation guarantees query is only psid=1234
- self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
-
-
- # normalized story URL.
- self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?psid='+self.story.getMetadata('storyId'))
-
-
- @staticmethod
- def getSiteDomain():
- return 'www.harrypotterfanfiction.com'
-
- @classmethod
- def getAcceptDomains(cls):
- return ['www.harrypotterfanfiction.com','harrypotterfanfiction.com']
-
- @classmethod
- def getSiteExampleURLs(cls):
- return "http://www.harrypotterfanfiction.com/viewstory.php?psid=1234"
-
- def getSiteURLPattern(self):
- return re.escape("http://")+r"(www\.)?"+re.escape("harrypotterfanfiction.com/viewstory.php?psid=")+r"\d+$"
-
- 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 extractChapterUrlsAndMetadata(self):
-
- url = self.url+'&index=1'
- 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 "Access denied. This story has not been validated by the adminstrators of this site." in data:
- raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
-
- # use BeautifulSoup HTML parser to make everything easier to find.
- soup = bs.BeautifulSoup(data)
-
- ## Title
- a = soup.find('a', href=re.compile(r'\?psid='+self.story.getMetadata('storyId')))
- self.story.setMetadata('title',stripHTML(a))
- ## javascript:if (confirm('Please note. This story may contain adult themes. By clicking here you are stating that you are over 17. Click cancel if you do not meet this requirement.')) location = '?psid=290995'
- if "This story may contain adult themes." in a['href'] and not (self.is_adult or self.getConfig("is_adult")):
- raise exceptions.AdultCheckRequired(self.url)
-
-
- # Find authorid and URL from... author url.
- a = soup.find('a', href=re.compile(r"viewuser.php\?showuid=\d+"))
- self.story.setMetadata('authorId',a['href'].split('=')[1])
- self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href'])
- self.story.setMetadata('author',a.string)
-
- ## hpcom doesn't give us total words--but it does give
- ## us words/chapter. I'd rather add than fetch and
- ## parse another page.
- words=0
- for tr in soup.find('table',{'class':'text'}).findAll('tr'):
- tdstr = tr.findAll('td')[2].string
- if tdstr and tdstr.isdigit():
- words+=int(tdstr)
- self.story.setMetadata('numWords',str(words))
-
- # Find the chapters:
- tablelist = soup.find('table',{'class':'text'})
- for chapter in tablelist.findAll('a', href=re.compile(r'\?chapterid=\d+')):
- #javascript:if (confirm('Please note. This story may contain adult themes. By clicking here you are stating that you are over 17. Click cancel if you do not meet this requirement.')) location = '?chapterid=433441&i=1'
- # just in case there's tags, like in chapter titles.
- chpt=re.sub(r'^.*?(\?chapterid=\d+).*?',r'\1',chapter['href'])
- self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/viewstory.php'+chpt))
-
- self.story.setMetadata('numChapters',len(self.chapterUrls))
-
- ## Finding the metadata is a bit of a pain. Desc is the only thing this color.
- desctable= soup.find('table',{'bgcolor':'#f0e8e8'})
- self.setDescription(url,desctable)
- #self.story.setMetadata('description',stripHTML(desctable))
-
- ## Finding the metadata is a bit of a pain. Most of the meta
- ## data is in a center.table without a bgcolor.
- #for center in soup.findAll('center'):
- table = soup.find('table',{'class':'storymaininfo'})
- if table:
- metastr = stripHTML(str(table)).replace('\n',' ').replace('\t',' ')
- # Rating: 12+ Story Reviews: 3
- # Chapters: 3
- # Characters: Andromeda, Ted, Bellatrix, R. Lestrange, Lucius, Narcissa, OC
- # Genre(s): Fluff, Romance, Young Adult Era: OtherPairings: Other Pairing, Lucius/Narcissa
- # Status: Completed
- # First Published: 2010.09.02
- # Last Published Chapter: 2010.09.28
- # Last Updated: 2010.09.28
- # Favorite Story Of: 1 users
- # Warnings: Scenes of a Mild Sexual Nature
-
- m = re.match(r".*?Status: Completed.*?",metastr)
- if m:
- self.story.setMetadata('status','Completed')
- else:
- self.story.setMetadata('status','In-Progress')
-
- m = re.match(r".*?Rating: (.+?) Story Reviews.*?",metastr)
- if m:
- self.story.setMetadata('rating', m.group(1))
-
- m = re.match(r".*?Genre\(s\): (.+?) Era.*?",metastr)
- if m:
- for g in m.group(1).split(','):
- self.story.addToList('genre',g)
-
- m = re.match(r".*?Characters: (.+?) Genre.*?",metastr)
- if m:
- for g in m.group(1).split(','):
- self.story.addToList('characters',g)
-
- m = re.match(r".*?Warnings: (.+).*?",metastr)
- if m:
- for w in m.group(1).split(','):
- if w != 'Now Warnings':
- self.story.addToList('warnings',w)
-
- m = re.match(r".*?First Published: ([0-9\.]+).*?",metastr)
- if m:
- self.story.setMetadata('datePublished',makeDate(m.group(1), "%Y.%m.%d"))
-
- # Updated can have more than one space after it.
- m = re.match(r".*?Last Updated: ([0-9\.]+).*?",metastr)
- if m:
- self.story.setMetadata('dateUpdated',makeDate(m.group(1), "%Y.%m.%d"))
-
- def getChapterText(self, url):
-
- logger.debug('Getting chapter text from: %s' % url)
-
- ## most adapters use BeautifulStoneSoup here, but non-Stone
- ## allows nested div tags.
- soup = bs.BeautifulSoup(self._fetchUrl(url),
- selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
-
- div = soup.find('div', {'id' : 'fluidtext'})
-
- if None == div:
- raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
-
- return self.utf8FromSoup(url,div)
-
-def getClass():
- return HarryPotterFanFictionComSiteAdapter
-
+# -*- coding: utf-8 -*-
+
+# Copyright 2011 Fanficdownloader 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 urllib
+import urllib2
+
+from .. import BeautifulSoup as bs
+from ..htmlcleanup import stripHTML
+from .. import exceptions as exceptions
+
+from base_adapter import BaseSiteAdapter, makeDate
+
+class HarryPotterFanFictionComSiteAdapter(BaseSiteAdapter):
+
+ def __init__(self, config, url):
+ BaseSiteAdapter.__init__(self, config, url)
+ self.story.setMetadata('siteabbrev','hp')
+ 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.is_adult=False
+
+ # get storyId from url--url validation guarantees query is only psid=1234
+ self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
+
+
+ # normalized story URL.
+ self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?psid='+self.story.getMetadata('storyId'))
+
+
+ @staticmethod
+ def getSiteDomain():
+ return 'www.harrypotterfanfiction.com'
+
+ @classmethod
+ def getAcceptDomains(cls):
+ return ['www.harrypotterfanfiction.com','harrypotterfanfiction.com']
+
+ @classmethod
+ def getSiteExampleURLs(cls):
+ return "http://www.harrypotterfanfiction.com/viewstory.php?psid=1234"
+
+ def getSiteURLPattern(self):
+ return re.escape("http://")+r"(www\.)?"+re.escape("harrypotterfanfiction.com/viewstory.php?psid=")+r"\d+$"
+
+ 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 extractChapterUrlsAndMetadata(self):
+
+ url = self.url+'&index=1'
+ 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 "Access denied. This story has not been validated by the adminstrators of this site." in data:
+ raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
+
+ # use BeautifulSoup HTML parser to make everything easier to find.
+ soup = bs.BeautifulSoup(data)
+
+ ## Title
+ a = soup.find('a', href=re.compile(r'\?psid='+self.story.getMetadata('storyId')))
+ self.story.setMetadata('title',stripHTML(a))
+ ## javascript:if (confirm('Please note. This story may contain adult themes. By clicking here you are stating that you are over 17. Click cancel if you do not meet this requirement.')) location = '?psid=290995'
+ if "This story may contain adult themes." in a['href'] and not (self.is_adult or self.getConfig("is_adult")):
+ raise exceptions.AdultCheckRequired(self.url)
+
+
+ # Find authorid and URL from... author url.
+ a = soup.find('a', href=re.compile(r"viewuser.php\?showuid=\d+"))
+ self.story.setMetadata('authorId',a['href'].split('=')[1])
+ self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href'])
+ self.story.setMetadata('author',a.string)
+
+ ## hpcom doesn't give us total words--but it does give
+ ## us words/chapter. I'd rather add than fetch and
+ ## parse another page.
+ words=0
+ for tr in soup.find('table',{'class':'text'}).findAll('tr'):
+ tdstr = tr.findAll('td')[2].string
+ if tdstr and tdstr.isdigit():
+ words+=int(tdstr)
+ self.story.setMetadata('numWords',str(words))
+
+ # Find the chapters:
+ tablelist = soup.find('table',{'class':'text'})
+ for chapter in tablelist.findAll('a', href=re.compile(r'\?chapterid=\d+')):
+ #javascript:if (confirm('Please note. This story may contain adult themes. By clicking here you are stating that you are over 17. Click cancel if you do not meet this requirement.')) location = '?chapterid=433441&i=1'
+ # just in case there's tags, like in chapter titles.
+ chpt=re.sub(r'^.*?(\?chapterid=\d+).*?',r'\1',chapter['href'])
+ self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/viewstory.php'+chpt))
+
+ self.story.setMetadata('numChapters',len(self.chapterUrls))
+
+ ## Finding the metadata is a bit of a pain. Desc is the only thing this color.
+ desctable= soup.find('table',{'bgcolor':'#f0e8e8'})
+ self.setDescription(url,desctable)
+ #self.story.setMetadata('description',stripHTML(desctable))
+
+ ## Finding the metadata is a bit of a pain. Most of the meta
+ ## data is in a center.table without a bgcolor.
+ #for center in soup.findAll('center'):
+ table = soup.find('table',{'class':'storymaininfo'})
+ if table:
+ metastr = stripHTML(str(table)).replace('\n',' ').replace('\t',' ')
+ # Rating: 12+ Story Reviews: 3
+ # Chapters: 3
+ # Characters: Andromeda, Ted, Bellatrix, R. Lestrange, Lucius, Narcissa, OC
+ # Genre(s): Fluff, Romance, Young Adult Era: OtherPairings: Other Pairing, Lucius/Narcissa
+ # Status: Completed
+ # First Published: 2010.09.02
+ # Last Published Chapter: 2010.09.28
+ # Last Updated: 2010.09.28
+ # Favorite Story Of: 1 users
+ # Warnings: Scenes of a Mild Sexual Nature
+
+ m = re.match(r".*?Status: Completed.*?",metastr)
+ if m:
+ self.story.setMetadata('status','Completed')
+ else:
+ self.story.setMetadata('status','In-Progress')
+
+ m = re.match(r".*?Rating: (.+?) Story Reviews.*?",metastr)
+ if m:
+ self.story.setMetadata('rating', m.group(1))
+
+ m = re.match(r".*?Genre\(s\): (.+?) Era.*?",metastr)
+ if m:
+ for g in m.group(1).split(','):
+ self.story.addToList('genre',g)
+
+ m = re.match(r".*?Characters: (.+?) Genre.*?",metastr)
+ if m:
+ for g in m.group(1).split(','):
+ self.story.addToList('characters',g)
+
+ m = re.match(r".*?Warnings: (.+).*?",metastr)
+ if m:
+ for w in m.group(1).split(','):
+ if w != 'Now Warnings':
+ self.story.addToList('warnings',w)
+
+ m = re.match(r".*?First Published: ([0-9\.]+).*?",metastr)
+ if m:
+ self.story.setMetadata('datePublished',makeDate(m.group(1), "%Y.%m.%d"))
+
+ # Updated can have more than one space after it.
+ m = re.match(r".*?Last Updated: ([0-9\.]+).*?",metastr)
+ if m:
+ self.story.setMetadata('dateUpdated',makeDate(m.group(1), "%Y.%m.%d"))
+
+ def getChapterText(self, url):
+
+ logger.debug('Getting chapter text from: %s' % url)
+
+ ## most adapters use BeautifulStoneSoup here, but non-Stone
+ ## allows nested div tags.
+ soup = bs.BeautifulSoup(self._fetchUrl(url),
+ selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
+
+ div = soup.find('div', {'id' : 'fluidtext'})
+
+ if None == div:
+ raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
+
+ return self.utf8FromSoup(url,div)
+
+def getClass():
+ return HarryPotterFanFictionComSiteAdapter
+
diff --git a/fff_internals/adapters/adapter_hennethannunnet.py b/fanficfare/adapters/adapter_hennethannunnet.py
similarity index 97%
rename from fff_internals/adapters/adapter_hennethannunnet.py
rename to fanficfare/adapters/adapter_hennethannunnet.py
index 4a5391d..bdcc441 100644
--- a/fff_internals/adapters/adapter_hennethannunnet.py
+++ b/fanficfare/adapters/adapter_hennethannunnet.py
@@ -1,172 +1,172 @@
-# -*- coding: utf-8 -*-
-
-# Copyright 2013 Fanficdownloader team
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-import time
-import logging
-logger = logging.getLogger(__name__)
-import re
-import urllib2
-
-from .. import BeautifulSoup as bs
-from ..htmlcleanup import stripHTML
-from .. import exceptions as exceptions
-
-from base_adapter import BaseSiteAdapter, makeDate
-
-def getClass():
- return HennethAnnunNetAdapter
-
-# Class name has to be unique. Our convention is camel case the
-# sitename with Adapter at the end. www is skipped.
-class HennethAnnunNetAdapter(BaseSiteAdapter):
-
- def __init__(self, config, url):
- BaseSiteAdapter.__init__(self, config, url)
-
- self.decode = ["Windows-1252",
- "utf8"] # 1252 is a superset of iso-8859-1.
- # Most sites that claim to be
- # iso-8859-1 (and some that claim to be
- # utf8) are really windows-1252.
- self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
- self.password = ""
- self.is_adult=False
-
- # get storyId from url--url validation guarantees query is only sid=1234
- self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
-
-
- # normalized story URL.
- self._setURL('http://' + self.getSiteDomain() + '/stories/chapter.cfm?stid='+self.story.getMetadata('storyId'))
-
- # Each adapter needs to have a unique site abbreviation.
- self.story.setMetadata('siteabbrev','htan')
-
- # The date format will vary from site to site.
- # http://docs.python.org/library/datetime.html#strftime-strptime-behavior
- self.dateformat = "%m/%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.henneth-annun.net'
-
- @classmethod
- def getSiteExampleURLs(cls):
- return "http://"+cls.getSiteDomain()+"/stories/chapter.cfm?stid=1234"
-
- def getSiteURLPattern(self):
- return "http://"+self.getSiteDomain()+"/stories/chapter(_view)?.cfm\?stid="+r"\d+$"
-
- ## Getting the chapter list and the meta data, plus 'is adult' checking.
- def extractChapterUrlsAndMetadata(self):
-
-
- # index=1 makes sure we see the story chapter index. Some
- # sites skip that for one-chapter stories.
- url = self.url
- logger.debug("URL: "+url)
-
- try:
- data = self._fetchUrl(url)
- except urllib2.HTTPError, e:
- if e.code == 404:
- raise exceptions.StoryDoesNotExist(self.url)
- else:
- raise e
-
-
- if "We're sorry. This story is not available." in data:
- raise exceptions.FailedToDownload(self.getSiteDomain() +" says: This story is not available.")
-
- # use BeautifulSoup HTML parser to make everything easier to find.
- soup = bs.BeautifulSoup(data)
- # print data
-
- # Now go hunting for all the meta data and the chapter list.
-
- ## Title
- a = soup.find('h2', {'id':'page_heading'})
- self.story.setMetadata('title',stripHTML(a))
-
- # Find the chapters: chapter_view.cfm?stid=6663&spordinal=1"
- for chapter in soup.findAll('a', href=re.compile(r'chapter_view.cfm\?stid='+self.story.getMetadata('storyId')+"&spordinal=\d+$")):
- # just in case there's tags, like in chapter titles.
- self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/stories/'+chapter['href']))
-
- self.story.setMetadata('numChapters',len(self.chapterUrls))
-
- self.story.setMetadata('numWords', soup.find('tr', {'class':'foot'}).findAll('td')[1].text)
-
- self.setDescription(url,soup.find('div', {'id':'summary'}))
-
- # Rated: NC-17
etc
- info = soup.find('div', {'id':'storyinformation'})
- labels=info.findAll('b')
- for labelspan in labels:
- value = labelspan.nextSibling
- label = labelspan.string
-
- if 'Completion' in label:
- if 'Complete' in value.string:
- self.story.setMetadata('status', 'Completed')
- else:
- self.story.setMetadata('status', 'In-Progress')
-
- if 'Rating' in label:
- self.story.setMetadata('rating', value.string)
-
- if 'Era:' in label:
- self.story.addToList('category',value.string)
-
- if 'Genre' in label:
- self.story.addToList('genre',value.string)
-
- labels=info.findAll('strong')
- for labelspan in labels:
- value = labelspan.nextSibling
- label = labelspan.string
-
- if 'Author' in label:
- value=value.nextSibling
- self.story.setMetadata('authorId',value['href'].split('=')[1])
- self.story.setMetadata('authorUrl','http://'+self.host+'/'+value['href'])
- self.story.setMetadata('author',value.string)
-
- if 'Post' in label:
- self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
-
- if 'Updated:' in label:
- self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
-
- for char in soup.findAll('a', href=re.compile(r"/resources/bios_view.cfm\?scid=\d+")):
- self.story.addToList('characters',stripHTML(char))
-
- # grab the text for an individual chapter.
- def getChapterText(self, url):
-
- logger.debug('Getting chapter text from: %s' % url)
-
- soup = bs.BeautifulStoneSoup(self._fetchUrl(url),
- selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
-
- div = soup.find('div', {'class' : 'block chapter'})
-
- if None == div:
- raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
-
- return self.utf8FromSoup(url,div)
+# -*- coding: utf-8 -*-
+
+# Copyright 2013 Fanficdownloader team
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import time
+import logging
+logger = logging.getLogger(__name__)
+import re
+import urllib2
+
+from .. import BeautifulSoup as bs
+from ..htmlcleanup import stripHTML
+from .. import exceptions as exceptions
+
+from base_adapter import BaseSiteAdapter, makeDate
+
+def getClass():
+ return HennethAnnunNetAdapter
+
+# Class name has to be unique. Our convention is camel case the
+# sitename with Adapter at the end. www is skipped.
+class HennethAnnunNetAdapter(BaseSiteAdapter):
+
+ def __init__(self, config, url):
+ BaseSiteAdapter.__init__(self, config, url)
+
+ self.decode = ["Windows-1252",
+ "utf8"] # 1252 is a superset of iso-8859-1.
+ # Most sites that claim to be
+ # iso-8859-1 (and some that claim to be
+ # utf8) are really windows-1252.
+ self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
+ self.password = ""
+ self.is_adult=False
+
+ # get storyId from url--url validation guarantees query is only sid=1234
+ self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
+
+
+ # normalized story URL.
+ self._setURL('http://' + self.getSiteDomain() + '/stories/chapter.cfm?stid='+self.story.getMetadata('storyId'))
+
+ # Each adapter needs to have a unique site abbreviation.
+ self.story.setMetadata('siteabbrev','htan')
+
+ # The date format will vary from site to site.
+ # http://docs.python.org/library/datetime.html#strftime-strptime-behavior
+ self.dateformat = "%m/%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.henneth-annun.net'
+
+ @classmethod
+ def getSiteExampleURLs(cls):
+ return "http://"+cls.getSiteDomain()+"/stories/chapter.cfm?stid=1234"
+
+ def getSiteURLPattern(self):
+ return "http://"+self.getSiteDomain()+"/stories/chapter(_view)?.cfm\?stid="+r"\d+$"
+
+ ## Getting the chapter list and the meta data, plus 'is adult' checking.
+ def extractChapterUrlsAndMetadata(self):
+
+
+ # index=1 makes sure we see the story chapter index. Some
+ # sites skip that for one-chapter stories.
+ url = self.url
+ logger.debug("URL: "+url)
+
+ try:
+ data = self._fetchUrl(url)
+ except urllib2.HTTPError, e:
+ if e.code == 404:
+ raise exceptions.StoryDoesNotExist(self.url)
+ else:
+ raise e
+
+
+ if "We're sorry. This story is not available." in data:
+ raise exceptions.FailedToDownload(self.getSiteDomain() +" says: This story is not available.")
+
+ # use BeautifulSoup HTML parser to make everything easier to find.
+ soup = bs.BeautifulSoup(data)
+ # print data
+
+ # Now go hunting for all the meta data and the chapter list.
+
+ ## Title
+ a = soup.find('h2', {'id':'page_heading'})
+ self.story.setMetadata('title',stripHTML(a))
+
+ # Find the chapters: chapter_view.cfm?stid=6663&spordinal=1"
+ for chapter in soup.findAll('a', href=re.compile(r'chapter_view.cfm\?stid='+self.story.getMetadata('storyId')+"&spordinal=\d+$")):
+ # just in case there's tags, like in chapter titles.
+ self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/stories/'+chapter['href']))
+
+ self.story.setMetadata('numChapters',len(self.chapterUrls))
+
+ self.story.setMetadata('numWords', soup.find('tr', {'class':'foot'}).findAll('td')[1].text)
+
+ self.setDescription(url,soup.find('div', {'id':'summary'}))
+
+ # Rated: NC-17
etc
+ info = soup.find('div', {'id':'storyinformation'})
+ labels=info.findAll('b')
+ for labelspan in labels:
+ value = labelspan.nextSibling
+ label = labelspan.string
+
+ if 'Completion' in label:
+ if 'Complete' in value.string:
+ self.story.setMetadata('status', 'Completed')
+ else:
+ self.story.setMetadata('status', 'In-Progress')
+
+ if 'Rating' in label:
+ self.story.setMetadata('rating', value.string)
+
+ if 'Era:' in label:
+ self.story.addToList('category',value.string)
+
+ if 'Genre' in label:
+ self.story.addToList('genre',value.string)
+
+ labels=info.findAll('strong')
+ for labelspan in labels:
+ value = labelspan.nextSibling
+ label = labelspan.string
+
+ if 'Author' in label:
+ value=value.nextSibling
+ self.story.setMetadata('authorId',value['href'].split('=')[1])
+ self.story.setMetadata('authorUrl','http://'+self.host+'/'+value['href'])
+ self.story.setMetadata('author',value.string)
+
+ if 'Post' in label:
+ self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
+
+ if 'Updated:' in label:
+ self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
+
+ for char in soup.findAll('a', href=re.compile(r"/resources/bios_view.cfm\?scid=\d+")):
+ self.story.addToList('characters',stripHTML(char))
+
+ # grab the text for an individual chapter.
+ def getChapterText(self, url):
+
+ logger.debug('Getting chapter text from: %s' % url)
+
+ soup = bs.BeautifulStoneSoup(self._fetchUrl(url),
+ selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
+
+ div = soup.find('div', {'class' : 'block chapter'})
+
+ if None == div:
+ raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
+
+ return self.utf8FromSoup(url,div)
diff --git a/fff_internals/adapters/adapter_hlfictionnet.py b/fanficfare/adapters/adapter_hlfictionnet.py
similarity index 97%
rename from fff_internals/adapters/adapter_hlfictionnet.py
rename to fanficfare/adapters/adapter_hlfictionnet.py
index 96deb95..beabb75 100644
--- a/fff_internals/adapters/adapter_hlfictionnet.py
+++ b/fanficfare/adapters/adapter_hlfictionnet.py
@@ -1,232 +1,232 @@
-# -*- coding: utf-8 -*-
-
-# Copyright 2012 Fanficdownloader 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 .. import BeautifulSoup as bs
-from ..htmlcleanup import stripHTML
-from .. import exceptions as exceptions
-
-from base_adapter import BaseSiteAdapter, makeDate
-
-def getClass():
- return HLFictionNetAdapter
-
-# Class name has to be unique. Our convention is camel case the
-# sitename with Adapter at the end. www is skipped.
-class HLFictionNetAdapter(BaseSiteAdapter):
-
- def __init__(self, config, url):
- BaseSiteAdapter.__init__(self, config, url)
-
- self.decode = ["Windows-1252",
- "utf8"] # 1252 is a superset of iso-8859-1.
- # Most sites that claim to be
- # iso-8859-1 (and some that claim to be
- # utf8) are really windows-1252.
- self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
- self.password = ""
- self.is_adult=False
-
- # get storyId from url--url validation guarantees query is only sid=1234
- self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
-
-
- # normalized story URL.
- self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
-
- # Each adapter needs to have a unique site abbreviation.
- self.story.setMetadata('siteabbrev','hlf')
-
- # The date format will vary from site to site.
- # http://docs.python.org/library/datetime.html#strftime-strptime-behavior
- self.dateformat = "%m/%d/%y"
-
- @staticmethod # must be @staticmethod, don't remove it.
- def getSiteDomain():
- # The site domain. Does have www here, if it uses it.
- return 'hlfiction.net'
-
- @classmethod
- def getSiteExampleURLs(cls):
- return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234"
-
- def getSiteURLPattern(self):
- return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$"
-
- ## Getting the chapter list and the meta data, plus 'is adult' checking.
- def extractChapterUrlsAndMetadata(self):
-
- # index=1 makes sure we see the story chapter index. Some
- # sites skip that for one-chapter stories.
- url = self.url
- logger.debug("URL: "+url)
-
- try:
- data = self._fetchUrl(url)
- except urllib2.HTTPError, e:
- if e.code == 404:
- raise exceptions.StoryDoesNotExist(self.url)
- else:
- raise e
-
- if "Access denied. This story has not been validated by the adminstrators of this site." in data:
- raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
-
- # use BeautifulSoup HTML parser to make everything easier to find.
- soup = bs.BeautifulSoup(data)
- # print data
-
- # Now go hunting for all the meta data and the chapter list.
-
- ## Title and author
- a = soup.find('div', {'id' : 'pagetitle'})
-
- aut = a.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
- self.story.setMetadata('authorId',aut['href'].split('=')[1])
- self.story.setMetadata('authorUrl','http://'+self.host+'/'+aut['href'])
- self.story.setMetadata('author',aut.string)
- aut.extract()
-
- self.story.setMetadata('title',stripHTML(a)[:(len(a.string)-3)])
-
- # Find the chapters:
- chapters=soup.find('select')
- if chapters != None:
- for chapter in chapters.findAll('option'):
- # just in case there's tags, like in chapter titles.
- self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/viewstory.php?sid='+self.story.getMetadata('storyId')+'&chapter='+chapter['value']))
- else:
- self.chapterUrls.append((self.story.getMetadata('title'),url))
-
- self.story.setMetadata('numChapters',len(self.chapterUrls))
-
- asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')))
-
- for list in asoup.findAll('div', {'class' : re.compile('listbox\s+')}):
- a = list.find('a')
- if ('viewstory.php?sid='+self.story.getMetadata('storyId')) in a['href']:
- break
-
- # 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 = list.findAll('span', {'class' : 'classification'})
- for labelspan in labels:
- label = labelspan.string
- value = labelspan.nextSibling
-
- if 'Summary' in label:
- ## Everything until the next span class='label'
- svalue = ""
- while not defaultGetattr(value,'class') == 'classification':
- svalue += str(value)
- value = value.nextSibling
- self.setDescription(url,svalue)
- #self.story.setMetadata('description',stripHTML(svalue))
-
- if 'Rated' in label:
- self.story.setMetadata('rating', value[:len(value)-2])
-
- if 'Word count' in label:
- self.story.setMetadata('numWords', value)
-
- if 'Categories' in label:
- cats = labelspan.parent.findAll('a',href=re.compile(r'categories.php\?catid=\d+'))
- for cat in cats:
- self.story.addToList('category',cat.string)
-
- if 'Characters' in label:
- for char in value.string.split(', '):
- if not 'None' in char:
- self.story.addToList('characters',char)
-
- if 'Genre' in label:
- for genre in value.string.split(', '):
- if not 'None' in genre:
- self.story.addToList('genre',genre)
-
- if 'Warnings' in label:
- for warning in value.string.split(', '):
- if not 'None' in warning:
- self.story.addToList('warnings',warning)
-
- 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 = list.find('a', href=re.compile(r"series.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 = bs.BeautifulSoup(self._fetchUrl(series_url))
- # can't use ^viewstory...$ in case of higher rated stories with javascript href.
- storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+'))
- i=1
- for a in storyas:
- # skip 'report this' and 'TOC' links
- if 'contact.php' not in a['href'] and 'index' not in a['href']:
- if ('viewstory.php?sid='+self.story.getMetadata('storyId')) in a['href']:
- 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 = bs.BeautifulStoneSoup(self._fetchUrl(url),
- selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
-
- 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)
+# -*- coding: utf-8 -*-
+
+# Copyright 2012 Fanficdownloader 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 .. import BeautifulSoup as bs
+from ..htmlcleanup import stripHTML
+from .. import exceptions as exceptions
+
+from base_adapter import BaseSiteAdapter, makeDate
+
+def getClass():
+ return HLFictionNetAdapter
+
+# Class name has to be unique. Our convention is camel case the
+# sitename with Adapter at the end. www is skipped.
+class HLFictionNetAdapter(BaseSiteAdapter):
+
+ def __init__(self, config, url):
+ BaseSiteAdapter.__init__(self, config, url)
+
+ self.decode = ["Windows-1252",
+ "utf8"] # 1252 is a superset of iso-8859-1.
+ # Most sites that claim to be
+ # iso-8859-1 (and some that claim to be
+ # utf8) are really windows-1252.
+ self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
+ self.password = ""
+ self.is_adult=False
+
+ # get storyId from url--url validation guarantees query is only sid=1234
+ self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
+
+
+ # normalized story URL.
+ self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
+
+ # Each adapter needs to have a unique site abbreviation.
+ self.story.setMetadata('siteabbrev','hlf')
+
+ # The date format will vary from site to site.
+ # http://docs.python.org/library/datetime.html#strftime-strptime-behavior
+ self.dateformat = "%m/%d/%y"
+
+ @staticmethod # must be @staticmethod, don't remove it.
+ def getSiteDomain():
+ # The site domain. Does have www here, if it uses it.
+ return 'hlfiction.net'
+
+ @classmethod
+ def getSiteExampleURLs(cls):
+ return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234"
+
+ def getSiteURLPattern(self):
+ return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$"
+
+ ## Getting the chapter list and the meta data, plus 'is adult' checking.
+ def extractChapterUrlsAndMetadata(self):
+
+ # index=1 makes sure we see the story chapter index. Some
+ # sites skip that for one-chapter stories.
+ url = self.url
+ logger.debug("URL: "+url)
+
+ try:
+ data = self._fetchUrl(url)
+ except urllib2.HTTPError, e:
+ if e.code == 404:
+ raise exceptions.StoryDoesNotExist(self.url)
+ else:
+ raise e
+
+ if "Access denied. This story has not been validated by the adminstrators of this site." in data:
+ raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
+
+ # use BeautifulSoup HTML parser to make everything easier to find.
+ soup = bs.BeautifulSoup(data)
+ # print data
+
+ # Now go hunting for all the meta data and the chapter list.
+
+ ## Title and author
+ a = soup.find('div', {'id' : 'pagetitle'})
+
+ aut = a.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
+ self.story.setMetadata('authorId',aut['href'].split('=')[1])
+ self.story.setMetadata('authorUrl','http://'+self.host+'/'+aut['href'])
+ self.story.setMetadata('author',aut.string)
+ aut.extract()
+
+ self.story.setMetadata('title',stripHTML(a)[:(len(a.string)-3)])
+
+ # Find the chapters:
+ chapters=soup.find('select')
+ if chapters != None:
+ for chapter in chapters.findAll('option'):
+ # just in case there's tags, like in chapter titles.
+ self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/viewstory.php?sid='+self.story.getMetadata('storyId')+'&chapter='+chapter['value']))
+ else:
+ self.chapterUrls.append((self.story.getMetadata('title'),url))
+
+ self.story.setMetadata('numChapters',len(self.chapterUrls))
+
+ asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')))
+
+ for list in asoup.findAll('div', {'class' : re.compile('listbox\s+')}):
+ a = list.find('a')
+ if ('viewstory.php?sid='+self.story.getMetadata('storyId')) in a['href']:
+ break
+
+ # 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 = list.findAll('span', {'class' : 'classification'})
+ for labelspan in labels:
+ label = labelspan.string
+ value = labelspan.nextSibling
+
+ if 'Summary' in label:
+ ## Everything until the next span class='label'
+ svalue = ""
+ while not defaultGetattr(value,'class') == 'classification':
+ svalue += str(value)
+ value = value.nextSibling
+ self.setDescription(url,svalue)
+ #self.story.setMetadata('description',stripHTML(svalue))
+
+ if 'Rated' in label:
+ self.story.setMetadata('rating', value[:len(value)-2])
+
+ if 'Word count' in label:
+ self.story.setMetadata('numWords', value)
+
+ if 'Categories' in label:
+ cats = labelspan.parent.findAll('a',href=re.compile(r'categories.php\?catid=\d+'))
+ for cat in cats:
+ self.story.addToList('category',cat.string)
+
+ if 'Characters' in label:
+ for char in value.string.split(', '):
+ if not 'None' in char:
+ self.story.addToList('characters',char)
+
+ if 'Genre' in label:
+ for genre in value.string.split(', '):
+ if not 'None' in genre:
+ self.story.addToList('genre',genre)
+
+ if 'Warnings' in label:
+ for warning in value.string.split(', '):
+ if not 'None' in warning:
+ self.story.addToList('warnings',warning)
+
+ 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 = list.find('a', href=re.compile(r"series.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 = bs.BeautifulSoup(self._fetchUrl(series_url))
+ # can't use ^viewstory...$ in case of higher rated stories with javascript href.
+ storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+'))
+ i=1
+ for a in storyas:
+ # skip 'report this' and 'TOC' links
+ if 'contact.php' not in a['href'] and 'index' not in a['href']:
+ if ('viewstory.php?sid='+self.story.getMetadata('storyId')) in a['href']:
+ 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 = bs.BeautifulStoneSoup(self._fetchUrl(url),
+ selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
+
+ 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/fff_internals/adapters/adapter_hpfandomnet.py b/fanficfare/adapters/adapter_hpfandomnet.py
similarity index 97%
rename from fff_internals/adapters/adapter_hpfandomnet.py
rename to fanficfare/adapters/adapter_hpfandomnet.py
index 16a4cfc..77274d6 100644
--- a/fff_internals/adapters/adapter_hpfandomnet.py
+++ b/fanficfare/adapters/adapter_hpfandomnet.py
@@ -1,233 +1,233 @@
-# -*- coding: utf-8 -*-
-
-# Copyright 2011 Fanficdownloader 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 .. import BeautifulSoup as bs
-from ..htmlcleanup import stripHTML
-from .. import exceptions as exceptions
-
-from base_adapter import BaseSiteAdapter, makeDate
-
-# This function is called by the downloader in all adapter_*.py files
-# in this dir to register the adapter class. So it needs to be
-# updated to reflect the class below it. That, plus getSiteDomain()
-# take care of 'Registering'.
-def getClass():
- return HPFandomNetAdapterAdapter # XXX
-
-# Class name has to be unique. Our convention is camel case the
-# sitename with Adapter at the end. www is skipped.
-class HPFandomNetAdapterAdapter(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 /eff part. Replace all to remove it usually.
- self._setURL('http://' + self.getSiteDomain() + '/eff/viewstory.php?sid='+self.story.getMetadata('storyId'))
-
- # Each adapter needs to have a unique site abbreviation.
- self.story.setMetadata('siteabbrev','hpfdm') # XXX
-
- # The date format will vary from site to site.
- # http://docs.python.org/library/datetime.html#strftime-strptime-behavior
- self.dateformat = "%Y.%m.%d" # XXX
-
- @staticmethod # must be @staticmethod, don't remove it.
- def getSiteDomain():
- # The site domain. Does have www here, if it uses it.
- return 'www.hpfandom.net' # XXX
-
- @classmethod
- def getSiteExampleURLs(cls):
- return "http://"+cls.getSiteDomain()+"/eff/viewstory.php?sid=1234"
-
- def getSiteURLPattern(self):
- return re.escape("http://"+self.getSiteDomain()+"/eff/viewstory.php?sid=")+r"\d+$"
-
- ## Getting the chapter list and the meta data, plus 'is adult' checking.
- def extractChapterUrlsAndMetadata(self):
-
- url = self.url
- 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
-
- # use BeautifulSoup HTML parser to make everything easier to find.
- soup = bs.BeautifulSoup(data)
- # print data
-
- # Now go hunting for all the meta data and the chapter list.
-
- # Find authorid and URL from... author url.
- a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
- self.story.setMetadata('authorId',a['href'].split('=')[1])
- self.story.setMetadata('authorUrl','http://'+self.host+'/eff/'+a['href'])
- self.story.setMetadata('author',a.string)
-
- ## Going to get the rest from the author page.
- authdata = self._fetchUrl(self.story.getMetadata('authorUrl'))
- # fix a typo in the site HTML so I can find the Characters list.
- authdata = authdata.replace('',' ')
-
- # hpfandom.net only seems to indicate adult-only by javascript on the story/chapter links.
- if "javascript:if (confirm('Slash/het fiction which incorporates sexual situations to a somewhat graphic degree and some violence. ')) location = 'viewstory.php?sid=%s'"%self.story.getMetadata('storyId') in authdata \
- and not (self.is_adult or self.getConfig("is_adult")):
- raise exceptions.AdultCheckRequired(self.url)
-
- authsoup = bs.BeautifulSoup(authdata)
-
- reviewsa = authsoup.find('a', href="reviews.php?sid="+self.story.getMetadata('storyId')+"&a=")
- #