Compare commits

...
84 changed files with 2219 additions and 390 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# ffd-retief-hrd fanfictiondownloader
application: fanfictiondownloader
version: 4-4-23
version: 4-4-25
runtime: python27
api_version: 1
threadsafe: true
+1 -1
View File
@@ -27,7 +27,7 @@ class FanFictionDownLoaderBase(InterfaceActionBase):
description = 'UI plugin to download FanFiction stories from various sites.'
supported_platforms = ['windows', 'osx', 'linux']
author = 'Jim Miller'
version = (1, 6, 7)
version = (1, 6, 9)
minimum_calibre_version = (0, 8, 57)
#: This field defines the GUI plugin class that contains all the code
+1 -1
View File
@@ -4,7 +4,7 @@
'<a href="http://www.mobileread.com/forums/showthread.php?t=134856">Reading List</a>',
'<a href="http://www.mobileread.com/forums/showthread.php?t=126727">Extract ISBN</a>' and
'<a href="http://www.mobileread.com/forums/showthread.php?t=134000">Count Pages</a>'
plugins.</p>
plugins. bbcodeutils code contributed by Pau Sanchez.</p>
<p>
Calibre officially distributes plugins from the mobileread.com forum site.
+14 -4
View File
@@ -715,6 +715,7 @@ permitted_values = {
'language',
'series',
'characters',
'ships',
'status',
'datePublished',
'dateUpdated',
@@ -751,6 +752,7 @@ titleLabels = {
'status-I':'Status:In-Progress',
'series':'Series',
'characters':'Characters',
'ships':'Relationships',
'datePublished':'Published',
'dateUpdated':'Updated',
'dateCreated':'Packaged',
@@ -793,6 +795,15 @@ class CustomColumnsTab(QWidget):
self.custcol_dropdowns = {}
self.custcol_newonlycheck = {}
scrollable = QScrollArea()
scrollcontent = QWidget()
scrollable.setWidget(scrollcontent)
scrollable.setWidgetResizable(True)
self.l.addWidget(scrollable)
self.sl = QVBoxLayout()
scrollcontent.setLayout(self.sl)
for key, column in custom_columns.iteritems():
if column['datatype'] in permitted_values:
@@ -823,9 +834,9 @@ class CustomColumnsTab(QWidget):
newonlycheck.setChecked(prefs['custom_cols_newonly'][key])
horz.addWidget(newonlycheck)
self.l.addLayout(horz)
self.sl.addLayout(horz)
self.l.insertStretch(-1)
self.sl.insertStretch(-1)
self.l.addSpacing(5)
label = QLabel("Special column:")
@@ -847,8 +858,7 @@ class CustomColumnsTab(QWidget):
self.errorcol.setCurrentIndex(self.errorcol.findData(QVariant(prefs['errorcol'])))
horz.addWidget(self.errorcol)
self.l.addLayout(horz)
#print("prefs['custom_cols'] %s"%prefs['custom_cols'])
+27 -35
View File
@@ -7,7 +7,7 @@ __license__ = 'GPL v3'
__copyright__ = '2012, Jim Miller'
__docformat__ = 'restructuredtext en'
import time, os, copy, threading, re
import time, os, copy, threading, re, platform
from ConfigParser import SafeConfigParser
from StringIO import StringIO
from functools import partial
@@ -28,6 +28,7 @@ from calibre.gui2 import error_dialog, warning_dialog, question_dialog, info_dia
from calibre.gui2.dialogs.message_box import ViewLog
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.utils.date import local_tz
from calibre.library.comments import sanitize_comments_html
# The class that all interface action plugins must inherit from
from calibre.gui2.actions import InterfaceAction
@@ -36,7 +37,7 @@ from calibre_plugins.fanfictiondownloader_plugin.common_utils import (set_plugin
create_menu_action_unique, get_library_uuid)
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters, writers, exceptions
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.htmlcleanup import stripHTML
#from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.htmlcleanup import stripHTML
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.epubutils import get_dcsource, get_dcsource_chaptercount, get_story_url_from_html
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.geturls import get_urls_from_page
@@ -175,18 +176,21 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
self.get_list_url_action = self.create_menu_item_ex(self.menu, 'Get Story URLs from Web Page', image='view.png',
triggered=self.get_urls_from_page)
self.menu.addSeparator()
self.config_action = self.create_menu_item_ex(self.menu, '&Configure Plugin', #shortcut=False, # causes crashes on some Macs.
image= 'config.png',
unique_name='Configure FanFictionDownLoader',
shortcut_name='Configure FanFictionDownLoader',
triggered=partial(do_user_config,parent=self.gui))
# print("platform.system():%s"%platform.system())
# print("platform.mac_ver()[0]:%s"%platform.mac_ver()[0])
if not platform.mac_ver()[0]: # Some macs crash on these menu items for unknown reasons.
self.menu.addSeparator()
self.config_action = self.create_menu_item_ex(self.menu, '&Configure Plugin',
image= 'config.png',
unique_name='Configure FanFictionDownLoader',
shortcut_name='Configure FanFictionDownLoader',
triggered=partial(do_user_config,parent=self.gui))
self.about_action = self.create_menu_item_ex(self.menu, 'About Plugin', #shortcut=False, # causes crashes on some Macs.
image= 'images/icon.png',
unique_name='About FanFictionDownLoader',
shortcut_name='About FanFictionDownLoader',
triggered=self.about)
self.about_action = self.create_menu_item_ex(self.menu, 'About Plugin',
image= 'images/icon.png',
unique_name='About FanFictionDownLoader',
shortcut_name='About FanFictionDownLoader',
triggered=self.about)
# Before we finalize, make sure we delete any actions for menus that are no longer displayed
for menu_id, unique_name in self.old_actions_unique_map.iteritems():
@@ -429,13 +433,6 @@ class FanFictionDownLoaderPlugin(InterfaceAction):
print("url:%s"%url)
skip_date_update = False
## was self.ffdlconfig, but we need to be able to change it
## when doing epub update.
ffdlconfig = SafeConfigParser()
ffdlconfig.readfp(StringIO(get_resources("plugin-defaults.ini")))
ffdlconfig.readfp(StringIO(prefs['personal.ini']))
adapter = adapters.getAdapter(ffdlconfig,url,fileform)
options['personal.ini'] = prefs['personal.ini']
if prefs['includeimages']:
# this is a cheat to make it easier for users.
@@ -445,6 +442,13 @@ keep_summary_html:true
make_firstimage_cover:true
''' + options['personal.ini']
## was self.ffdlconfig, but we need to be able to change it
## when doing epub update.
ffdlconfig = SafeConfigParser()
ffdlconfig.readfp(StringIO(get_resources("plugin-defaults.ini")))
ffdlconfig.readfp(StringIO(options['personal.ini']))
adapter = adapters.getAdapter(ffdlconfig,url,fileform)
## three tries, that's enough if both user/pass & is_adult needed,
## or a couple tries of one or the other
for x in range(0,2):
@@ -472,9 +476,9 @@ make_firstimage_cover:true
book['title'] = story.getMetadata("title", removeallentities=True)
book['author_sort'] = book['author'] = story.getList("author", removeallentities=True)
book['publisher'] = story.getMetadata("site")
book['tags'] = writer.getTags() # getTags could be moved up into adapter now. Adapter didn't used to know the fileform
book['comments'] = stripHTML(story.getMetadata("description")) #, removeallentities=True) comments handles entities better.
book['series'] = story.getMetadata("series")
book['tags'] = writer.getTags(removeallentities=True) # getTags could be moved up into adapter now. Adapter didn't used to know the fileform
book['comments'] = sanitize_comments_html(story.getMetadata("description"))
book['series'] = story.getMetadata("series", removeallentities=True)
# adapter.opener is the element with a threadlock. But del
# adapter.opener doesn't work--subproc fails when it tries
@@ -896,18 +900,6 @@ make_firstimage_cover:true
db.set_link_field_for_author(autid, unicode(authurls[i]),
commit=False, notify=False)
# mi.title = oldmi.title
# mi.authors = oldmi.authors
# mi.publisher = oldmi.publisher
# mi.tags = oldmi.tags
# mi.languages = oldmi.languages
# mi.pubdate = oldmi.pubdate
# mi.timestamp = oldmi.timestamp
# mi.comments = oldmi.comments
# mi.series = oldmi.series
# mi.set_identifiers(oldmi.get_identifiers())
# implement 'newonly' flags here by setting to the current
# value again.
if not book['added']:
+8
View File
@@ -109,6 +109,9 @@ def do_download_for_worker(book,options):
when run as a worker job
'''
try:
# import logging
# logging.basicConfig(level=logging.DEBUG,format="%(levelname)s:%(filename)s(%(lineno)d):%(message)s")
book['comment'] = 'Download started...'
ffdlconfig = SafeConfigParser()
@@ -117,6 +120,11 @@ def do_download_for_worker(book,options):
if not options['updateepubcover'] and 'epub_for_update' in book and options['collision'] in (UPDATE, UPDATEALWAYS):
ffdlconfig.set("overrides","never_make_cover","true")
# images only for epub, even if the user mistakenly turned it
# on else where.
if options['fileform'] != "epub":
ffdlconfig.set("overrides","include_images","false")
adapter = adapters.getAdapter(ffdlconfig,book['url'],options['fileform'])
adapter.is_adult = book['is_adult']
+236 -8
View File
@@ -38,6 +38,7 @@ category_label:Category
genre_label:Genre
language_label:Language
characters_label:Characters
ships_label:Relationships
series_label:Series
## Completed/In-Progress
status_label:Status
@@ -70,7 +71,7 @@ version_label:FFDL Version
## You can include extra text or HTML that will be included as-is in
## the title page. Eg: titlepage_entries: ...,<br />,summary,<br />,...
## All current formats already include title and author.
titlepage_entries: series,category,genre,language,characters,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,numWords,site,description
titlepage_entries: series,category,genre,language,characters,ships,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,numWords,site,description
## Try to collect series name and number of this story in series.
## Some sites (ab)use 'series' for reading lists and personal
@@ -121,11 +122,20 @@ allow_unsafe_filename: false
## entries to make epub subjects and calibre tags
## lastupdate creates two tags: "Last Update Year/Month: %Y/%m" and "Last Update: %Y/%m/%d"
include_subject_tags: extratags, genre, category, characters, lastupdate, status
include_subject_tags: extratags, genre, category, characters, ships, lastupdate, status
## extra tags (comma separated) to include, primarily for epub.
extratags: FanFiction
## extra categories, genres, characters, ships and warnings can be
## configured. Used primarily for sites that are dedicated to a genre
## or 'ship and so don't included it for every story.
#extracategories:
#extragenres:
#extracharacters:
#extraships:
#extrawarnings:
## number of seconds to sleep between calls to the story site. May by
## useful if pulling large numbers of stories or if the site is slow.
#slow_down_sleep_time:0.5
@@ -154,6 +164,45 @@ extratags: FanFiction
## doesn't work on some devices either.)
#replace_hr: false
## If set false, the summary will have all html stripped.
## Both this and include_images must be true to get images in the
## summary.
keep_summary_html:true
## Don't like the numbers at the start of chapter titles on some
## sites? You can use strip_chapter_numbers to strip them off. Just
## want to make them all look the same? Strip them off, then add them
## back on with add_chapter_numbers. Don't like the way it strips
## numbers or adds them back? See chapter_title_strip_pattern and
## chapter_title_add_pattern.
strip_chapter_numbers:false
add_chapter_numbers:false
## (Two versions of chapter_title_strip_pattern are shown below. You
## should only have one uncommented.)
## This version will remove the leading number from:
## "1." => ""
## "1. The Beginning" => "The Beginning"
## "1: Start" => "Start"
## "2, Chapter the second" => "Chapter the second"
## etc
chapter_title_strip_pattern:^[0-9]+[\.: -]+
## This version will strip all of the above *plus* remove 'Chapter 1':
## "Chapter 1" => ""
## "1. Chapter 1" => ""
## "1. Chapter 1, Bob's First Clue" => "Bob's First Clue"
## "Chapter 2 - Pirates Place" => "Pirates Place"
## etc
#chapter_title_strip_pattern:^([0-9]+[\.: -]+)?(Chapter *[0-9]+[\.:, -]*)?
## Uses a python template substitution. The ${index} is the 'chapter'
## number and ${title} is the chapter title, after applying
## chapter_title_strip_pattern. Those are the only variables available.
## "The Beginning" => "1. The Beginning"
chapter_title_add_pattern:${index}. ${title}
## Each output format has a section that overrides [defaults]
[html]
@@ -261,11 +310,6 @@ output_css:
## stories. Images will be converted to jpg for size if possible.
#include_images:false
## If not set, the summary will have all html stripped for safety.
## Both this and include_images must be true to get images in the
## summary.
#keep_summary_html:false
## If set, the first image found will be made the cover image. If
## keep_summary_html is true, any images in summary will be before any
## in chapters.
@@ -318,6 +362,11 @@ nook_img_fix:true
## URLs like: http://test1.com?sid=12345
[test1.com]
extratags: FanFiction,Testing
extracategories:Fafner
extragenres:Romance,Fluff
extracharacters:Reginald Smythe-Smythe,Mokona
extraships:Smythe-Smythe/Mokona
extrawarnings:Extreme Bogosity
## If necessary, you can define [<site>:<format>] sections to
## customize the formats differently for the same site. Overrides
@@ -344,6 +393,11 @@ extratags: FanFiction,Testing,HTML
#is_adult:true
[ashwinder.sycophanthex.com]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
extracharacters:Severus Snape,Hermione Granger
extraships:Severus Snape/Hermione Granger
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -352,6 +406,9 @@ extratags: FanFiction,Testing,HTML
#password:yourpassword
[castlefans.org]
## Site dedicated to these categories/characters/ships
extracategories:Castle
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -375,6 +432,11 @@ extratags: FanFiction,Testing,HTML
cover_exclusion_regexp:/images/.*?ribbon.gif
[dark-solace.org]
## Site dedicated to these categories/characters/ships
extracategories:Buffy: The Vampire Slayer
extracharacters:Buffy, Spike
extraships:Spike/Buffy
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -383,6 +445,11 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#password:yourpassword
[dramione.org]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
extracharacters:Draco Malfoy,Hermione Granger
extraships:Draco Malfoy/Hermione Granger
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -396,6 +463,9 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#is_adult:true
[erosnsappho.sycophanthex.com]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
@@ -406,6 +476,9 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
cover_exclusion_regexp:/images/.*?ribbon.gif
[fanfiction.mugglenet.com]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -419,6 +492,10 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#is_adult:true
[fanfiction.portkey.org]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
extraships:Harry Potter/Hermione Granger
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
@@ -441,6 +518,10 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#password:yourpassword
[grangerenchanted.com]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
extracharacters:Hermione Granger
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -454,26 +535,67 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#is_adult:true
[hlfiction.net]
## Site dedicated to these categories/characters/ships
extracategories:Highlander
[ksarchive.com]
## Site dedicated to these categories/characters/ships
extracategories:Star Trek
extracharacters:Kirk,Spock
extraships:Kirk/Spock
[lumos.sycophanthex.com]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[merlinfic.dtwins.co.uk]
## Site dedicated to these categories/characters/ships
extracategories:Merlin
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
## defaults.ini.
#username:YourName
#password:yourpassword
## Some sites also require the user to confirm they are adult for
## adult content. In commandline version, this should go in your
## personal.ini, not defaults.ini.
#is_adult:true
[national-library.net]
## Site dedicated to these categories/characters/ships
extracategories:West Wing
[ncisfic.com]
## Site dedicated to these categories/characters/ships
extracategories:NCIS
[nfacommunity.com]
## Site dedicated to these categories/characters/ships
extracategories:NCIS
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[nha.magical-worlds.us]
## Site dedicated to these categories/characters/ships
extracategories:Buffy: The Vampire Slayer
extracharacters:Willow
[occlumency.sycophanthex.com]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
extracharacters:Severus Snape
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -482,6 +604,9 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#password:yourpassword
[onedirectionfanfiction.com]
## Site dedicated to these categories/characters/ships
extracategories:One Direction
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -495,18 +620,28 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#is_adult:true
[ponyfictionarchive.net]
## Site dedicated to these categories/characters/ships
extracategories:My Little Pony: Friendship is Magic
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[pretendercentre.com]
## Site dedicated to these categories/characters/ships
extracategories:The Pretender
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[samdean.archive.nu]
## Site dedicated to these categories/characters/ships
extracategories:Supernatural
extracharacters:Sam,Dean
extraships:Sam/Dean
[sg1-heliopolis.com]
## Some sites do not require a login, but do require the user to
@@ -515,6 +650,8 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#is_adult:true
[stargate-atlantis.org]
## Site dedicated to these categories/characters/ships
extracategories:Stargate: Atlantis
[svufiction.com]
## Some sites require login (or login for some rated stories) The
@@ -530,6 +667,10 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#is_adult:true
[thehexfiles.net]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
extracharacters:Draco Malfoy,Harry Potter
extraships:Harry Potter/Draco Malfoy
[themasque.net]
## Some sites require login (or login for some rated stories) The
@@ -545,6 +686,9 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#is_adult:true
[thequidditchpitch.org]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -558,12 +702,20 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#is_adult:true
[www.adastrafanfic.com]
## Site dedicated to these categories/characters/ships
extracategories:Star Trek
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[www.dracoandginny.com]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
extracharacters:Draco Malfoy,Ginny Weasley
extraships:Draco Malfoy/Ginny Weasley
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -577,8 +729,13 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#is_adult:true
[www.thealphagate.com]
## Site dedicated to these categories/characters/ships
extracategories:Stargate: SG-1
[www.checkmated.com]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -593,6 +750,10 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#is_adult:true
[www.dokuga.com]
## Site dedicated to these categories/characters/ships
extracategories:InuYasha
extracharacters:Sesshoumaru,Kagome
extraships:Sesshoumaru/Kagome
[www.fanfiction.net]
## fanfiction.net's 'cover' images are really just tiny thumbnails.
@@ -610,6 +771,9 @@ never_make_cover: true
[www.ficbook.net]
[www.fictionalley.org]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
@@ -631,6 +795,9 @@ extratags:
#password:yourpassword
[www.fimfiction.net]
## Site dedicated to these categories/characters/ships
extracategories:My Little Pony: Friendship is Magic
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
@@ -642,20 +809,33 @@ extratags:
#fail_on_password: false
[www.harrypotterfanfiction.com]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[www.hpfandom.net]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[www.hpfanficarchive.com]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
[www.ik-eternal.net]
## Site dedicated to these categories/characters/ships
extracategories:InuYasha
extracharacters:InuYasha,Kagome
extraships:InuYasha/Kagome
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -669,6 +849,9 @@ extratags:
#is_adult:true
[www.libraryofmoria.com]
## Site dedicated to these categories/characters/ships
extracategories:Lord of the Rings
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
@@ -677,6 +860,9 @@ extratags:
[www.mediaminer.org]
[www.midnightwhispers.ca]
## Site dedicated to these categories/characters/ships
extracategories:Queer as Folk
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
@@ -687,8 +873,14 @@ extratags:
cover_exclusion_regexp:/stories/999/images/.*?_trophy.png
[www.ncisfiction.com]
## Site dedicated to these categories/characters/ships
extracategories:NCIS
[www.phoenixsong.net]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
extraships:Harry Potter/Ginny Weasley
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -697,28 +889,43 @@ cover_exclusion_regexp:/stories/999/images/.*?_trophy.png
#password:yourpassword
[www.potionsandsnitches.net]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
[www.prisonbreakfic.net]
## Site dedicated to these categories/characters/ships
extracategories:Prison Break
[www.scarvesandcoffee.net]
## Site dedicated to these categories/characters/ships
extracategories:Glee
extracharacters:Kurt Hummel,Blaine Anderson
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[www.sinful-desire.org]
## Site dedicated to these categories/characters/ships
extracategories:Supernatural
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[www.siye.co.uk]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
extracharacters:Harry Potter,Ginny Weasley
extraships:Harry Potter/Ginny Weasley
[www.squidge.org/peja]
# www.squidge.org/peja calls it Fandom <shrug>
category_label:Fandom
# Remove numWords -- www.squidge.org/peja word counts are inaccurate
titlepage_entries: series,category,genre,language,characters,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,site,description
titlepage_entries: series,category,genre,language,characters,ships,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,site,description
[www.squidge.org/peja:txt]
## Add URLs since there aren't links.
@@ -726,6 +933,8 @@ titlepage_entries: series,category,genre,language,characters,status,datePublishe
titlepage_entries: series,category,genre,language,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,site,storyUrl, authorUrl, description
[www.storiesofarda.com]
## Site dedicated to these categories/characters/ships
extracategories:Lord of the Rings
[www.thepetulantpoetess.com]
## Some sites require login (or login for some rated stories) The
@@ -765,8 +974,13 @@ collect_series: false
#password:yourpassword
[www.twilightarchives.com]
## Site dedicated to these categories/characters/ships
extracategories:Twilight
[www.twilighted.net]
## Site dedicated to these categories/characters/ships
extracategories:Twilight
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -778,6 +992,9 @@ collect_series: false
collect_series: false
[www.twiwrite.net]
## Site dedicated to these categories/characters/ships
extracategories:Twilight
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -789,6 +1006,11 @@ collect_series: false
collect_series: false
[www.walkingtheplank.org]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
extracharacters:Severus Snape,Harry Potter
extraships:Severus Snape/Harry Potter
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
@@ -810,8 +1032,14 @@ collect_series: false
#is_adult:true
[www.wolverineandrogue.com]
## Site dedicated to these categories/characters/ships
extracategories:X-Men Movie
extracharacters:Wolverine,Rogue
[www.wraithbait.com]
## Site dedicated to these categories/characters/ships
extracategories:Stargate: Atlantis
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
+5
View File
@@ -121,6 +121,11 @@ def main():
if options.update and not options.updatecover:
config.set("overrides","never_make_cover","true")
# images only for epub, even if the user mistakenly turned it
# on else where.
if options.format != "epub":
config.set("overrides","include_images","false")
if options.options:
for opt in options.options:
+65 -13
View File
@@ -1,5 +1,5 @@
## This is an example of what your personal configuration might look
## like.
## like. Uncomment options by removing the '#' in front of them.
[defaults]
## Some sites also require the user to confirm they are adult for
@@ -7,34 +7,86 @@
## personal.ini, not defaults.ini.
#is_adult:true
[epub]
## include images from img tags in the body and summary of stories.
## Images will be converted to jpg for size if possible. Images work
## in epub format only. To get mobi or other format with images,
## download as epub and use Calibre to convert.
#include_images:true
## If not set, the summary will have all html stripped for safety.
## Both this and include_images must be true to get images in the
## summary.
#keep_summary_html:true
## If set, the first image found will be made the cover image. If
## keep_summary_html is true, any images in summary will be before any
## in chapters.
#make_firstimage_cover:true
## Resize images down to width, height, preserving aspect ratio.
## Nook size, with margin.
#image_max_size: 580, 725
## Change image to grayscale, if graphics library allows, to save
## space.
#grayscale_images: false
## Most common, I expect will be using this to save username/passwords
## for different sites.
## for different sites. Here are a few examples.
[www.twilighted.net]
#username:YourPenname
#password:YourPassword
## default is false
#collect_series: true
[www.ficwad.com]
#username:YourUsername
#password:YourPassword
[www.twiwrite.net]
#username:YourName
#password:yourpassword
## default is false
#collect_series: true
[www.adastrafanfic.com]
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content.
#is_adult:true
## The [defaults] section here will override the system [defaults],
## but not format, site for site:format sections.
[defaults]
## Directories only useful in commandline or zip files.
#output_filename: books/${title}-${siteabbrev}_${storyId}${formatext}
#output_filename: books/${site}/${authorId}/${title}-${storyId}${formatext}
[www.thewriterscoffeeshop.com]
#username:YourName
#password:yourpassword
#is_adult:true
## default is false
#collect_series: true
[www.fictionalley.org]
#is_adult:true
[www.harrypotterfanfiction.com]
#is_adult:true
[www.fimfiction.net]
#is_adult:true
#fail_on_password: false
[www.tthfanfic.org]
#is_adult:true
## tth is a little unusual--it doesn't require user/pass, but the site
## keeps track of which chapters you've read and won't send another
## update until it thinks you're up to date. This way, on download,
## it thinks you're up to date.
#username:YourName
#password:yourpassword
## For example, zip_output here will turn on zip for html and txt, but
## not epub because the system [epub] section explicitly says
## zip_output: false (epubs *are* specially formated zip files.)
#zip_output: true
#zip_filename: ${title}-${siteabbrev}_${storyId}${formatext}.zip
## This section will override anything in the system defaults or other
## sections here.
[overrides]
## default varies by site. Set true here to force all sites to
## collect series.
#collect_series: true
+1
View File
@@ -97,6 +97,7 @@ import adapter_scarvesandcoffeenet
import adapter_thepetulantpoetesscom
import adapter_wolverineandroguecom
import adapter_sinfuldesireorg
import adapter_merlinficdtwinscouk
## This bit of complexity allows adapters to be added by just adding
## importing. It eliminates the long if/else clauses we used to need
@@ -37,7 +37,6 @@ class AdAstraFanficComSiteAdapter(BaseSiteAdapter):
# Most sites that claim to be
# iso-8859-1 (and some that claim to be
# utf8) are really windows-1252.
self.story.addToList("category","Star Trek")
self.is_adult=False
# get storyId from url--url validation guarantees query is only sid=1234
@@ -191,7 +191,7 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
a = metasoup.find('blockquote',{'class':'userstuff'})
if a != None:
self.setDescription(url,a.text)
self.setDescription(url,a)
#self.story.setMetadata('description',a.text)
a = metasoup.find('dd',{'class':"rating tags"})
@@ -231,9 +231,9 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
self.story.addToList('characters',char.string)
a = metasoup.find('dd',{'class':"relationship tags"})
if a != None:
chars = a.findAll('a',{'class':"tag"})
for char in chars:
self.story.addToList('characters',char.string)
ships = a.findAll('a',{'class':"tag"})
for ship in ships:
self.story.addToList('ships',ship.string)
stats = metasoup.find('dl',{'class':'stats'})
@@ -56,11 +56,6 @@ class AshwinderSycophantHexComAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','asph')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Harry Potter")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%m/%d/%Y"
@@ -80,11 +80,6 @@ class CastleFansOrgAdapter(BaseSiteAdapter): # XXX
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','cslf') # XXX
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Castle") # XXX
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%b %d, %Y" # XXX
@@ -56,11 +56,6 @@ class ChaosSycophantHexComAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','csph')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
#self.story.addToList("category","Harry Potter")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%m/%d/%Y"
@@ -44,25 +44,20 @@ class CheckmatedComAdapter(BaseSiteAdapter):
self.username = "" # 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])
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
self._setURL('http://' + self.getSiteDomain() + '/story.php?story='+self.story.getMetadata('storyId'))
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','chm')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Harry Potter")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%b %d, %Y"
@staticmethod # must be @staticmethod, don't remove it.
def getSiteDomain():
# The site domain. Does have www here, if it uses it.
@@ -82,7 +77,7 @@ class CheckmatedComAdapter(BaseSiteAdapter):
return True
else:
return False
def performLogin(self, url):
params = {}
@@ -93,8 +88,8 @@ class CheckmatedComAdapter(BaseSiteAdapter):
params['name'] = self.getConfig("username")
params['pass'] = self.getConfig("password")
params['login'] = 'yes'
params['submit'] = 'login'
params['submit'] = 'login'
loginUrl = 'http://' + self.getSiteDomain()+'/login.php'
d = self._fetchUrl(loginUrl,params)
e = self._fetchUrl(url)
@@ -135,33 +130,33 @@ class CheckmatedComAdapter(BaseSiteAdapter):
# The actual text that is used to announce you need to be an
# adult varies from site to site. Again, print data before
# the title search to troubleshoot.
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.findAll('span', {'class' : 'storytitle'})
self.story.setMetadata('title',a[0].string)
# Find authorid and URL from... author url.
a = a[1].find('a', href=re.compile(r"authors.php\?name\=\w+"))
self.story.setMetadata('authorId',a['href'].split('=')[1])
self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href'])
self.story.setMetadata('author',a.string)
a = soup.find('select', {'name' : 'chapter'})
if a == None:
self.chapterUrls.append((self.story.getMetadata('title'),url))
else:
for chapter in a.findAll('option'):
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/story.php?story='+self.story.getMetadata('storyId')+'&chapter='+chapter['value']))
self.story.setMetadata('numChapters',len(self.chapterUrls))
# eFiction sites don't help us out a lot with their meta data
@@ -174,7 +169,7 @@ class CheckmatedComAdapter(BaseSiteAdapter):
except:
return ""
# website does not keep track of word count, and there is no convenient way to calculate it
summary = soup.find('fieldset')
@@ -182,21 +177,23 @@ class CheckmatedComAdapter(BaseSiteAdapter):
summary.name='div'
self.setDescription(url,summary)
# <span class="label">Rated:</span> NC-17<br /> etc
table = soup.findAll('div', {'class' : 'text'})[1]
for labels in table.findAll('tr'):
value = labels.findAll('td')[1]
label = labels.findAll('td')[0]
if 'Rating' in stripHTML(label):
self.story.setMetadata('rating', stripHTML(value))
if 'Ship' in stripHTML(label):
if value.string != "none/none":
self.story.addToList('ships',value.string)
for char in value.string.split('/'):
if char != 'none':
self.story.addToList('characters',char)
self.story.addToList('characters',char)
if 'Status' in stripHTML(label):
if value.find('img', {'src' : 'img/incomplete.gif'}) == None:
@@ -206,11 +203,11 @@ class CheckmatedComAdapter(BaseSiteAdapter):
if 'Published' in stripHTML(label):
self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat))
if 'Updated' in stripHTML(label):
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
a = self._fetchUrl(self.story.getMetadata('authorUrl')+'&cat=stories')
a = self._fetchUrl(self.story.getMetadata('authorUrl')+'&cat=stories')
for story in bs.BeautifulSoup(a).findAll('table', {'class' : 'storyinfo'}):
a = story.find('a', href=re.compile(r"review.php\?s\="+self.story.getMetadata('storyId')+'&act=view'))
if a != None:
@@ -220,8 +217,8 @@ class CheckmatedComAdapter(BaseSiteAdapter):
if 'genre' in stripHTML(label):
for genre in value.findAll('img'):
self.story.addToList('genre',genre['title'])
# grab the text for an individual chapter.
def getChapterText(self, url):
@@ -229,11 +226,11 @@ class CheckmatedComAdapter(BaseSiteAdapter):
soup = bs.BeautifulSoup(self._fetchUrl(url),
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
div = soup.find('div', {'id' : 'resizeableText'})
div.find('div', {'class' : 'storyTools'}).extract()
if None == div:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
return self.utf8FromSoup(url,div)
@@ -56,11 +56,6 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','dksl')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Buffy the Vampire Slayer")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%d %B %Y"
@@ -56,11 +56,6 @@ class DokugaComAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','dkg')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","InuYasha")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
if 'fanfiction' in self.story.getMetadata('section'):
@@ -55,13 +55,6 @@ class DracoAndGinnyComAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','dcagn')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Harry Potter")
self.story.addToList("characters","Ginny Weasley")
self.story.addToList("characters","Draco Malfoy")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%b %d, %Y"
@@ -295,6 +288,9 @@ class DracoAndGinnyComAdapter(BaseSiteAdapter):
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
div = soup.find('div', {'class' : 'listbox'})
if None == div:
div = soup.find('div', {'id' : 'story'})
if None == div:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
@@ -55,11 +55,6 @@ class DramioneOrgAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','drmn')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Harry Potter")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%d %B %Y"
@@ -56,11 +56,6 @@ class ErosnSapphoSycophantHexComAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','essph')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Harry Potter")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%d/%m/%y"
@@ -201,7 +201,7 @@ class FicBookNetAdapter(BaseSiteAdapter):
break
summary=soup.find('span', {'class' : 'urlize'})
self.setDescription(url,summary.text)
self.setDescription(url,summary)
#self.story.setMetadata('description', summary.text)
# grab the text for an individual chapter.
@@ -37,7 +37,6 @@ class FictionAlleyOrgSiteAdapter(BaseSiteAdapter):
# Most sites that claim to be
# iso-8859-1 (and some that claim to be
# utf8) are really windows-1252.
self.story.addToList("category","Harry Potter")
self.is_adult=False
# get storyId from url--url validation guarantees query correct
@@ -124,7 +124,7 @@ class FicwadComSiteAdapter(BaseSiteAdapter):
# description
storydiv = soup.find("div",{"id":"story"})
self.setDescription(url,storydiv.find("blockquote",{'class':'summary'}).p.string)
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:
@@ -27,6 +27,8 @@ from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from ..bbcodeutils.bbcodeparser import bbcodeparser
from base_adapter import BaseSiteAdapter, makeDate
def getClass():
@@ -133,19 +135,23 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
if storyMetadata["categories"][category]:
self.story.addToList("genre", category)
self.story.addToList("category", "My Little Pony")
self.story.setMetadata("numWords", str(storyMetadata["words"]))
# fimfic is the first site with an explicit cover image.
if self.getConfig('include_images') and "image" in storyMetadata.keys():
coverurl = storyMetadata["image"]
if "full_image" in storyMetadata:
coverurl = storyMetadata["full_image"]
else:
coverurl = storyMetadata["image"]
if coverurl.startswith('//static.fimfiction.net'): # fix for img urls missing 'http:'
coverurl = "http:"+coverurl
self.story.addImgUrl(self,self.url,coverurl,self._fetchUrlRaw,cover=True)
self.setDescription(self.url, storyMetadata["description"])
# the fimfic API gives bbcode for desc, not html.
# btw, bbcode honors newlines, html doesn't. change newlines to br tags.
self.setDescription(self.url,
bbcodeparser().parse(storyMetadata["description"]).html(doDeepCopy=False).replace('\r','').replace('\n','<br />'))
# Dates are in Unix time
# Take the publish date from the first chapter posted
@@ -155,8 +161,18 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
self.story.setMetadata("dateUpdated", datetime.fromtimestamp(rawDateUpdated))
soup = bs.BeautifulSoup(data).find("div", {"class":"story"})
for character in [character_icon["title"] for character_icon in soup.findAll("a", {"class":"character_icon"})]:
self.story.addToList("characters", character)
# fimfic stopped putting the char name on or around the char
# icon now for some reason. Pull it from the image name with
# some heuristics.
for character in [character_icon["src"] for character_icon in soup.findAll("img", {"class":"character_icon"})]:
# //static.fimfiction.net/images/characters/twilight_sparkle.png
# 5th split /, remove last four, replace _, capitolize every word(title())
char = character.split('/')[5][:-4].replace('_',' ').title()
if char == 'Oc':
char = "OC"
if char == 'Cmc':
char = "Cutie Mark Crusaders"
self.story.addToList("characters", char)
def getChapterText(self, url):
@@ -60,11 +60,6 @@ class GrangerEnchantedCom(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','gech')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Harry Potter")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%d/%b/%Y"
@@ -37,7 +37,6 @@ class HarryPotterFanFictionComSiteAdapter(BaseSiteAdapter):
# Most sites that claim to be
# iso-8859-1 (and some that claim to be
# utf8) are really windows-1252.
self.story.addToList("category","Harry Potter")
self.is_adult=False
# get storyId from url--url validation guarantees query is only psid=1234
@@ -55,11 +55,6 @@ class HLFictionNetAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','hlf')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Highlander")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%m/%d/%y"
@@ -60,11 +60,6 @@ class HPFandomNetAdapterAdapter(BaseSiteAdapter): # XXX
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','hpfdm') # XXX
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Harry Potter") # 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
@@ -55,11 +55,6 @@ class HPFanficArchiveComAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','hpffa')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Harry Potter")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%B %d, %Y"
@@ -55,11 +55,6 @@ class IkEternalNetAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','ike')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","InuYasha")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%B %d, %Y"
@@ -60,11 +60,6 @@ class KSArchiveComAdapter(BaseSiteAdapter): # XXX
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','ksa') # XXX
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Star Trek") # XXX
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%b/%d/%Y" # XXX
@@ -56,11 +56,6 @@ class LibraryOfMoriaComAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','lom')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Lord of the Rings")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%B %d, %Y"
@@ -56,11 +56,6 @@ class LumosSycophantHexComAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','lsph')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Harry Potter")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%m/%d/%Y"
@@ -0,0 +1,290 @@
# -*- 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.
#
import time
import logging
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 MerlinFicDtwinsCoUk
# Class name has to be unique. Our convention is camel case the
# sitename with Adapter at the end. www is skipped.
class MerlinFicDtwinsCoUk(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])
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# 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','mrfd')
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%b %d, %Y"
@staticmethod # must be @staticmethod, don't remove it.
def getSiteDomain():
# The site domain. Does have www here, if it uses it.
return 'merlinfic.dtwins.co.uk'
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
def getSiteURLPattern(self):
return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$"
## Login seems to be reasonably standard across eFiction sites.
def needToLoginCheck(self, data):
if 'Registered Users Only' in data \
or 'There is no such account on our website' in data \
or "That password doesn't match the one in our database" in data:
return True
else:
return False
def performLogin(self, url):
params = {}
if self.password:
params['penname'] = self.username
params['password'] = self.password
else:
params['penname'] = self.getConfig("username")
params['password'] = self.getConfig("password")
params['cookiecheck'] = '1'
params['submit'] = 'Submit'
loginUrl = 'http://' + self.getSiteDomain() + '/user.php?action=login'
logging.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
logging.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=4"
else:
addurl=""
# index=1 makes sure we see the story chapter index. Some
# sites skip that for one-chapter stories.
url = self.url+'&index=1'+addurl
logging.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+((?:&amp;ageconsent=ok)?&amp;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 &amp; error in url.
addurl = addurl.replace("&amp;","&")
url = self.url+'&index=1'+addurl
logging.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',a.string)
# 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 <i> in chapter titles.
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+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 ""
# <span class="label">Rated:</span> NC-17<br /> 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 '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 'Pairing' in label:
ships = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1'))
for ship in ships:
self.story.addToList('ships',ship.string)
if 'Genre' in label:
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=3'))
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+'/'+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)
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):
logging.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)
@@ -60,11 +60,6 @@ class MidnightwhispersCaAdapter(BaseSiteAdapter): # XXX
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','mw') # XXX
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Queer as Folk") # XXX
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%B %d, %Y" # XXX
@@ -59,11 +59,6 @@ class MuggleNetComAdapter(BaseSiteAdapter): # XXX
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','mgln') # XXX
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Harry Potter") # XXX
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%m/%d/%y" # XXX
@@ -55,11 +55,6 @@ class NationalLibraryNetAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','ntlb')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","West Wing")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%m-%d-%y"
@@ -154,6 +149,10 @@ class NationalLibraryNetAdapter(BaseSiteAdapter):
for char in value.string.split(', '):
self.story.addToList('characters',char)
if 'Pairing' in label:
for char in value.string.split(', '):
self.story.addToList('ships',char)
if 'Warnings' in label:
for warning in value.string.split(', '):
self.story.addToList('warnings',warning)
@@ -55,11 +55,6 @@ class NCISFicComAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','ncisf')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","NCIS")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%m-%d-%y"
@@ -154,6 +149,10 @@ class NCISFicComAdapter(BaseSiteAdapter):
for char in value.string.split(', '):
self.story.addToList('characters',char)
if 'Pairing' in label:
for char in value.string.split(', '):
self.story.addToList('ships',char)
if 'Warnings' in label:
for warning in value.string.split(', '):
self.story.addToList('warnings',warning)
@@ -56,11 +56,6 @@ class NCISFictionComAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','ncisfn')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","NCIS")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%d/%m/%Y"
@@ -157,6 +152,11 @@ class NCISFictionComAdapter(BaseSiteAdapter):
for char in chars:
self.story.addToList('characters',char.string)
if 'Pairing' in label:
ships = value.findAll('a')
for ship in ships:
self.story.addToList('ships',ship.string)
if 'Genre' in label:
genres = value.findAll('a')
for genre in genres:
@@ -60,11 +60,6 @@ class NfaCommunityComAdapter(BaseSiteAdapter): # XXX
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','nfa') # XXX
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","NCIS") # XXX
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%m/%d/%Y" # XXX
@@ -56,11 +56,6 @@ class NHAMagicalWorldsUsAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','nha')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Buffy the Vampire Slayer")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = " %m/%d/%y"
@@ -56,11 +56,6 @@ class OcclumencySycophantHexComAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','osph')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Harry Potter")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%m/%d/%Y"
@@ -55,11 +55,6 @@ class OneDirectionFanfictionComAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','odf')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","One Direction")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%m/%d/%y"
@@ -55,11 +55,6 @@ class PhoenixSongNetAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','phs')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Harry Potter")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%B %d %Y"
@@ -60,11 +60,6 @@ class PonyFictionArchiveNetAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','pffa')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","My Little Pony")
@staticmethod # must be @staticmethod, don't remove it.
def getSiteDomain():
# The site domain. Does have www here, if it uses it.
@@ -62,11 +62,6 @@ class PortkeyOrgAdapter(BaseSiteAdapter): # XXX
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','prtky') # XXX
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Harry Potter") # XXX
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%d/%m/%y" # XXX
@@ -37,7 +37,6 @@ class PotionsAndSnitchesNetSiteAdapter(BaseSiteAdapter):
# Most sites that claim to be
# iso-8859-1 (and some that claim to be
# utf8) are really windows-1252.
self.story.addToList("category","Harry Potter")
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
@@ -142,7 +141,7 @@ class PotionsAndSnitchesNetSiteAdapter(BaseSiteAdapter):
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
charstext = [char.string for char in chars]
for char in charstext:
if char == "!Snape and Harry (required)":
if "Snape and Harry (required)" in char:
self.story.addToList('characters',"Snape")
self.story.addToList('characters',"Harry")
else:
@@ -55,11 +55,6 @@ class PretenderCenterComAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','ptdc')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","The Pretender")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%d/%m/%y"
@@ -55,11 +55,6 @@ class PrisonBreakFicNetAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','pbf')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Prison Break")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%B %d, %Y"
@@ -157,6 +152,11 @@ class PrisonBreakFicNetAdapter(BaseSiteAdapter):
for char in chars:
self.story.addToList('characters',char.string)
if 'Pairing' in label:
ships = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=3')) # XXX
for ship in ships:
self.story.addToList('ships',ship.string.replace(" and ","/"))
if 'Genre' in label:
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) # XXX
for genre in genres:
@@ -55,11 +55,6 @@ class SamDeanArchiveNuAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','sda')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Supernatural")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%m/%d/%y"
@@ -55,13 +55,6 @@ class ScarvesAndCoffeeNetAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','scacf')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Glee")
self.story.addToList("characters","Kurt Hummel")
self.story.addToList("characters","Blaine Anderson")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%m/%d/%Y"
@@ -58,7 +58,7 @@ class SG1HeliopolisComAdapter(BaseSiteAdapter):
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
# do. Can't use extracategories, could be Atlantis or SG-1
if 'atlantis' in self.story.getMetadata('section'):
self.story.addToList("category","Stargate: Atlantis")
else:
@@ -55,11 +55,6 @@ class SinfulDesireOrgAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','snds')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Supernatural")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%m/%d/%Y"
@@ -56,11 +56,6 @@ class SiyeCoUkAdapter(BaseSiteAdapter): # XXX
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','siye') # XXX
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Harry Potter") # 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
@@ -55,11 +55,6 @@ class StargateAtlantisOrgAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','stat')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Stargate: Atlantis")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%B %d %Y"
@@ -55,11 +55,6 @@ class StoriesOfArdaComAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','soa')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Lord of the Rings")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%m/%d/%Y"
+2 -2
View File
@@ -73,8 +73,8 @@ class TestSiteAdapter(BaseSiteAdapter):
self.story.setMetadata(u'title',"Test Story Title "+self.story.getMetadata('storyId'))
self.story.setMetadata('author','Test Author aa')
self.story.setMetadata('storyUrl',self.url)
self.story.setMetadata('description',u'Description '+self.crazystring+u''' Done
self.setDescription(self.url,u'Description '+self.crazystring+u''' Done
<p>
Some more longer description. "I suck at summaries!" "Better than it sounds!" "My first fic"
''')
self.story.setMetadata('datePublished',makeDate("1975-03-15","%Y-%m-%d"))
@@ -55,11 +55,6 @@ class TheAlphaGateComAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','tag')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Stargate: SG-1")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%d %b %Y"
@@ -55,11 +55,6 @@ class TheHexFilesNetAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','thf')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Harry Potter")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%Y.%m.%d"
@@ -60,11 +60,6 @@ class TheQuidditchPitchOrgAdapter(BaseSiteAdapter): # XXX
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','tqdpch') # XXX
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Harry Potter") # XXX
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%m/%d/%Y" # XXX
@@ -55,11 +55,6 @@ class TwilightArchivesComAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','twa')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Twilight")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%d %b %y"
@@ -37,7 +37,6 @@ class TwilightedNetSiteAdapter(BaseSiteAdapter):
# Most sites that claim to be
# iso-8859-1 (and some that claim to be
# utf8) are really windows-1252.
self.story.addToList("category","Twilight")
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
self.password = ""
@@ -37,7 +37,6 @@ class TwiwriteNetSiteAdapter(BaseSiteAdapter):
# Most sites that claim to be
# iso-8859-1 (and some that claim to be
# utf8) are really windows-1252.
self.story.addToList("category","Twilight")
self.is_adult = False
self.username = "NoneGiven" # if left empty, twiwrite.net doesn't return any message at all.
self.password = ""
@@ -53,11 +53,6 @@ class WalkingThePlankOrgAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','wtp')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Harry Potter")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%b %d, %Y"
@@ -55,13 +55,6 @@ class WolverineAndRogueComAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','wrfa')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","X-Men Movie")
self.story.addToList("characters","Wolverine")
self.story.addToList("characters","Rogue")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%m/%d/%Y"
@@ -56,11 +56,6 @@ class WraithBaitComAdapter(BaseSiteAdapter):
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','wb')
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Stargate")
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%d %b %Y"
+5 -3
View File
@@ -199,7 +199,8 @@ class BaseSiteAdapter(Configurable):
if (self.chapterFirst!=None and index < self.chapterFirst) or \
(self.chapterLast!=None and index > self.chapterLast):
self.story.addChapter(removeEntities(title),
None)
None,
self)
else:
if self.oldchapters and index < len(self.oldchapters):
data = self.utf8FromSoup(None,
@@ -208,7 +209,8 @@ class BaseSiteAdapter(Configurable):
else:
data = self.getChapterText(url)
self.story.addChapter(removeEntities(title),
removeEntities(data))
removeEntities(data),
self)
self.storyDone = True
# include image, but no cover from story, add default_cover_image cover.
@@ -289,7 +291,7 @@ class BaseSiteAdapter(Configurable):
def setDescription(self,url,svalue):
#print("\n\nsvalue:\n%s\n"%svalue)
if self.getConfig('keep_summary_html'):
if isinstance(svalue,str) or isinstance(svalue,unicode):
if isinstance(svalue,basestring):
svalue = bs.BeautifulSoup(svalue)
self.story.setMetadata('description',self.utf8FromSoup(url,svalue))
else:
+325
View File
@@ -0,0 +1,325 @@
#!/usr/bin/python
# -*- coding: UTF-8 -*-
#
# Author: Pau Sanchez (contact@pausanchez.com)
# Version: v1.0
# Last Modified: 2010/09/15
#
# For the latest version check out:
# http://www.codigomanso.com/en/projects
#
# My blog:
# http://www.codigomanso.com/en/ - English Version
# http://www.codigomanso.com/es/ - Spanish Version
#
import sys
import os
import re
import urllib
class bbcode2html:
'''
This class gets a parsed BBCode and transforms it to valid HTML
Useful functions of this class:
html
convertToHTML
Example:
> parser = bbcodeparser ()
> parser.parse ('[b]bold[/b]')
> bbcode2html (parser).html()
<b>bold</b>
# This is faster for huge strings but changes the parser object internally
> bbcode2html (parser).html(doDeepCopy = False)
<b>bold</b>
'''
def __init__ (self, parser):
self._parser = parser
return
def html (self, allowClassAttr = False, doDeepCopy = True, parser = None):
'''
Convert current parsed code to HTML
Example:
code = bbcodeparser ('[b]bold[/b]')
code.html() -> '<b>bold</b>'
'''
if parser is None:
parser = self._parser
tokens = parser
if instanceof (parser, bbcodeparser):
tokens = parser.getTokens()
return bbcode2html.convertToHTML (tokens, allowClassAttr = allowClassAttr, doDeepCopy = doDeepCopy)
@staticmethod
def htmlString (string):
toReplace = {
u'<' : '&lt;',
u'>' : '&gt;',
u'"' : "&quot;",
u'&' : "&amp;"
}
for entity in toReplace:
string = string.replace(entity, toReplace[entity])
return string
@staticmethod
def getValidTags ():
simpleTags = ['b', 'u', 'i', 'sup', 'sub', 'ul', 'ol', 'li', 'table', 'tr', 'th', 'td', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']
validTags = {
'p' : { 'color' : 'color', 'size' : 'size', 'font' : 'font' },
'color' : { 'color' : 'color' },
'size' : { 'size' : 'size' },
'font' : { 'font' : 'font' },
'img' : { 'alt' : 'alt', 'title' : 'title', 'width' : 'width' , 'height' : 'height', 'img' : 'img'},
'url' : { 'href' : 'href', 'url' : 'href', 'link' : 'href', 'title' : 'title' },
's' : { },
'code' : { },
'quote' : { },
'list' : { 'list' : 'type' },
'email' : { 'email': 'href'},
'google' : { 'google': 'google'},
'wikipedia' : { 'wikipedia' : 'wikipedia', 'language' : 'language', 'lang' : 'lang'}
}
for tag in simpleTags:
validTags[tag] = { }
return validTags
@staticmethod
def convertToHTML (tokens, allowClassAttr = False, validTags = None, doDeepCopy = True):
'''
Convert internally parsed BBCode to XHTML
@doDeepCopy
True: it does a deep copy of tokens so this list will remain unchanged
False: tokens will be modified internally, but the output will be produced like 5x faster
it's a good idea to use False only when this is the last operation
'''
# do a deep copy
if doDeepCopy:
import copy
tokens = copy.deepcopy (tokens)
# filter invalid tags and attributes
if validTags is None:
validTags = bbcode2html.getValidTags()
bbcode2html._filterInvalidTagsAndAttributes (tokens, validTags, allowClassAttr)
# Start to convert
index = 0
tokenLength = len (tokens)
# use a list for the output (an order of magnitude faster than using string concatenation)
htmlList = []
lastListOpener = []
while index < tokenLength:
if isinstance (tokens [index], basestring):
htmlList.append (bbcode2html.htmlString (tokens [index]))
index += 1
continue
token = tokens[index]
tag = token['tag'] # opening or closing simple tag. e.g: 'b', '/b', '/u', ...
tagName = (tag[1:] if tag[0] == '/' else tag)
tagOpener = (u'/' if tag[0] == '/' else u'')
tokenArgs = (token['args'] if 'args' in token else {})
# opening or closing simple tag COLOR / SIZE
if (tagName in ['p', 'color', 'size', 'font']):
style = ''
style += ((u' color: ' + tokenArgs['color'] + u';') if ('color' in tokenArgs) else '')
style += ((u' font-size: ' + tokenArgs['size'] + u'pt;') if ('size' in tokenArgs) else '')
style += ((u' font-family: ' + tokenArgs['font'] + u';') if ('font' in tokenArgs) else '')
style = style.strip()
pArgs = {}
if style != '':
pArgs ['style'] = style
if 'class' in tokenArgs:
pArgs ['class'] = tokenArgs['class']
if ('args' not in token) and (tagName != 'p'):
if (tagOpener == '/'): # if closing tag, close it
htmlList.append (u'</span>')
index += 1
continue
if tagName != 'p':
tag = tagOpener + u'span'
htmlList.append (bbcode2html.xml (tag, pArgs))
# IMG tag
elif tag == 'img' and (index+2 < tokenLength):
if 'img' in tokenArgs:
# has the form of <width>x<height> ?
sizeMatch = re.match (u'^\s*(\d+)[xX](\d+)\s*$', tokenArgs['img'])
if sizeMatch is not None:
tokenArgs['width'] = sizeMatch.group(1)
tokenArgs['height'] = sizeMatch.group(2)
# then assume is the alternative text
else:
tokenArgs['alt'] = tokenArgs['img']
del tokenArgs['img']
# add the source of the image
tokenArgs ['src'] = tokens[index+1]
# [img]http://www.whatever.com/pic.jpg[/img]
htmlList.append (
bbcode2html.xml ('img', tokenArgs, soloTag=True)
)
index += 2 # skip next token and closing tag
# URL tag
elif tag == 'url':
if ('args' not in token) and (index+2 < tokenLength):
# [url]http://www.google.com[/url]
htmlList.append (bbcode2html.xml ('a', { 'href' : tokens[index+1] }))
else:
# [url=http://www.google.com]Google[/url]
# [url link=http://www.google.com title="This is Google"]Google[/url]
htmlList.append (bbcode2html.xml ('a', tokenArgs))
# URL closing tag (sometimes needed)
elif (tag == '/url') or (tag == '/email'):
htmlList.append (u'</a>')
# Email tag
elif tag == 'email':
if ('args' not in token) and (index+2 < tokenLength):
# [email]asdf@asdf.com]
htmlList.append (bbcode2html.xml ('a', { 'href' : u'mailto:' + tokens[index+1].strip() }))
else:
# [email=asdf@asfd.com]john smith[/email]
if 'href' in tokenArgs:
tokenArgs['href'] = u'mailto:' + tokenArgs['href']
htmlList.append (bbcode2html.xml ('a', tokenArgs))
elif tagName == 'list':
if tagOpener == '/':
htmlList.append (bbcode2html.xml (u'/' + lastListOpener.pop()))
else:
if ('type' not in tokenArgs):
htmlList.append (bbcode2html.xml (tagOpener + u'ul', tokenArgs))
lastListOpener.append ('ul')
else:
htmlList.append (bbcode2html.xml (tagOpener + u'ol', tokenArgs))
lastListOpener.append ('ol')
elif tagName == '*':
htmlList.append (bbcode2html.xml (tagOpener + u'li', tokenArgs))
elif (tagName == 's'):
tokenArgs['style'] = 'text-decoration: line-through;'
htmlList.append (bbcode2html.xml (tagOpener + u'span', tokenArgs))
elif (tagName == 'code'):
htmlList.append (bbcode2html.xml (tagOpener + u'pre', tokenArgs))
elif (tagName == 'quote'):
htmlList.append (bbcode2html.xml (tagOpener + u'blockquote', tokenArgs))
elif (tagName == 'google'):
htmlList.append (
bbcode2html.xml (
tagOpener + u'a',
{'href' : 'http://www.google.com/search?q=' + urllib.quote_plus (tokens[index+1])},
tokens[index+1]
)
)
index += 2
elif (tagName == 'wikipedia'):
subdomain = 'www'
for arg in ['lang', 'language', 'wikipedia']:
if arg in tokenArgs:
subdomain = tokenArgs[arg]
htmlList.append (
bbcode2html.xml (
tagOpener + u'a',
{'href' : 'http://' + subdomain + '.wikipedia.org/wiki/' + tokens[index+1].replace (' ', '_')},
tokens[index+1]
)
)
index += 2
elif (tagName in validTags):
htmlList.append (
bbcode2html.xml (tag, tokenArgs)
)
else:
# ignore this tag
pass
index += 1
return ''.join (htmlList)
@staticmethod
def _filterInvalidTagsAndAttributes (tokens, validTags, allowClassAttr):
'''
Helper function to filter out invalid attributes from the tokens list
'''
# add 'class' attribute as valid (mapping 'class' itself)
if allowClassAttr:
for attr in validTags:
validTags[attr]['class'] = 'class'
# remove invalid attributes from tokens
for tindex in range(0, len(tokens)):
if isinstance (tokens[tindex], dict) and ('args' in tokens[tindex]) and (tokens[tindex]['tag'] in validTags):
validList = validTags[tokens[tindex]['tag']]
filteredArgs = {}
for arg in tokens[tindex]['args']:
if arg in validList:
# rename the argument
filteredArgs[validList[arg]] = tokens[tindex]['args'][arg]
else:
pass # do not include this arg in the filteredArgs
tokens[tindex]['args'] = filteredArgs
return
@staticmethod
def xml (tag, attrs = {}, text = None, soloTag = False):
'''
Helper function to produce valid XML output
'''
xml = u'<' + tag.lower()
# make sure we sort attributes alphabetically (for deterministic output)
# Faster but non-deterministic:
# for (key, value) in attrs.iteritems():
# xml += u' ' + key + u'="' + value + u'"'
for key in sorted (attrs.keys()):
xml += u' ' + key + u'="' + attrs[key] + u'"'
# close tag
if text is None:
if soloTag:
xml += u' />'
else:
xml += u'>'
else:
xml += u'>' + text + u'</' + tag.lower() + '>'
return xml
@@ -0,0 +1,77 @@
#!/usr/bin/python
# -*- coding: UTF-8 -*-
#
# Author: Pau Sanchez (contact@pausanchez.com)
# Version: v1.0
# Last Modified: 2010/09/15
#
# For the latest version check out:
# http://www.codigomanso.com/en/projects
#
# My blog:
# http://www.codigomanso.com/en/ - English Version
# http://www.codigomanso.com/es/ - Spanish Version
#
import sys
import os
import re
import hashlib
class bbcodebuilder:
'''
This class helps to build BBCode programmatically.
The function names are used as the tag name, then the first parameter
is the string that goes inside the tags and any extra parameter is
appended as a parameter to the tag
Examples:
> bbcode = bbcodebuilder() # create a instance!
> print bbcode.b ('bold')
[b]bold[/b]
> print bbcode.color ('this goes in red', 'red')
[color=red]this goes in red[/color]
> print bbcode.url ('Google', 'http://www.google.com')
[url=http://www.google.com]Google[/url]
> print bbcode.alist('item 1', 'item 2')
[list=a]
[*]item 1
[*]item 2
[/list]
This solution is based on the recipe found on:
http://code.activestate.com/recipes/576831-simple-bbcode-support/
'''
def __getattr__(self, name):
'''
This is a generic getter that returns a function which gets the first parameter
as the string that goes between the tags, and extra parameters as tag parameters.
The name of the attribute is used as the tag name
'''
class bbcodebuilder_helper:
def __init__(self, name):
self._name = name
def __call__(self, string, *args):
return u'[{0}{1}]{2}[/{0}]'.format(self._name, (u'=' + u','.join(map(str, args))) if args else u'', string)
return bbcodebuilder_helper (name)
def list(self, *items):
return u'[list]' + u''.join(map(lambda item: u"\n [*]" + item, items)) + u"\n[/list]"
def nlist(self, *items):
return u'[list=1]' + u''.join(map(lambda item: u"\n [*]" + item, items)) + u"\n[/list]"
def alist(self, *items):
return u'[list=a]' + u''.join(map(lambda item: u"\n [*]" + item, items)) + u"\n[/list]"
@@ -0,0 +1,251 @@
#!/usr/bin/python
# -*- coding: UTF-8 -*-
#
# Author: Pau Sanchez (contact@pausanchez.com)
# Version: v1.0
# Last Modified: 2010/09/15
#
# For the latest version check out:
# http://www.codigomanso.com/en/projects
#
# My blog:
# http://www.codigomanso.com/en/ - English Version
# http://www.codigomanso.com/es/ - Spanish Version
#
import sys
import os
import re
import hashlib
class bbcodeparser:
'''
This class parses BBCode into a internal structure to allow later processing and
conversion to HTML.
The parser tries to fix invalid code (like unclosed tags)
Useful URLs:
http://en.wikipedia.org/wiki/BBCode
http://www.bbcode.org/reference.php
Example:
> bbcode = bbcodeparser ()
> bbcode.parse ('[b]text in bold[/b]').html()
<b>text in bold</b>
# dump HTML
> bbcode.parse ('[p][color=red]text in red').html()
<p><span style="color:red;">text in red</span></p>
# dump fixed BBCode
> bbcode.parse ('[p][color=red]text in red').bbcode()
[p][color=red]text in red[/color][/p]
> bbcode.parse ('This [b][i]code[/b] will be fixed[/invalid]').bbcode()
This [b][i]code[/i][/b] will be fixed
# dump fixed bbcode
> str (bbcodeparse ('This [b][i]code[/b] will be fixed[/invalid]'))
This [b][i]code[/i][/b] will be fixed
'''
_bbcode = ''
_tokens = []
def __init__ (self, bbcode = '', fixInvalidCode = True):
''' Initialize and parse bbcode string (if any is given)
'''
self.parse (bbcode, fixInvalidCode)
return
def __str__ (self):
return self.bbcode()
def parse (self, bbcode = None, fixInvalidCode = True):
'''
It will parse and return the token list, trying to fix tags if
fixInvalidCode is True
It will return the current object to allow chaining
Example:
code = bbcode()
code.parse ('<b>bold</b>', True) ->
code.parse ('<b>bold<i>italics</b>', True) -> internally will add the missing '</i>'
'''
if bbcode is not None:
self._bbcode = bbcode
self._tokens = self.tokenize (bbcode)
if fixInvalidCode:
self._tokens = self.fixWrongTags (self._tokens)
return self
# return ALL tokens
def getTokens (self):
return self._tokens
def bbcode (self):
'''
Dump BBCode again. This is useful for dumping valid BBCode
'''
bbcode = []
for token in self._tokens:
if token is None:
continue
if isinstance (token, basestring):
bbcode.append (token.replace (u'[', u'\[').replace (u']', u'\]'))
continue
tag = token['tag'] # opening or closing simple tag. e.g: 'b', '/b', '/u', ...
tagOpener = (u'/' if tag[0] == u'/' else u'')
if (tagOpener == '/') or ('args' not in token):
bbcode.append (u'[' + tag + u']')
else:
# process args
argstr = ''
# the arg with the same name as the tag repersents the '=whatever'
if tag in token['args']:
if re.match ('\s|"', token['args'][tag]) is None:
argstr = u'=' + token['args'][tag]
else:
argstr = u'="' + token['args'][tag].replace (u'"', u'\"') + u'"'
for (k,v) in token['args'].iteritems():
if k == tag: # already processed
continue
argstr += ' ' + k + u'="' + v.replace (u'"', u'\"') + u'"'
bbcode.append (u'[' + tag + argstr + ']')
return u''.join (bbcode)
def html (self, allowClassAttr = False, doDeepCopy = True):
'''
Convert current parsed code to HTML
@allowClassAttr
Is something like [b class="asdf"] allowed?
@doDeepCopy
True: it does a deep copy of tokens so this list will remain unchanged
False: tokens will be modified internally, but the output will be produced like 5x faster
it's a good idea to use False when the string parsed is huge and this is the
last operation on the string
Example:
code = bbcode ('[b]bold[/b]')
code.html() -> '<b>bold</b>'
'''
from bbcode2html import bbcode2html
return bbcode2html.convertToHTML (self._tokens, allowClassAttr = allowClassAttr, doDeepCopy = doDeepCopy)
@staticmethod
def fixWrongTags (inTokenList):
''' Add missing tokens that have not been closed properly and try to fix some scenarios
'''
opened = []
outTokenList = []
for token in inTokenList:
# normal string... do nothing
if isinstance(token, basestring):
outTokenList.append (token)
else:
# if starts with '/' is closing a tag
if token['tag'][0] == '/':
while (len (opened) > 0) and (opened[-1] != token['tag'][1:]):
outTokenList.append ({'tag' : '/' + opened[-1] })
del opened[-1]
if len(opened):
del opened[-1]
outTokenList.append (token)
# opening tag
else:
# if I open the same tag I opened before, close it, and open it again
if (len(opened) > 0) and (token['tag'] == opened[-1]):
outTokenList.append ({'tag' : '/' + opened[-1] })
else:
opened.append (token['tag'])
outTokenList.append (token)
# close all elements that have not been closed
while len(opened):
outTokenList.append ({'tag' : '/' + opened[-1] })
del opened[-1]
return outTokenList
@staticmethod
def tokenize(code):
'''
Tokenize BBCode tags and parameters
Return the token list using a internal format. See the example:
[
{ 'tag' : 'p', 'args' : { 'font' : 'arial' } },
'This is ',
{ 'tag' : 'url', 'args' : {'url' : 'http://www.google.com'} },
'a link to google',
{ 'tag' : '/url' },
{ 'tag' : '/p' }
]
'''
re_tags = re.compile (r'(\[[^]]+\])', re.DOTALL | re.UNICODE)
re_tagName = re.compile (r'\[([^]=\s]+)([^]]*)\]', re.DOTALL | re.UNICODE)
#re_tagArgs = re.compile (r'\s*([^=]*)=(("([^"]+)")|([^\s]+))', re.DOTALL | re.UNICODE)
re_tagArgs = re.compile (r'\s*([\w]*)=(("([^"]+)")|([^\s]+))', re.DOTALL | re.UNICODE)
# get a unique name and replace escaped braces
unique = hashlib.md5(code).hexdigest()
code = code.replace ('\[', unique+'_OPEN_BRACE')
code = code.replace ('\]', unique+'_CLOSE_BRACE')
splitted = re_tags.split(code)
outTokenList = []
for token in splitted:
if len(token) == 0:
continue
if token[0] == '[':
match = re_tagName.match (token)
tagName = match.group(1)
tagArgs = match.group(2)
tagToken = { 'tag' : tagName.lower() }
# parse arguments (if any)
if len(tagArgs) > 0:
allArgs = re_tagArgs.findall(tagArgs)
tagArgs = {}
for arg in allArgs:
# if the argument has no name, use the tagName itself
argName = (arg[0] if arg[0] != '' else tagName)
argValue = (arg[3] if (arg[1][0] == '"') else arg[4])
tagArgs[argName.lower()] = argValue.replace ('\"', '"')
tagToken['args'] = tagArgs
outTokenList.append (tagToken)
# append the text as it is
else:
outTokenList.append (token)
# restore escaped braces back (once code is parsed)
restoredTokenList = []
for token in outTokenList:
if isinstance (token, basestring):
token = token.replace (unique+'_OPEN_BRACE', '[').replace (unique+'_CLOSE_BRACE', ']')
restoredTokenList.append (token)
return restoredTokenList
+81
View File
@@ -0,0 +1,81 @@
AUTHOR
Pau Sanchez
http://www.codigomanso.com/
VERSION:
bbcodeutils v1.0
LICENSE
This code is licensed under Creative Commons Attribution 3.0
http://creativecommons.org/licenses/by/3.0/
You can use this python module or any part of the code you want as long as you add
my name as a contributor to your project.
DESCRIPTION
This module can be used to produce HTML from BBCode, to generate BBCode or to fix invalid BBCode.
The classes are:
- bbcodeparser
- bbcodebuilder
- bbcode2html
You can use bbcodeparser to parse BBCode and to produce output in any format you want.
Open the python file to find more information and examples of use of each class. It can
be a good idea to check the test.py for examples
To run the unit tests:
> python test.py
To run the performance test:
> python test.py BBCodeTests.performanceTest
EXAMPLES OF BBCode:
[b] -> bold
[u] -> underline
[i] -> italic
[center] -> center the text inside
[color=XXX] -> change color of text
[size=XXX] -> change size of text
Lists:
[ul] -> unordered list
[ol] -> ordered list
[li] -> list item
[list] -> start unordered list
[*] -> list item
[list=1] -> start a list of numbers
[list=a] -> start a list of alphabetic characters
Advanced:
[url] -> link to url
[url=http://link/url/]text[/url]
[url link=http://link/url/ title="This is the title"]text[/url]
[img]http://to/image[/img]
[img=230x330]http://to/image[/img]
[img="Alt text here"]http://to/image[/img]
[img="Alt text here" width=320 height=240]http://to/image[/img]
[email]asdf@asdf.com[/email]
[email=john@asdf.com]John Smith[/email]
[google]search this[/google]
[wikipedia]Tom Hanks[/wikipedia]
[wikipedia lang=es]Tom Hanks[/wikipedia]
Tables:
[table]
[tr]
[th]
[td]
Advanced:
[google]
[wikipedia]
+420
View File
@@ -0,0 +1,420 @@
#!/usr/bin/python
# -*- coding: UTF-8 -*-
#
# Author: Pau Sanchez (contact@pausanchez.com)
# Version: v1.0
# Last Modified: 2010/09/15
#
# For the latest version check out:
# http://www.codigomanso.com/en/projects
#
# My blog:
# http://www.codigomanso.com/en/ - English Version
# http://www.codigomanso.com/es/ - Spanish Version
#
from bbcodeparser import bbcodeparser
from bbcodebuilder import bbcodebuilder
import random
import unittest
class BBCodeTests(unittest.TestCase):
def setUp (self):
self.bbcode = bbcodeparser()
return
def testConstructor (self):
self.assertEqual (bbcodeparser ('whatever').html(), 'whatever')
self.assertEqual (bbcodeparser ('[b]bold[/b]').html(), '<b>bold</b>')
self.assertEqual (str (bbcodeparser ('[b]bold[/b]')), '[b]bold[/b]')
return
def testBold (self):
self.assertEqual (self.bbcode.parse ('whatever').html(), 'whatever')
self.assertEqual (self.bbcode.parse ('[b]bold[/b]').html(), '<b>bold</b>')
self.assertEqual (self.bbcode.parse ('[B]bold[/b]').html(), '<b>bold</b>')
self.assertEqual (self.bbcode.parse ('this is [B]bold[/B]').html(), 'this is <b>bold</b>')
return
def testItalic (self):
self.assertEqual (self.bbcode.parse ('[i]italic[/i]').html(), '<i>italic</i>')
return
def testUnderline (self):
self.assertEqual (self.bbcode.parse ('[u]italic[/u]').html(), '<u>italic</u>')
return
def testURLs (self):
self.assertEqual (
self.bbcode.parse ('[url]http://www.google.com[/url]').html(),
'<a href="http://www.google.com">http://www.google.com</a>'
)
self.assertEqual (
self.bbcode.parse ('[url=http://www.google.com]Google[/url]').html(),
'<a href="http://www.google.com">Google</a>'
)
self.assertEqual (
self.bbcode.parse ('[url="http://www.google.com"]Google[/url]').html(),
'<a href="http://www.google.com">Google</a>'
)
self.assertEqual (
self.bbcode.parse ('[url="http://www.google.com" title="Search Engine"]Google[/url]').html(),
'<a href="http://www.google.com" title="Search Engine">Google</a>'
)
self.assertEqual (
self.bbcode.parse ('[url link="http://www.google.com"]Google[/url]').html(),
'<a href="http://www.google.com">Google</a>'
)
return
def testPTag (self):
self.assertEqual (
self.bbcode.parse ('[p color=#0000ff]blue[/p]').html(),
u'<p style="color: #0000ff;">blue</p>'
)
self.assertEqual (
self.bbcode.parse ('[p size=12]12pt font[/p]').html(),
u'<p style="font-size: 12pt;">12pt font</p>'
)
self.assertEqual (
self.bbcode.parse ('[p font=arial]arial[/p]').html(),
u'<p style="font-family: arial;">arial</p>'
)
self.assertEqual (
self.bbcode.parse ('[p font=arial color=blue size=14]blue 14pt arial').html(),
u'<p style="color: blue; font-size: 14pt; font-family: arial;">blue 14pt arial</p>'
)
self.assertEqual (
self.bbcode.parse ('[p class=whatever]text[/p]').html(),
u'<p>text</p>'
)
return
def testColorTag (self):
self.assertEqual (
self.bbcode.parse ('[color=#0000ff]blue[/color]').html(),
u'<span style="color: #0000ff;">blue</span>'
)
return
def testSizeTag (self):
self.assertEqual (
self.bbcode.parse ('[size=12]12pt font[/size]').html(),
u'<span style="font-size: 12pt;">12pt font</span>'
)
return
def testEmail(self):
self.assertEqual (
self.bbcode.parse ('[email]asdf@asdf.com[/email]').html(),
u'<a href="mailto:asdf@asdf.com">asdf@asdf.com</a>'
)
self.assertEqual (
self.bbcode.parse ('[email=john@smith.com]John Smith[/email]').html(),
u'<a href="mailto:john@smith.com">John Smith</a>'
)
return
def testImgTag (self):
self.assertEqual (
self.bbcode.parse ('[img]http://www.codigomanso.com/image.jpg[/img]').html(),
u'<img src="http://www.codigomanso.com/image.jpg" />'
)
self.assertEqual (
self.bbcode.parse ('[img="This is the ALT of the image"]http://www.codigomanso.com/image.jpg[/img]').html(),
u'<img alt="This is the ALT of the image" src="http://www.codigomanso.com/image.jpg" />'
)
self.assertEqual (
self.bbcode.parse ('[img=320x200]http://www.codigomanso.com/image.jpg[/img]').html(),
u'<img height="200" src="http://www.codigomanso.com/image.jpg" width="320" />'
)
self.assertEqual (
self.bbcode.parse ('[img=320x200 title="Image Test"]http://www.codigomanso.com/image.jpg[/img]').html(),
u'<img height="200" src="http://www.codigomanso.com/image.jpg" title="Image Test" width="320" />'
)
self.assertEqual (
self.bbcode.parse ('[img="whatever" width=320 height="212" title="Image Test"]http://www.codigomanso.com/image.jpg[/img]').html(),
u'<img alt="whatever" height="212" src="http://www.codigomanso.com/image.jpg" title="Image Test" width="320" />'
)
return
def testGoogleURL (self):
self.assertEqual (
self.bbcode.parse ('[google]asdf[/google]').html(),
u'<a href="http://www.google.com/search?q=asdf">asdf</a>'
)
self.assertEqual (
self.bbcode.parse ('[google]Tom Hanks[/google]').html(),
u'<a href="http://www.google.com/search?q=Tom+Hanks">Tom Hanks</a>'
)
return
def testWikipediaURL (self):
self.assertEqual (
self.bbcode.parse ('[wikipedia]Tom Hanks[/wikipedia]').html(),
u'<a href="http://www.wikipedia.org/wiki/Tom_Hanks">Tom Hanks</a>'
)
self.assertEqual (
self.bbcode.parse ('[wikipedia language=en]Tom Hanks[/wikipedia]').html(),
u'<a href="http://en.wikipedia.org/wiki/Tom_Hanks">Tom Hanks</a>'
)
self.assertEqual (
self.bbcode.parse ('[wikipedia lang=es]Tom Hanks[/wikipedia]').html(),
u'<a href="http://es.wikipedia.org/wiki/Tom_Hanks">Tom Hanks</a>'
)
self.assertEqual (
self.bbcode.parse ('[wikipedia=es]Tom Hanks[/wikipedia]').html(),
u'<a href="http://es.wikipedia.org/wiki/Tom_Hanks">Tom Hanks</a>'
)
return
def testListTags (self):
self.assertEqual (
self.bbcode.parse ('[ul][li]item 1[/li][li]item 2[/li][/ul]').html(),
u'<ul><li>item 1</li><li>item 2</li></ul>'
)
self.assertEqual (
self.bbcode.parse ('[ol][li]item 1[/li][li]item 2[/li][/ol]').html(),
u'<ol><li>item 1</li><li>item 2</li></ol>'
)
self.assertEqual (
self.bbcode.parse ('[list][li]item 1[/li][li]item 2[/li][/list]').html(),
u'<ul><li>item 1</li><li>item 2</li></ul>'
)
self.assertEqual (
self.bbcode.parse ('[list][*]item 1[*]item 2[/list]').html(),
u'<ul><li>item 1</li><li>item 2</li></ul>'
)
self.assertEqual (
self.bbcode.parse ('[list=1][li]item 1[/li][li]item 2[/li][/list]').html(),
u'<ol type="1"><li>item 1</li><li>item 2</li></ol>'
)
return
def testInvalidCode (self):
self.assertEqual (self.bbcode.parse ('[invalid]valid text[/invalid]').html(), 'valid text')
self.assertEqual (
self.bbcode.parse ('[b]bold and [i]italics[/b]').html(),
'<b>bold and <i>italics</i></b>'
)
self.assertEqual (
self.bbcode.parse ('[/b]invalid[/b][/p]').html(),
'invalid'
)
self.assertEqual (
self.bbcode.parse ('[p][b]bold').html(),
'<p><b>bold</b></p>'
)
self.assertEqual (
self.bbcode.parse ('[p][b]a <b>').html(),
'<p><b>a &lt;b&gt;</b></p>'
)
self.assertEqual (
self.bbcode.parse ('[ol][li]item 1[li]item 2[/li][/ol]').html(),
u'<ol><li>item 1</li><li>item 2</li></ol>'
)
self.assertEqual (
self.bbcode.parse ('[b]\[b\] stands for [b]bold[/b]').html(),
u'<b>[b] stands for </b><b>bold</b>'
)
return
def testEscapedBrackets (self):
self.assertEqual (
self.bbcode.parse ('\[b\]not bold\[/b\]').html(),
u'[b]not bold[/b]'
)
self.assertEqual (
self.bbcode.parse ('[b]\[b\] stands for bold[/b]').html(),
u'<b>[b] stands for bold</b>'
)
self.assertEqual (
self.bbcode.parse ('\[b\][b]stands for bold[/b]').html(),
u'[b]<b>stands for bold</b>'
)
self.assertEqual (
self.bbcode.parse ('\[b\][b]stands for bold[/b] just like <b> in HTML').html(),
u'[b]<b>stands for bold</b> just like &lt;b&gt; in HTML'
)
def testBigExample (self):
inputText = """check this out
[h1 class=circle]heading[/h1]
[p size=14 color=blue font="verdana, Times New Roman"]This is [b] bold [/b] and this [i]italic[/i] and this is [color=red]red[/color] and this is [color="red"]also red[/color].
[/p]
fix [b][i]bold [font=verdana][size=12]and[/size][/font] italic[/b]
[img]http://www.codigomanso.com/b.jpg[/img]
[url]http://www.codigomanso.com/[/url]
[url=http://www.codigomanso.com/]Codigo Manso[/url]
[uRl link=http://www.codigomanso.com title="Codigo Manso Blog"]Codigo Manso[/url]
[ul]
[Li]item 1[/Li]
[li]item 2[/LI]
[/UL]
[list=1 ]
[*]item 1
[*]item 2
[/list]
[table class="big"]
[tr]
[th]big[/th]
[/tr]
[/table]
[invalid class="extra"]whatever[/invalid]"""
out = self.bbcode.parse (inputText).html(allowClassAttr = True)
self.assertEquals (out, '''check this out
<h1 class="circle">heading</h1>
<p style="color: blue; font-size: 14pt; font-family: verdana, Times New Roman;">This is <b> bold </b> and this <i>italic</i> and this is <span style="color: red;">red</span> and this is <span style="color: red;">also red</span>.
</p>
fix <b><i>bold <span style="font-family: verdana;"><span style="font-size: 12pt;">and</span></span> italic</i></b>
<img src="http://www.codigomanso.com/b.jpg" />
<a href="http://www.codigomanso.com/">http://www.codigomanso.com/</a>
<a href="http://www.codigomanso.com/">Codigo Manso</a>
<a href="http://www.codigomanso.com" title="Codigo Manso Blog">Codigo Manso</a>
<ul>
<li>item 1</li>
<li>item 2</li>
</ul>
<ol type="1">
<li>item 1
</li><li>item 2
</li></ol>
<table class="big">
<tr>
<th>big</th>
</tr>
</table>
whatever''')
def testBBCodeDumper (self):
self.assertEquals (
self.bbcode.parse ('[b]bold[/b]').bbcode(),
'[b]bold[/b]'
)
self.assertEquals (
self.bbcode.parse ('[color=red]text in red[/color]').bbcode(),
'[color=red]text in red[/color]'
)
self.assertEquals (
self.bbcode.parse ('[p][color=red]text in red').bbcode(),
'[p][color=red]text in red[/color][/p]'
)
self.assertEquals (
self.bbcode.parse ('This [b][i]code[/b] will be fixed[/invalid]').bbcode(),
'This [b][i]code[/i][/b] will be fixed'
)
self.assertEquals (
self.bbcode.parse ('\[[url]http://www.codigomanso.com/en[/url]\]').bbcode(),
"\[[url]http://www.codigomanso.com/en[/url]\]"
)
def performanceTest(self):
'''
This test checks the performance of parse and html operations
To run this test type:
> python test.py BBCodeTests.performanceTest
'''
inputText = """check this out
[h1 class=circle]heading[/h1]
[p size=14 color=blue font="verdana, Times New Roman"]This is [b] bold [/b] and this [i]italic[/i] and this is [color=red]red[/color] and this is [color="red"]also red[/color].
[/p]
fix [b][i]bold [font=verdana][size=12]and[/size][/font] italic[/b]
[img]http://www.codigomanso.com/b.jpg[/img]
[url]http://www.codigomanso.com/[/url]
[url=http://www.codigomanso.com/]Codigo Manso[/url]
[uRl link=http://www.codigomanso.com title="Codigo Manso Blog"]Codigo Manso[/url]
[ul]
[Li]item 1[/Li]
[li]item 2[/LI]
[/UL]
[list=1 ]
[*]item 1
[*]item 2
[/list]
[table class="big"]
[tr]
[th]big[/th]
[/tr]
[/table]
[invalid class="extra"]whatever[/invalid]"""
import time
start = time.time()
for i in range(0, 12):
inputText += inputText
print "len(inputText) = %.2f MB (took %.2f seconds)" % (len(inputText)/(1024.0*1024.0), time.time() - start)
bbcode = bbcodeparser()
start = time.time()
bbcode.parse (inputText)
total = (time.time() - start)
print "time (bbcode.parse()) = %f" % total
print " >> %.2f chars/second" % (len(inputText) / total)
start = time.time()
bbcode.html(doDeepCopy = False)
total = (time.time() - start)
print "time (bbcode.html()) = %f" % total
print " >> %.2f chars/second" % (len(inputText) / total)
return
def testCodeBuilder (self):
bbcode = bbcodebuilder ()
self.assertEquals (bbcode.b ('bold'), u'[b]bold[/b]')
self.assertEquals (bbcode.color ('this goes in red', 'red'), u'[color=red]this goes in red[/color]')
self.assertEquals (bbcode.url ('Google', 'http://www.google.com'), u'[url=http://www.google.com]Google[/url]')
self.assertEquals (bbcode.alist('item 1', 'item 2'), u"[list=a]\n [*]item 1\n [*]item 2\n[/list]")
return
if __name__ == '__main__':
unittest.main()
+19 -6
View File
@@ -225,7 +225,7 @@ class Story:
def getMetadata(self, key, removeallentities=False, doreplacements=True):
value = None
if self.isList(key):
value = ', '.join(self.getList(key))
value = ', '.join(self.getList(key, removeallentities))
elif self.metadata.has_key(key):
value = self.metadata[key]
if value:
@@ -310,21 +310,34 @@ class Story:
return retlist
def getLists(self):
def getLists(self, removeallentities=False):
lsts = {}
for ln in self.listables.keys():
lsts[ln] = self.getList(ln)
lsts[ln] = self.getList(ln, removeallentities)
return lsts
def isList(self,listname):
return self.listables.has_key(listname)
def addChapter(self, title, html):
def addChapter(self, title, html, configurable=None):
if configurable and \
configurable.getConfig('strip_chapter_numbers') and \
configurable.getConfig('chapter_title_strip_pattern'):
title = re.sub(configurable.getConfig('chapter_title_strip_pattern'),"",title)
self.chapters.append( (title,html) )
def getChapters(self):
def getChapters(self, configurable=None):
"Chapters will be tuples of (title,html)"
return self.chapters
retval = []
if configurable and \
configurable.getConfig('add_chapter_numbers') and \
configurable.getConfig('chapter_title_add_pattern'):
for index, (title,html) in enumerate(self.chapters):
retval.append( (string.Template(configurable.getConfig('chapter_title_add_pattern')).substitute({'index':index+1,'title':title}),html) )
else:
retval = self.chapters
return retval
def formatFileName(self,template,allowunsafefilename=True):
values = origvalues = self.getAllMetadata()
+19 -10
View File
@@ -50,6 +50,7 @@ class BaseStoryWriter(Configurable):
'genre',
'language',
'characters',
'ships',
'series',
'status',
'datePublished',
@@ -81,6 +82,7 @@ class BaseStoryWriter(Configurable):
'status':'Status',
'series':'Series',
'characters':'Characters',
'ships':'Relationships',
'datePublished':'Published',
'dateUpdated':'Updated',
'dateCreated':'Packaged',
@@ -104,12 +106,19 @@ class BaseStoryWriter(Configurable):
}
self.story.setMetadata('formatname',self.getFormatName())
self.story.setMetadata('formatext',self.getFormatExt())
for tag in self.getConfigList("extratags"):
self.story.addToList("extratags",tag)
def getMetadata(self,key):
return stripHTML(self.story.getMetadata(key))
## Look for config parameter, split and add each to metadata field.
for (config,metadata) in [("extratags","extratags"),
("extracategories","category"),
("extragenres","genre"),
("extracharacters","characters"),
("extraships","ships"),
("extrawarnings","warnings")]:
for val in self.getConfigList(config):
self.story.addToList(metadata,val)
def getMetadata(self,key, removeallentities=False):
return stripHTML(self.story.getMetadata(key, removeallentities))
def getOutputFileName(self):
if self.getConfig('zip_output'):
@@ -175,10 +184,10 @@ class BaseStoryWriter(Configurable):
names as Story.metadata, but ENTRY should use index and chapter.
"""
# Only do TOC if there's more than one chapter and it's configured.
if len(self.story.getChapters()) > 1 and self.getConfig("include_tocpage") and not self.metaonly :
if len(self.story.getChapters(self)) > 1 and self.getConfig("include_tocpage") and not self.metaonly :
self._write(out,START.substitute(self.story.getAllMetadata()))
for index, (title,html) in enumerate(self.story.getChapters()):
for index, (title,html) in enumerate(self.story.getChapters(self)):
if html:
self._write(out,ENTRY.substitute({'chapter':title, 'index':"%04d"%(index+1)}))
@@ -252,7 +261,7 @@ class BaseStoryWriter(Configurable):
if close:
outstream.close()
def getTags(self):
def getTags(self, removeallentities=False):
# set to avoid duplicates subject tags.
subjectset = set()
@@ -265,10 +274,10 @@ class BaseStoryWriter(Configurable):
if entry in self.getConfigList("include_subject_tags") and \
entry not in self.story.getLists() and \
self.story.getMetadata(entry):
subjectset.add(self.getMetadata(entry))
subjectset.add(self.getMetadata(entry, removeallentities))
# listables all go into dc:subject tags, but only if they are configured.
for (name,lst) in self.story.getLists().iteritems():
for (name,lst) in self.story.getLists(removeallentities).iteritems():
if name in self.getConfigList("include_subject_tags"):
for tag in lst:
subjectset.add(tag)
+3 -3
View File
@@ -441,7 +441,7 @@ div { margin: 0pt; padding: 0pt; }
if self.getConfig("include_titlepage"):
items.append(("title_page","OEBPS/title_page.xhtml","application/xhtml+xml","Title Page"))
itemrefs.append("title_page")
if len(self.story.getChapters()) > 1 and self.getConfig("include_tocpage") and not self.metaonly :
if len(self.story.getChapters(self)) > 1 and self.getConfig("include_tocpage") and not self.metaonly :
items.append(("toc_page","OEBPS/toc_page.xhtml","application/xhtml+xml","Table of Contents"))
itemrefs.append("toc_page")
@@ -449,7 +449,7 @@ div { margin: 0pt; padding: 0pt; }
items.append(("log_page","OEBPS/log_page.xhtml","application/xhtml+xml","Update Log"))
itemrefs.append("log_page")
for index, (title,html) in enumerate(self.story.getChapters()):
for index, (title,html) in enumerate(self.story.getChapters(self)):
if html:
i=index+1
items.append(("file%04d"%i,
@@ -587,7 +587,7 @@ div { margin: 0pt; padding: 0pt; }
outputepub.writestr("OEBPS/log_page.xhtml",logpageIO.getvalue())
logpageIO.close()
for index, (title,html) in enumerate(self.story.getChapters()):
for index, (title,html) in enumerate(self.story.getChapters(self)):
if html:
logging.debug('Writing chapter text for: %s' % title)
fullhtml = self.EPUB_CHAPTER_START.substitute({'chapter':title, 'index':index+1}) + html + self.EPUB_CHAPTER_END.substitute({'chapter':title, 'index':index+1})
+1 -1
View File
@@ -94,7 +94,7 @@ ${output_css}
self.HTML_TOC_ENTRY,
self.HTML_TOC_PAGE_END)
for index, (title,html) in enumerate(self.story.getChapters()):
for index, (title,html) in enumerate(self.story.getChapters(self)):
if html:
logging.debug('Writing chapter text for: %s' % title)
self._write(out,self.HTML_CHAPTER_START.substitute({'chapter':title, 'index':"%04d"%(index+1)}))
+1 -1
View File
@@ -169,7 +169,7 @@ ${value}<br />
# files.append(tocpageIO.getvalue())
# tocpageIO.close()
for index, (title,html) in enumerate(self.story.getChapters()):
for index, (title,html) in enumerate(self.story.getChapters(self)):
if html:
logging.debug('Writing chapter text for: %s' % title)
fullhtml = self.MOBI_CHAPTER_START.substitute({'chapter':title, 'index':index+1}) + html + self.MOBI_CHAPTER_END.substitute({'chapter':title, 'index':index+1})
+1 -1
View File
@@ -133,7 +133,7 @@ End file.
self._write(out,self.lineends(self.wraplines(towrap)))
for index, (title,html) in enumerate(self.story.getChapters()):
for index, (title,html) in enumerate(self.story.getChapters(self)):
if html:
logging.debug('Writing chapter text for: %s' % title)
self._write(out,self.lineends(self.wraplines(removeAllEntities(self.TEXT_CHAPTER_START.substitute({'chapter':title, 'index':index+1})))))
+14 -6
View File
@@ -54,14 +54,17 @@
much easier. </p>
</div>
<!-- put announcements here, h3 is a good title size. -->
<h3>New Sites</h3>
<h3>New Site / Features / Fixes</h3>
<p>
Now supporing www.dracoandginny.com,
www.scarvesandcoffee.net, www.thepetulantpoetess.com,
www.wolverineandrogue.com and www.sinful-desire.com. Thanks, Ida!
New site merlinfic.dtwins.co.uk, thanks Ida.
</p>
<p>
FFDL now supports over 70 different fanfiction sites.
New feature options strip_chapter_numbers and add_chapter_numbers can be turned on to
normalize numbers in front of chapter titles.
</p>
<p>
Fixed BBCode in fimfiction.net story summaries. Also improved handling of html
in story summaries.
</p>
<p>
Questions? Check out our
@@ -71,7 +74,7 @@
If you have any problems with this application, please
report them in
the <a href="http://groups.google.com/group/fanfic-downloader">FanFictionDownLoader Google Group</a>. The
<a href="http://4-4-22.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
<a href="http://4-4-24.fanfictiondownloader.appspot.com">Previous Version</a> is also available for you to use if necessary.
</p>
<div id='error'>
{{ error_message }}
@@ -533,6 +536,11 @@
Use the URL of the story's chapter list, such as
<br /><a href="http://www.sinful-desire.org/archive/viewstory.php?sid=1234">http://www.sinful-desire.org/archive/viewstory.php?sid=1234</a>
</dd>
<dt>merlinfic.dtwins.co.uk</dt>
<dd>
Use the URL of the story's chapter list, such as
<br /><a href="http://merlinfic.dtwins.co.uk/viewstory.php?sid=1234">http://merlinfic.dtwins.co.uk/viewstory.php?sid=1234</a>
</dd>
</dl>
<p>
A few additional things to know, which will make your life substantially easier:
+236 -8
View File
@@ -42,6 +42,7 @@ category_label:Category
genre_label:Genre
language_label:Language
characters_label:Characters
ships_label:Relationships
series_label:Series
## Completed/In-Progress
status_label:Status
@@ -74,7 +75,7 @@ version_label:FFDL Version
## You can include extra text or HTML that will be included as-is in
## the title page. Eg: titlepage_entries: ...,<br />,summary,<br />,...
## All current formats already include title and author.
titlepage_entries: series,category,genre,language,characters,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,numWords,site,description
titlepage_entries: series,category,genre,language,characters,ships,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,numWords,site,description
## Try to collect series name and number of this story in series.
## Some sites (ab)use 'series' for reading lists and personal
@@ -100,11 +101,20 @@ include_tocpage: true
## entries to make epub subjects and calibre tags
## lastupdate creates two tags: "Last Update Year/Month: %Y/%m" and "Last Update: %Y/%m/%d"
include_subject_tags: extratags, genre, category, characters, status
include_subject_tags: extratags, genre, category, characters, ships, status
## extra tags (comma separated) to include, primarily for epub.
extratags: FanFiction
## extra categories, genres, characters, ships and warnings can be
## configured. Used primarily for sites that are dedicated to a genre
## or 'ship and so don't included it for every story.
#extracategories:
#extragenres:
#extracharacters:
#extraships:
#extrawarnings:
## number of seconds to sleep between calls to the story site. May by
## useful if pulling large numbers of stories or if the site is slow.
#slow_down_sleep_time:0.5
@@ -142,6 +152,45 @@ extratags: FanFiction
# ${category} => Buffy:? [tT]he Vampire Slayer => BuffyCover
# ${category} => Star Trek => StarTrekCover
## If set false, the summary will have all html stripped.
## Both this and include_images must be true to get images in the
## summary.
keep_summary_html:true
## Don't like the numbers at the start of chapter titles on some
## sites? You can use strip_chapter_numbers to strip them off. Just
## want to make them all look the same? Strip them off, then add them
## back on with add_chapter_numbers. Don't like the way it strips
## numbers or adds them back? See chapter_title_strip_pattern and
## chapter_title_add_pattern.
strip_chapter_numbers:false
add_chapter_numbers:false
## (Two versions of chapter_title_strip_pattern are shown below. You
## should only have one uncommented.)
## This version will remove the leading number from:
## "1." => ""
## "1. The Beginning" => "The Beginning"
## "1: Start" => "Start"
## "2, Chapter the second" => "Chapter the second"
## etc
chapter_title_strip_pattern:^[0-9]+[\.: -]+
## This version will strip all of the above *plus* remove 'Chapter 1':
## "Chapter 1" => ""
## "1. Chapter 1" => ""
## "1. Chapter 1, Bob's First Clue" => "Bob's First Clue"
## "Chapter 2 - Pirates Place" => "Pirates Place"
## etc
#chapter_title_strip_pattern:^([0-9]+[\.: -]+)?(Chapter *[0-9]+[\.:, -]*)?
## Uses a python template substitution. The ${index} is the 'chapter'
## number and ${title} is the chapter title, after applying
## chapter_title_strip_pattern. Those are the only variables available.
## "The Beginning" => "1. The Beginning"
chapter_title_add_pattern:${index}. ${title}
## Each output format has a section that overrides [defaults]
[html]
@@ -246,11 +295,6 @@ output_css:
## stories. Images will be converted to jpg for size if possible.
#include_images:false
## If not set, the summary will have all html stripped for safety.
## Both this and include_images must be true to get images in the
## summary.
#keep_summary_html:false
## If set, the first image found will be made the cover image. If
## keep_summary_html is true, any images in summary will be before any
## in chapters.
@@ -303,6 +347,11 @@ nook_img_fix:true
## URLs like: http://test1.com?sid=12345
[test1.com]
extratags: FanFiction,Testing
extracategories:Fafner
extragenres:Romance,Fluff
extracharacters:Reginald Smythe-Smythe,Mokona
extraships:Smythe-Smythe/Mokona
extrawarnings:Extreme Bogosity
## If necessary, you can define [<site>:<format>] sections to
## customize the formats differently for the same site. Overrides
@@ -329,6 +378,11 @@ extratags: FanFiction,Testing,HTML
#is_adult:true
[ashwinder.sycophanthex.com]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
extracharacters:Severus Snape,Hermione Granger
extraships:Severus Snape/Hermione Granger
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -337,6 +391,9 @@ extratags: FanFiction,Testing,HTML
#password:yourpassword
[castlefans.org]
## Site dedicated to these categories/characters/ships
extracategories:Castle
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -360,6 +417,11 @@ extratags: FanFiction,Testing,HTML
cover_exclusion_regexp:/images/.*?ribbon.gif
[dark-solace.org]
## Site dedicated to these categories/characters/ships
extracategories:Buffy: The Vampire Slayer
extracharacters:Buffy, Spike
extraships:Spike/Buffy
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -368,6 +430,11 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#password:yourpassword
[dramione.org]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
extracharacters:Draco Malfoy,Hermione Granger
extraships:Draco Malfoy/Hermione Granger
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -381,6 +448,9 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#is_adult:true
[erosnsappho.sycophanthex.com]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
@@ -391,6 +461,9 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
cover_exclusion_regexp:/images/.*?ribbon.gif
[fanfiction.mugglenet.com]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -404,6 +477,10 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#is_adult:true
[fanfiction.portkey.org]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
extraships:Harry Potter/Hermione Granger
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
@@ -426,6 +503,10 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#password:yourpassword
[grangerenchanted.com]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
extracharacters:Hermione Granger
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -439,26 +520,67 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#is_adult:true
[hlfiction.net]
## Site dedicated to these categories/characters/ships
extracategories:Highlander
[ksarchive.com]
## Site dedicated to these categories/characters/ships
extracategories:Star Trek
extracharacters:Kirk,Spock
extraships:Kirk/Spock
[lumos.sycophanthex.com]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[merlinfic.dtwins.co.uk]
## Site dedicated to these categories/characters/ships
extracategories:Merlin
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
## defaults.ini.
#username:YourName
#password:yourpassword
## Some sites also require the user to confirm they are adult for
## adult content. In commandline version, this should go in your
## personal.ini, not defaults.ini.
#is_adult:true
[national-library.net]
## Site dedicated to these categories/characters/ships
extracategories:West Wing
[ncisfic.com]
## Site dedicated to these categories/characters/ships
extracategories:NCIS
[nfacommunity.com]
## Site dedicated to these categories/characters/ships
extracategories:NCIS
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[nha.magical-worlds.us]
## Site dedicated to these categories/characters/ships
extracategories:Buffy: The Vampire Slayer
extracharacters:Willow
[occlumency.sycophanthex.com]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
extracharacters:Severus Snape
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -467,6 +589,9 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#password:yourpassword
[onedirectionfanfiction.com]
## Site dedicated to these categories/characters/ships
extracategories:One Direction
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -480,18 +605,28 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#is_adult:true
[ponyfictionarchive.net]
## Site dedicated to these categories/characters/ships
extracategories:My Little Pony: Friendship is Magic
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[pretendercentre.com]
## Site dedicated to these categories/characters/ships
extracategories:The Pretender
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[samdean.archive.nu]
## Site dedicated to these categories/characters/ships
extracategories:Supernatural
extracharacters:Sam,Dean
extraships:Sam/Dean
[sg1-heliopolis.com]
## Some sites do not require a login, but do require the user to
@@ -500,6 +635,8 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#is_adult:true
[stargate-atlantis.org]
## Site dedicated to these categories/characters/ships
extracategories:Stargate: Atlantis
[svufiction.com]
## Some sites require login (or login for some rated stories) The
@@ -515,6 +652,10 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#is_adult:true
[thehexfiles.net]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
extracharacters:Draco Malfoy,Harry Potter
extraships:Harry Potter/Draco Malfoy
[themasque.net]
## Some sites require login (or login for some rated stories) The
@@ -530,6 +671,9 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#is_adult:true
[thequidditchpitch.org]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -543,12 +687,20 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#is_adult:true
[www.adastrafanfic.com]
## Site dedicated to these categories/characters/ships
extracategories:Star Trek
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[www.dracoandginny.com]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
extracharacters:Draco Malfoy,Ginny Weasley
extraships:Draco Malfoy/Ginny Weasley
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -562,8 +714,13 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#is_adult:true
[www.thealphagate.com]
## Site dedicated to these categories/characters/ships
extracategories:Stargate: SG-1
[www.checkmated.com]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -578,6 +735,10 @@ cover_exclusion_regexp:/images/.*?ribbon.gif
#is_adult:true
[www.dokuga.com]
## Site dedicated to these categories/characters/ships
extracategories:InuYasha
extracharacters:Sesshoumaru,Kagome
extraships:Sesshoumaru/Kagome
[www.fanfiction.net]
## fanfiction.net's 'cover' images are really just tiny thumbnails.
@@ -595,6 +756,9 @@ never_make_cover: true
[www.ficbook.net]
[www.fictionalley.org]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
@@ -613,6 +777,9 @@ extratags:
#password:yourpassword
[www.fimfiction.net]
## Site dedicated to these categories/characters/ships
extracategories:My Little Pony: Friendship is Magic
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
@@ -624,20 +791,33 @@ extratags:
#fail_on_password: false
[www.harrypotterfanfiction.com]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[www.hpfandom.net]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[www.hpfanficarchive.com]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
[www.ik-eternal.net]
## Site dedicated to these categories/characters/ships
extracategories:InuYasha
extracharacters:InuYasha,Kagome
extraships:InuYasha/Kagome
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -651,6 +831,9 @@ extratags:
#is_adult:true
[www.libraryofmoria.com]
## Site dedicated to these categories/characters/ships
extracategories:Lord of the Rings
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
@@ -659,6 +842,9 @@ extratags:
[www.mediaminer.org]
[www.midnightwhispers.ca]
## Site dedicated to these categories/characters/ships
extracategories:Queer as Folk
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
@@ -669,8 +855,14 @@ extratags:
cover_exclusion_regexp:/stories/999/images/.*?_trophy.png
[www.ncisfiction.com]
## Site dedicated to these categories/characters/ships
extracategories:NCIS
[www.phoenixsong.net]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
extraships:Harry Potter/Ginny Weasley
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -679,28 +871,43 @@ cover_exclusion_regexp:/stories/999/images/.*?_trophy.png
#password:yourpassword
[www.potionsandsnitches.net]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
[www.prisonbreakfic.net]
## Site dedicated to these categories/characters/ships
extracategories:Prison Break
[www.scarvesandcoffee.net]
## Site dedicated to these categories/characters/ships
extracategories:Glee
extracharacters:Kurt Hummel,Blaine Anderson
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[www.sinful-desire.org]
## Site dedicated to these categories/characters/ships
extracategories:Supernatural
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[www.siye.co.uk]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
extracharacters:Harry Potter,Ginny Weasley
extraships:Harry Potter/Ginny Weasley
[www.squidge.org/peja]
# www.squidge.org/peja calls it Fandom <shrug>
category_label:Fandom
# Remove numWords -- www.squidge.org/peja word counts are inaccurate
titlepage_entries: series,category,genre,language,characters,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,site,description
titlepage_entries: series,category,genre,language,characters,ships,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,site,description
[www.squidge.org/peja:txt]
## Add URLs since there aren't links.
@@ -708,6 +915,8 @@ titlepage_entries: series,category,genre,language,characters,status,datePublishe
titlepage_entries: series,category,genre,language,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,site,storyUrl, authorUrl, description
[www.storiesofarda.com]
## Site dedicated to these categories/characters/ships
extracategories:Lord of the Rings
[www.thepetulantpoetess.com]
## Some sites require login (or login for some rated stories) The
@@ -747,8 +956,13 @@ collect_series: false
#password:yourpassword
[www.twilightarchives.com]
## Site dedicated to these categories/characters/ships
extracategories:Twilight
[www.twilighted.net]
## Site dedicated to these categories/characters/ships
extracategories:Twilight
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -760,6 +974,9 @@ collect_series: false
collect_series: false
[www.twiwrite.net]
## Site dedicated to these categories/characters/ships
extracategories:Twilight
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
@@ -771,6 +988,11 @@ collect_series: false
collect_series: false
[www.walkingtheplank.org]
## Site dedicated to these categories/characters/ships
extracategories:Harry Potter
extracharacters:Severus Snape,Harry Potter
extraships:Severus Snape/Harry Potter
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
@@ -792,8 +1014,14 @@ collect_series: false
#is_adult:true
[www.wolverineandrogue.com]
## Site dedicated to these categories/characters/ships
extracategories:X-Men Movie
extracharacters:Wolverine,Rogue
[www.wraithbait.com]
## Site dedicated to these categories/characters/ships
extracategories:Stargate: Atlantis
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
+18 -14
View File
@@ -1,13 +1,25 @@
## This is an example of what your personal configuration might look
## like.
## like. Uncomment options by removing the '#' in front of them.
[defaults]
## Some sites also require the user to confirm they are adult for
## adult content.
## adult content. Uncomment by removing '#' in front of is_adult.
#is_adult:true
## include images from img tags in the body and summary of
## stories. Images will be converted to jpg for size if possible.
## Don't like the numbers at the start of chapter titles on some
## sites? You can use strip_chapter_numbers to strip them off. Just
## want to make them all look the same? Strip them off, then add them
## back on with add_chapter_numbers. Don't like the way it strips
## numbers or adds them back? See chapter_title_strip_pattern and
## chapter_title_add_pattern.
#strip_chapter_numbers:true
#add_chapter_numbers:true
[epub]
## include images from img tags in the body and summary of stories.
## Images will be converted to jpg for size if possible. Images work
## in epub format only. To get mobi or other format with images,
## download as epub and use Calibre to convert.
#include_images:true
## If not set, the summary will have all html stripped for safety.
@@ -28,18 +40,10 @@
## space.
#grayscale_images: false
## Try to collect series name and number of this story in series.
## Some sites (ab)use 'series' for reading lists and personal
## collections. This lets us turn it on and off by site without
## keeping a lengthy titlepage_entries per site and prevents it
## updating in the plugin.
## Turn off in [defaults] or [overrides] to prevent all sites from
## updating series column.
## default is true
#collect_series: false
## Most common, I expect will be using this to save username/passwords
## for different sites.
## for different sites. Here are a few examples.
[www.twilighted.net]
#username:YourPenname
#password:YourPassword
+18
View File
@@ -1,3 +1,21 @@
# -*- 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.
#
# Other code contributed by Pau Sanchez(bbcodeutils).
To use, do:
python downloader.py [-f (epub|html|txt)] <url>