From 59ae4fe374ba2232f39cda08f530ab7d41d7ebd7 Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Sun, 28 Dec 2014 17:21:39 -0600 Subject: [PATCH] Add universe & crossoverfandom site-specific to ksarchive, clean up ini files. --- allrecent.html | 78 + app.yaml | 46 + bs4/COPYING.txt | 26 + bs4/__init__.py | 406 ++ bs4/builder/__init__.py | 321 ++ bs4/builder/_html5lib.py | 285 ++ bs4/builder/_htmlparser.py | 258 + bs4/builder/_lxml.py | 233 + bs4/dammit.py | 829 ++++ bs4/diagnose.py | 204 + bs4/element.py | 1611 ++++++ calibre-plugin/__init__.py | 131 + calibre-plugin/about.txt | 28 + calibre-plugin/common_utils.py | 553 +++ calibre-plugin/config.py | 1100 +++++ calibre-plugin/dialogs.py | 1169 +++++ calibre-plugin/ffdl_plugin.py | 2125 ++++++++ calibre-plugin/ffdl_util.py | 43 + calibre-plugin/images/icon.png | Bin 0 -> 24649 bytes calibre-plugin/images/icon.xcf | Bin 0 -> 63927 bytes calibre-plugin/jobs.py | 244 + ...mport-name-fanfictiondownloader_plugin.txt | 0 calibre-plugin/prefs.py | 151 + calibre-plugin/translations/de.po | 1674 +++++++ calibre-plugin/translations/es.po | 1673 +++++++ calibre-plugin/translations/fr.po | 1672 +++++++ calibre-plugin/translations/messages.pot | 1567 ++++++ calibre-plugin/translations/pt_BR.po | 1672 +++++++ calibre-plugin/translations/zz.po | 1867 +++++++ chardet/__init__.py | 32 + chardet/big5freq.py | 925 ++++ chardet/big5prober.py | 42 + chardet/chardetect.py | 80 + chardet/chardistribution.py | 231 + chardet/charsetgroupprober.py | 106 + chardet/charsetprober.py | 62 + chardet/codingstatemachine.py | 61 + chardet/compat.py | 34 + chardet/constants.py | 39 + chardet/cp949prober.py | 44 + chardet/escprober.py | 86 + chardet/escsm.py | 242 + chardet/eucjpprober.py | 90 + chardet/euckrfreq.py | 596 +++ chardet/euckrprober.py | 42 + chardet/euctwfreq.py | 428 ++ chardet/euctwprober.py | 41 + chardet/gb2312freq.py | 472 ++ chardet/gb2312prober.py | 41 + chardet/hebrewprober.py | 283 ++ chardet/jisfreq.py | 569 +++ chardet/jpcntx.py | 227 + chardet/langbulgarianmodel.py | 229 + chardet/langcyrillicmodel.py | 329 ++ chardet/langgreekmodel.py | 225 + chardet/langhebrewmodel.py | 201 + chardet/langhungarianmodel.py | 225 + chardet/langthaimodel.py | 200 + chardet/latin1prober.py | 139 + chardet/mbcharsetprober.py | 86 + chardet/mbcsgroupprober.py | 54 + chardet/mbcssm.py | 572 +++ chardet/sbcharsetprober.py | 120 + chardet/sbcsgroupprober.py | 69 + chardet/sjisprober.py | 91 + chardet/universaldetector.py | 170 + chardet/utf8prober.py | 76 + cron.yaml | 10 + css/index.css | 73 + defaults.ini | 1992 ++++++++ delete_fic.py | 59 + downloader.py | 302 ++ editconfig.html | 89 + epubmerge.py | 25 + example.ini | 103 + fanficdownloader.zip | Bin 0 -> 863236 bytes fanficdownloader/BeautifulSoup.py | 2017 ++++++++ fanficdownloader/HtmlTagStack.py | 57 + fanficdownloader/__init__.py | 19 + fanficdownloader/adapters/__init__.py | 254 + .../adapters/adapter_adastrafanficcom.py | 238 + .../adapters/adapter_archiveofourownorg.py | 392 ++ .../adapters/adapter_archiveskyehawkecom.py | 191 + .../adapter_ashwindersycophanthexcom.py | 254 + .../adapters/adapter_asr3slashzoneorg.py | 227 + .../adapters/adapter_bdsmgeschichten.py | 346 ++ .../adapters/adapter_bloodshedversecom.py | 193 + .../adapters/adapter_bloodtiesfancom.py | 337 ++ .../adapters/adapter_buffynfaithnet.py | 296 ++ .../adapters/adapter_chaossycophanthexcom.py | 238 + .../adapters/adapter_checkmatedcom.py | 238 + .../adapters/adapter_csiforensicscom.py | 237 + .../adapters/adapter_darksolaceorg.py | 336 ++ .../adapters/adapter_destinysgatewaycom.py | 244 + .../adapters/adapter_dokugacom.py | 278 ++ .../adapters/adapter_dotmoonnet.py | 217 + .../adapters/adapter_dracoandginnycom.py | 302 ++ .../adapters/adapter_dramioneorg.py | 311 ++ .../adapters/adapter_efictionestelielde.py | 224 + .../adapters/adapter_efpfanficnet.py | 316 ++ .../adapter_erosnsapphosycophanthexcom.py | 256 + .../adapters/adapter_fanficcastletvnet.py | 322 ++ fanficdownloader/adapters/adapter_fanfichu.py | 185 + .../adapters/adapter_fanfictioncsodaidokhu.py | 218 + .../adapters/adapter_fanfictionjunkiesde.py | 291 ++ .../adapters/adapter_fanfictionnet.py | 341 ++ .../adapters/adapter_fanfiktionde.py | 206 + .../adapters/adapter_fannation.py | 44 + .../adapters/adapter_fhsarchivecom.py | 57 + .../adapters/adapter_ficbooknet.py | 226 + .../adapters/adapter_fictionalleyorg.py | 241 + .../adapters/adapter_fictionmaniatv.py | 166 + .../adapters/adapter_fictionpadcom.py | 194 + .../adapters/adapter_fictionpresscom.py | 51 + .../adapters/adapter_ficwadcom.py | 233 + .../adapters/adapter_fimfictionnet.py | 338 ++ .../adapters/adapter_finestoriescom.py | 288 ++ .../adapters/adapter_grangerenchantedcom.py | 311 ++ .../adapter_harrypotterfanfictioncom.py | 203 + .../adapters/adapter_hennethannunnet.py | 172 + .../adapters/adapter_hlfictionnet.py | 232 + .../adapters/adapter_hpfandomnet.py | 233 + .../adapters/adapter_hpfanficarchivecom.py | 224 + .../adapters/adapter_iketernalnet.py | 283 ++ .../adapters/adapter_imagineeficcom.py | 290 ++ .../adapters/adapter_indeathnet.py | 200 + .../adapters/adapter_ksarchivecom.py | 329 ++ .../adapters/adapter_libraryofmoriacom.py | 251 + .../adapters/adapter_literotica.py | 267 + .../adapters/adapter_lotrfanfictioncom.py | 36 + .../adapters/adapter_lumossycophanthexcom.py | 238 + .../adapters/adapter_mediaminerorg.py | 237 + .../adapters/adapter_merlinficdtwinscouk.py | 294 ++ .../adapters/adapter_midnightwhispersca.py | 290 ++ .../adapters/adapter_mugglenetcom.py | 336 ++ .../adapters/adapter_nationallibrarynet.py | 213 + .../adapters/adapter_ncisficcom.py | 219 + .../adapters/adapter_ncisfictionnet.py | 210 + .../adapters/adapter_netraptororg.py | 213 + .../adapters/adapter_nfacommunitycom.py | 290 ++ .../adapters/adapter_nhamagicalworldsus.py | 237 + .../adapters/adapter_nickandgregnet.py | 177 + .../adapters/adapter_nocturnallightnet.py | 177 + .../adapter_occlumencysycophanthexcom.py | 263 + .../adapter_onedirectionfanfictioncom.py | 270 + .../adapters/adapter_phoenixsongnet.py | 240 + .../adapters/adapter_pommedesangcom.py | 301 ++ .../adapters/adapter_ponyfictionarchivenet.py | 249 + .../adapters/adapter_portkeyorg.py | 282 ++ .../adapters/adapter_potionsandsnitchesnet.py | 211 + .../adapters/adapter_potterficscom.py | 281 ++ .../adapter_potterheadsanonymouscom.py | 299 ++ .../adapters/adapter_pretendercentrecom.py | 254 + .../adapters/adapter_psychficcom.py | 249 + .../adapters/adapter_qafficcom.py | 265 + .../adapters/adapter_restrictedsectionorg.py | 265 + .../adapters/adapter_samandjacknet.py | 342 ++ .../adapters/adapter_samdeanarchivenu.py | 233 + .../adapters/adapter_scarheadnet.py | 300 ++ .../adapters/adapter_scarvesandcoffeenet.py | 248 + .../adapters/adapter_sg1heliopoliscom.py | 259 + .../adapters/adapter_sheppardweircom.py | 317 ++ .../adapters/adapter_simplyundeniablecom.py | 220 + .../adapters/adapter_sinfuldesireorg.py | 252 + fanficdownloader/adapters/adapter_siyecouk.py | 247 + .../adapters/adapter_spikeluvercom.py | 208 + .../adapters/adapter_squidgeorgpeja.py | 256 + .../adapters/adapter_stargateatlantisorg.py | 230 + .../adapters/adapter_storiesofardacom.py | 161 + .../adapters/adapter_storiesonlinenet.py | 422 ++ .../adapters/adapter_tenhawkpresentscom.py | 257 + fanficdownloader/adapters/adapter_test1.py | 367 ++ .../adapters/adapter_thealphagatecom.py | 215 + .../adapters/adapter_thehexfilesnet.py | 207 + .../adapters/adapter_thehookupzonenet.py | 309 ++ .../adapters/adapter_themaplebookshelf.py | 40 + .../adapters/adapter_themasquenet.py | 274 ++ .../adapters/adapter_thepetulantpoetesscom.py | 242 + .../adapters/adapter_thequidditchpitchorg.py | 292 ++ .../adapters/adapter_tokrafandomnetcom.py | 238 + .../adapters/adapter_tolkienfanfiction.py | 238 + .../adapters/adapter_trekiverseorg.py | 325 ++ .../adapters/adapter_tthfanficorg.py | 310 ++ .../adapters/adapter_twcslibrarynet.py | 272 + .../adapters/adapter_twilightarchivescom.py | 188 + .../adapters/adapter_twilightednet.py | 253 + .../adapters/adapter_twiwritenet.py | 281 ++ .../adapters/adapter_voracity2eficcom.py | 233 + .../adapters/adapter_walkingtheplankorg.py | 232 + .../adapters/adapter_whoficcom.py | 238 + .../adapters/adapter_wizardtalesnet.py | 303 ++ .../adapters/adapter_wolverineandroguecom.py | 219 + .../adapters/adapter_wraithbaitcom.py | 234 + fanficdownloader/adapters/base_adapter.py | 605 +++ .../adapters/base_efiction_adapter.py | 426 ++ fanficdownloader/configurable.py | 196 + fanficdownloader/epubutils.py | 194 + fanficdownloader/exceptions.py | 86 + fanficdownloader/geturls.py | 175 + fanficdownloader/gziphttp.py | 38 + fanficdownloader/html.py | 126 + fanficdownloader/html2text.py | 453 ++ fanficdownloader/htmlcleanup.py | 485 ++ fanficdownloader/htmlheuristics.py | 348 ++ fanficdownloader/mobi.py | 386 ++ fanficdownloader/story.py | 873 ++++ fanficdownloader/translit.py | 57 + fanficdownloader/writers/__init__.py | 38 + fanficdownloader/writers/base_writer.py | 285 ++ fanficdownloader/writers/writer_epub.py | 690 +++ fanficdownloader/writers/writer_html.py | 144 + fanficdownloader/writers/writer_mobi.py | 197 + fanficdownloader/writers/writer_txt.py | 190 + ffstorage.py | 63 + html5lib/LICENSE | 20 + html5lib/__init__.py | 23 + html5lib/constants.py | 3104 ++++++++++++ html5lib/filters/__init__.py | 0 html5lib/filters/_base.py | 12 + html5lib/filters/alphabeticalattributes.py | 20 + html5lib/filters/inject_meta_charset.py | 65 + html5lib/filters/lint.py | 93 + html5lib/filters/optionaltags.py | 205 + html5lib/filters/sanitizer.py | 12 + html5lib/filters/whitespace.py | 38 + html5lib/html5parser.py | 2713 ++++++++++ html5lib/ihatexml.py | 285 ++ html5lib/inputstream.py | 886 ++++ html5lib/sanitizer.py | 271 + html5lib/serializer/__init__.py | 16 + html5lib/serializer/htmlserializer.py | 320 ++ html5lib/tokenizer.py | 1731 +++++++ html5lib/treeadapters/__init__.py | 0 html5lib/treeadapters/sax.py | 44 + html5lib/treebuilders/__init__.py | 76 + html5lib/treebuilders/_base.py | 377 ++ html5lib/treebuilders/dom.py | 227 + html5lib/treebuilders/etree.py | 337 ++ html5lib/treebuilders/etree_lxml.py | 369 ++ html5lib/treewalkers/__init__.py | 57 + html5lib/treewalkers/_base.py | 200 + html5lib/treewalkers/dom.py | 46 + html5lib/treewalkers/etree.py | 138 + html5lib/treewalkers/genshistream.py | 69 + html5lib/treewalkers/lxmletree.py | 204 + html5lib/treewalkers/pulldom.py | 63 + html5lib/trie/__init__.py | 12 + html5lib/trie/_base.py | 37 + html5lib/trie/datrie.py | 44 + html5lib/trie/py.py | 67 + html5lib/utils.py | 82 + index-ajax.html | 109 + index.html | 213 + index.yaml | 28 + js/fdownloader.js | 116 + js/jquery-1.3.2.js | 4376 +++++++++++++++++ login.html | 118 + main.py | 639 +++ makeplugin.py | 39 + makezip.py | 52 + plugin-defaults.ini | 1992 ++++++++ plugin-example.ini | 122 + queue.yaml | 7 + readme.txt | 37 + recent.html | 85 + settings.py | 25 + six.py | 762 +++ static/ajax-loader.gif | Bin 0 -> 10819 bytes static/favicon.ico | Bin 0 -> 21792 bytes status.html | 94 + utils/__init__.py | 1 + utils/remover.py | 109 + utils/tally.py | 64 + 273 files changed, 87956 insertions(+) create mode 100644 allrecent.html create mode 100644 app.yaml create mode 100644 bs4/COPYING.txt create mode 100644 bs4/__init__.py create mode 100644 bs4/builder/__init__.py create mode 100644 bs4/builder/_html5lib.py create mode 100644 bs4/builder/_htmlparser.py create mode 100644 bs4/builder/_lxml.py create mode 100644 bs4/dammit.py create mode 100644 bs4/diagnose.py create mode 100644 bs4/element.py create mode 100644 calibre-plugin/__init__.py create mode 100644 calibre-plugin/about.txt create mode 100644 calibre-plugin/common_utils.py create mode 100644 calibre-plugin/config.py create mode 100644 calibre-plugin/dialogs.py create mode 100644 calibre-plugin/ffdl_plugin.py create mode 100644 calibre-plugin/ffdl_util.py create mode 100644 calibre-plugin/images/icon.png create mode 100644 calibre-plugin/images/icon.xcf create mode 100644 calibre-plugin/jobs.py create mode 100644 calibre-plugin/plugin-import-name-fanfictiondownloader_plugin.txt create mode 100644 calibre-plugin/prefs.py create mode 100644 calibre-plugin/translations/de.po create mode 100644 calibre-plugin/translations/es.po create mode 100644 calibre-plugin/translations/fr.po create mode 100644 calibre-plugin/translations/messages.pot create mode 100644 calibre-plugin/translations/pt_BR.po create mode 100644 calibre-plugin/translations/zz.po create mode 100644 chardet/__init__.py create mode 100644 chardet/big5freq.py create mode 100644 chardet/big5prober.py create mode 100644 chardet/chardetect.py create mode 100644 chardet/chardistribution.py create mode 100644 chardet/charsetgroupprober.py create mode 100644 chardet/charsetprober.py create mode 100644 chardet/codingstatemachine.py create mode 100644 chardet/compat.py create mode 100644 chardet/constants.py create mode 100644 chardet/cp949prober.py create mode 100644 chardet/escprober.py create mode 100644 chardet/escsm.py create mode 100644 chardet/eucjpprober.py create mode 100644 chardet/euckrfreq.py create mode 100644 chardet/euckrprober.py create mode 100644 chardet/euctwfreq.py create mode 100644 chardet/euctwprober.py create mode 100644 chardet/gb2312freq.py create mode 100644 chardet/gb2312prober.py create mode 100644 chardet/hebrewprober.py create mode 100644 chardet/jisfreq.py create mode 100644 chardet/jpcntx.py create mode 100644 chardet/langbulgarianmodel.py create mode 100644 chardet/langcyrillicmodel.py create mode 100644 chardet/langgreekmodel.py create mode 100644 chardet/langhebrewmodel.py create mode 100644 chardet/langhungarianmodel.py create mode 100644 chardet/langthaimodel.py create mode 100644 chardet/latin1prober.py create mode 100644 chardet/mbcharsetprober.py create mode 100644 chardet/mbcsgroupprober.py create mode 100644 chardet/mbcssm.py create mode 100644 chardet/sbcharsetprober.py create mode 100644 chardet/sbcsgroupprober.py create mode 100644 chardet/sjisprober.py create mode 100644 chardet/universaldetector.py create mode 100644 chardet/utf8prober.py create mode 100644 cron.yaml create mode 100644 css/index.css create mode 100644 defaults.ini create mode 100644 delete_fic.py create mode 100644 downloader.py create mode 100644 editconfig.html create mode 100644 epubmerge.py create mode 100644 example.ini create mode 100644 fanficdownloader.zip create mode 100644 fanficdownloader/BeautifulSoup.py create mode 100644 fanficdownloader/HtmlTagStack.py create mode 100644 fanficdownloader/__init__.py create mode 100644 fanficdownloader/adapters/__init__.py create mode 100644 fanficdownloader/adapters/adapter_adastrafanficcom.py create mode 100644 fanficdownloader/adapters/adapter_archiveofourownorg.py create mode 100644 fanficdownloader/adapters/adapter_archiveskyehawkecom.py create mode 100644 fanficdownloader/adapters/adapter_ashwindersycophanthexcom.py create mode 100644 fanficdownloader/adapters/adapter_asr3slashzoneorg.py create mode 100644 fanficdownloader/adapters/adapter_bdsmgeschichten.py create mode 100644 fanficdownloader/adapters/adapter_bloodshedversecom.py create mode 100644 fanficdownloader/adapters/adapter_bloodtiesfancom.py create mode 100644 fanficdownloader/adapters/adapter_buffynfaithnet.py create mode 100644 fanficdownloader/adapters/adapter_chaossycophanthexcom.py create mode 100644 fanficdownloader/adapters/adapter_checkmatedcom.py create mode 100644 fanficdownloader/adapters/adapter_csiforensicscom.py create mode 100644 fanficdownloader/adapters/adapter_darksolaceorg.py create mode 100644 fanficdownloader/adapters/adapter_destinysgatewaycom.py create mode 100644 fanficdownloader/adapters/adapter_dokugacom.py create mode 100644 fanficdownloader/adapters/adapter_dotmoonnet.py create mode 100644 fanficdownloader/adapters/adapter_dracoandginnycom.py create mode 100644 fanficdownloader/adapters/adapter_dramioneorg.py create mode 100644 fanficdownloader/adapters/adapter_efictionestelielde.py create mode 100644 fanficdownloader/adapters/adapter_efpfanficnet.py create mode 100644 fanficdownloader/adapters/adapter_erosnsapphosycophanthexcom.py create mode 100644 fanficdownloader/adapters/adapter_fanficcastletvnet.py create mode 100644 fanficdownloader/adapters/adapter_fanfichu.py create mode 100644 fanficdownloader/adapters/adapter_fanfictioncsodaidokhu.py create mode 100644 fanficdownloader/adapters/adapter_fanfictionjunkiesde.py create mode 100644 fanficdownloader/adapters/adapter_fanfictionnet.py create mode 100644 fanficdownloader/adapters/adapter_fanfiktionde.py create mode 100644 fanficdownloader/adapters/adapter_fannation.py create mode 100644 fanficdownloader/adapters/adapter_fhsarchivecom.py create mode 100644 fanficdownloader/adapters/adapter_ficbooknet.py create mode 100644 fanficdownloader/adapters/adapter_fictionalleyorg.py create mode 100644 fanficdownloader/adapters/adapter_fictionmaniatv.py create mode 100644 fanficdownloader/adapters/adapter_fictionpadcom.py create mode 100644 fanficdownloader/adapters/adapter_fictionpresscom.py create mode 100644 fanficdownloader/adapters/adapter_ficwadcom.py create mode 100644 fanficdownloader/adapters/adapter_fimfictionnet.py create mode 100644 fanficdownloader/adapters/adapter_finestoriescom.py create mode 100644 fanficdownloader/adapters/adapter_grangerenchantedcom.py create mode 100644 fanficdownloader/adapters/adapter_harrypotterfanfictioncom.py create mode 100644 fanficdownloader/adapters/adapter_hennethannunnet.py create mode 100644 fanficdownloader/adapters/adapter_hlfictionnet.py create mode 100644 fanficdownloader/adapters/adapter_hpfandomnet.py create mode 100644 fanficdownloader/adapters/adapter_hpfanficarchivecom.py create mode 100644 fanficdownloader/adapters/adapter_iketernalnet.py create mode 100644 fanficdownloader/adapters/adapter_imagineeficcom.py create mode 100644 fanficdownloader/adapters/adapter_indeathnet.py create mode 100644 fanficdownloader/adapters/adapter_ksarchivecom.py create mode 100644 fanficdownloader/adapters/adapter_libraryofmoriacom.py create mode 100644 fanficdownloader/adapters/adapter_literotica.py create mode 100644 fanficdownloader/adapters/adapter_lotrfanfictioncom.py create mode 100644 fanficdownloader/adapters/adapter_lumossycophanthexcom.py create mode 100644 fanficdownloader/adapters/adapter_mediaminerorg.py create mode 100644 fanficdownloader/adapters/adapter_merlinficdtwinscouk.py create mode 100644 fanficdownloader/adapters/adapter_midnightwhispersca.py create mode 100644 fanficdownloader/adapters/adapter_mugglenetcom.py create mode 100644 fanficdownloader/adapters/adapter_nationallibrarynet.py create mode 100644 fanficdownloader/adapters/adapter_ncisficcom.py create mode 100644 fanficdownloader/adapters/adapter_ncisfictionnet.py create mode 100644 fanficdownloader/adapters/adapter_netraptororg.py create mode 100644 fanficdownloader/adapters/adapter_nfacommunitycom.py create mode 100644 fanficdownloader/adapters/adapter_nhamagicalworldsus.py create mode 100644 fanficdownloader/adapters/adapter_nickandgregnet.py create mode 100644 fanficdownloader/adapters/adapter_nocturnallightnet.py create mode 100644 fanficdownloader/adapters/adapter_occlumencysycophanthexcom.py create mode 100644 fanficdownloader/adapters/adapter_onedirectionfanfictioncom.py create mode 100644 fanficdownloader/adapters/adapter_phoenixsongnet.py create mode 100644 fanficdownloader/adapters/adapter_pommedesangcom.py create mode 100644 fanficdownloader/adapters/adapter_ponyfictionarchivenet.py create mode 100644 fanficdownloader/adapters/adapter_portkeyorg.py create mode 100644 fanficdownloader/adapters/adapter_potionsandsnitchesnet.py create mode 100644 fanficdownloader/adapters/adapter_potterficscom.py create mode 100644 fanficdownloader/adapters/adapter_potterheadsanonymouscom.py create mode 100644 fanficdownloader/adapters/adapter_pretendercentrecom.py create mode 100644 fanficdownloader/adapters/adapter_psychficcom.py create mode 100644 fanficdownloader/adapters/adapter_qafficcom.py create mode 100644 fanficdownloader/adapters/adapter_restrictedsectionorg.py create mode 100644 fanficdownloader/adapters/adapter_samandjacknet.py create mode 100644 fanficdownloader/adapters/adapter_samdeanarchivenu.py create mode 100644 fanficdownloader/adapters/adapter_scarheadnet.py create mode 100644 fanficdownloader/adapters/adapter_scarvesandcoffeenet.py create mode 100644 fanficdownloader/adapters/adapter_sg1heliopoliscom.py create mode 100644 fanficdownloader/adapters/adapter_sheppardweircom.py create mode 100644 fanficdownloader/adapters/adapter_simplyundeniablecom.py create mode 100644 fanficdownloader/adapters/adapter_sinfuldesireorg.py create mode 100644 fanficdownloader/adapters/adapter_siyecouk.py create mode 100644 fanficdownloader/adapters/adapter_spikeluvercom.py create mode 100644 fanficdownloader/adapters/adapter_squidgeorgpeja.py create mode 100644 fanficdownloader/adapters/adapter_stargateatlantisorg.py create mode 100644 fanficdownloader/adapters/adapter_storiesofardacom.py create mode 100644 fanficdownloader/adapters/adapter_storiesonlinenet.py create mode 100644 fanficdownloader/adapters/adapter_tenhawkpresentscom.py create mode 100644 fanficdownloader/adapters/adapter_test1.py create mode 100644 fanficdownloader/adapters/adapter_thealphagatecom.py create mode 100644 fanficdownloader/adapters/adapter_thehexfilesnet.py create mode 100644 fanficdownloader/adapters/adapter_thehookupzonenet.py create mode 100644 fanficdownloader/adapters/adapter_themaplebookshelf.py create mode 100644 fanficdownloader/adapters/adapter_themasquenet.py create mode 100644 fanficdownloader/adapters/adapter_thepetulantpoetesscom.py create mode 100644 fanficdownloader/adapters/adapter_thequidditchpitchorg.py create mode 100644 fanficdownloader/adapters/adapter_tokrafandomnetcom.py create mode 100644 fanficdownloader/adapters/adapter_tolkienfanfiction.py create mode 100644 fanficdownloader/adapters/adapter_trekiverseorg.py create mode 100644 fanficdownloader/adapters/adapter_tthfanficorg.py create mode 100644 fanficdownloader/adapters/adapter_twcslibrarynet.py create mode 100644 fanficdownloader/adapters/adapter_twilightarchivescom.py create mode 100644 fanficdownloader/adapters/adapter_twilightednet.py create mode 100644 fanficdownloader/adapters/adapter_twiwritenet.py create mode 100644 fanficdownloader/adapters/adapter_voracity2eficcom.py create mode 100644 fanficdownloader/adapters/adapter_walkingtheplankorg.py create mode 100644 fanficdownloader/adapters/adapter_whoficcom.py create mode 100644 fanficdownloader/adapters/adapter_wizardtalesnet.py create mode 100644 fanficdownloader/adapters/adapter_wolverineandroguecom.py create mode 100644 fanficdownloader/adapters/adapter_wraithbaitcom.py create mode 100644 fanficdownloader/adapters/base_adapter.py create mode 100644 fanficdownloader/adapters/base_efiction_adapter.py create mode 100644 fanficdownloader/configurable.py create mode 100644 fanficdownloader/epubutils.py create mode 100644 fanficdownloader/exceptions.py create mode 100644 fanficdownloader/geturls.py create mode 100644 fanficdownloader/gziphttp.py create mode 100644 fanficdownloader/html.py create mode 100644 fanficdownloader/html2text.py create mode 100644 fanficdownloader/htmlcleanup.py create mode 100644 fanficdownloader/htmlheuristics.py create mode 100644 fanficdownloader/mobi.py create mode 100644 fanficdownloader/story.py create mode 100644 fanficdownloader/translit.py create mode 100644 fanficdownloader/writers/__init__.py create mode 100644 fanficdownloader/writers/base_writer.py create mode 100644 fanficdownloader/writers/writer_epub.py create mode 100644 fanficdownloader/writers/writer_html.py create mode 100644 fanficdownloader/writers/writer_mobi.py create mode 100644 fanficdownloader/writers/writer_txt.py create mode 100644 ffstorage.py create mode 100644 html5lib/LICENSE create mode 100644 html5lib/__init__.py create mode 100644 html5lib/constants.py create mode 100644 html5lib/filters/__init__.py create mode 100644 html5lib/filters/_base.py create mode 100644 html5lib/filters/alphabeticalattributes.py create mode 100644 html5lib/filters/inject_meta_charset.py create mode 100644 html5lib/filters/lint.py create mode 100644 html5lib/filters/optionaltags.py create mode 100644 html5lib/filters/sanitizer.py create mode 100644 html5lib/filters/whitespace.py create mode 100644 html5lib/html5parser.py create mode 100644 html5lib/ihatexml.py create mode 100644 html5lib/inputstream.py create mode 100644 html5lib/sanitizer.py create mode 100644 html5lib/serializer/__init__.py create mode 100644 html5lib/serializer/htmlserializer.py create mode 100644 html5lib/tokenizer.py create mode 100644 html5lib/treeadapters/__init__.py create mode 100644 html5lib/treeadapters/sax.py create mode 100644 html5lib/treebuilders/__init__.py create mode 100644 html5lib/treebuilders/_base.py create mode 100644 html5lib/treebuilders/dom.py create mode 100644 html5lib/treebuilders/etree.py create mode 100644 html5lib/treebuilders/etree_lxml.py create mode 100644 html5lib/treewalkers/__init__.py create mode 100644 html5lib/treewalkers/_base.py create mode 100644 html5lib/treewalkers/dom.py create mode 100644 html5lib/treewalkers/etree.py create mode 100644 html5lib/treewalkers/genshistream.py create mode 100644 html5lib/treewalkers/lxmletree.py create mode 100644 html5lib/treewalkers/pulldom.py create mode 100644 html5lib/trie/__init__.py create mode 100644 html5lib/trie/_base.py create mode 100644 html5lib/trie/datrie.py create mode 100644 html5lib/trie/py.py create mode 100644 html5lib/utils.py create mode 100644 index-ajax.html create mode 100644 index.html create mode 100644 index.yaml create mode 100644 js/fdownloader.js create mode 100644 js/jquery-1.3.2.js create mode 100644 login.html create mode 100644 main.py create mode 100644 makeplugin.py create mode 100644 makezip.py create mode 100644 plugin-defaults.ini create mode 100644 plugin-example.ini create mode 100644 queue.yaml create mode 100644 readme.txt create mode 100644 recent.html create mode 100644 settings.py create mode 100644 six.py create mode 100644 static/ajax-loader.gif create mode 100644 static/favicon.ico create mode 100644 status.html create mode 100644 utils/__init__.py create mode 100644 utils/remover.py create mode 100644 utils/tally.py diff --git a/allrecent.html b/allrecent.html new file mode 100644 index 0000000..477b17b --- /dev/null +++ b/allrecent.html @@ -0,0 +1,78 @@ + + + + + FanFictionDownLoader (fanfiction.net, fanficauthors, fictionalley, ficwad to epub and HTML) + + + + +
+

+ FanFictionDownLoader +

+ + + + + {{yourfile}} + + +
+ {% for fic in fics %} +

+ {{ fic.title }} + by {{ fic.author }} Download Count: {{ fic.count }}
+ Word Count: {{ fic.numWords }} Chapter Count: {{ fic.numChapters }}
+ {% if fic.category %} Categories: {{ fic.category }}
{% endif %} + {% if fic.genre %} Genres: {{ fic.genre }}
{% endif %} + {% if fic.language %} Language: {{ fic.language }}
{% endif %} + {% if fic.series %} Series: {{ fic.series }}
{% endif %} + {% if fic.characters %} Characters: {{ fic.characters }}
{% endif %} + {% if fic.status %} Status: {{ fic.status }}
{% endif %} + {% if fic.datePublished %} Published: {{ fic.datePublished }}
{% endif %} + {% if fic.dateUpdated %} Last Updated: {{ fic.dateUpdated }}
{% endif %} + {% if fic.dateCreated %} Last Downloaded: {{ fic.dateCreated }}
{% endif %} + {% if fic.rating %} Rating: {{ fic.rating }}
{% endif %} + {% if fic.warnings %} Warnings: {{ fic.warnings }}
{% endif %} + {% if fic.description %} Summary: {{ fic.description }}
{% endif %} +

+ {% endfor %} +
+ + + + +
+ + diff --git a/app.yaml b/app.yaml new file mode 100644 index 0000000..d04aac4 --- /dev/null +++ b/app.yaml @@ -0,0 +1,46 @@ +# ffd-retief-hrd fanfictiondownloader +application: fanfictiondownloader +version: 2-1-01 +runtime: python27 +api_version: 1 +threadsafe: true + +handlers: + +- url: /r3m0v3r.* + script: utils.remover.app + login: admin + +- url: /tally.* + script: utils.tally.app + login: admin + +- url: /fdownloadtask + script: main.app + login: admin + +- url: /css + static_dir: css + +- url: /js + static_dir: js + +- url: /static + static_dir: static + +- url: /favicon\.ico + static_files: static/favicon.ico + upload: static/favicon\.ico + +- url: /.* + script: main.app + +#builtins: +#- datastore_admin: on + +libraries: +- name: django + version: "1.2" + +- name: PIL + version: "1.1.7" diff --git a/bs4/COPYING.txt b/bs4/COPYING.txt new file mode 100644 index 0000000..d668d13 --- /dev/null +++ b/bs4/COPYING.txt @@ -0,0 +1,26 @@ +Beautiful Soup is made available under the MIT license: + + Copyright (c) 2004-2012 Leonard Richardson + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE, DAMMIT. + +Beautiful Soup incorporates code from the html5lib library, which is +also made available under the MIT license. diff --git a/bs4/__init__.py b/bs4/__init__.py new file mode 100644 index 0000000..7ba3426 --- /dev/null +++ b/bs4/__init__.py @@ -0,0 +1,406 @@ +"""Beautiful Soup +Elixir and Tonic +"The Screen-Scraper's Friend" +http://www.crummy.com/software/BeautifulSoup/ + +Beautiful Soup uses a pluggable XML or HTML parser to parse a +(possibly invalid) document into a tree representation. Beautiful Soup +provides provides methods and Pythonic idioms that make it easy to +navigate, search, and modify the parse tree. + +Beautiful Soup works with Python 2.6 and up. It works better if lxml +and/or html5lib is installed. + +For more than you ever wanted to know about Beautiful Soup, see the +documentation: +http://www.crummy.com/software/BeautifulSoup/bs4/doc/ +""" + +__author__ = "Leonard Richardson (leonardr@segfault.org)" +__version__ = "4.3.2" +__copyright__ = "Copyright (c) 2004-2013 Leonard Richardson" +__license__ = "MIT" + +__all__ = ['BeautifulSoup'] + +import os +import re +import warnings + +from .builder import builder_registry, ParserRejectedMarkup +from .dammit import UnicodeDammit +from .element import ( + CData, + Comment, + DEFAULT_OUTPUT_ENCODING, + Declaration, + Doctype, + NavigableString, + PageElement, + ProcessingInstruction, + ResultSet, + SoupStrainer, + Tag, + ) + +# The very first thing we do is give a useful error if someone is +# running this code under Python 3 without converting it. +syntax_error = u'You are trying to run the Python 2 version of Beautiful Soup under Python 3. This will not work. You need to convert the code, either by installing it (`python setup.py install`) or by running 2to3 (`2to3 -w bs4`).' + +class BeautifulSoup(Tag): + """ + This class defines the basic interface called by the tree builders. + + These methods will be called by the parser: + reset() + feed(markup) + + The tree builder may call these methods from its feed() implementation: + handle_starttag(name, attrs) # See note about return value + handle_endtag(name) + handle_data(data) # Appends to the current data node + endData(containerClass=NavigableString) # Ends the current data node + + No matter how complicated the underlying parser is, you should be + able to build a tree using 'start tag' events, 'end tag' events, + 'data' events, and "done with data" events. + + If you encounter an empty-element tag (aka a self-closing tag, + like HTML's
tag), call handle_starttag and then + handle_endtag. + """ + ROOT_TAG_NAME = u'[document]' + + # If the end-user gives no indication which tree builder they + # want, look for one with these features. + DEFAULT_BUILDER_FEATURES = ['html', 'fast'] + + ASCII_SPACES = '\x20\x0a\x09\x0c\x0d' + + def __init__(self, markup="", features=None, builder=None, + parse_only=None, from_encoding=None, **kwargs): + """The Soup object is initialized as the 'root tag', and the + provided markup (which can be a string or a file-like object) + is fed into the underlying parser.""" + + if 'convertEntities' in kwargs: + warnings.warn( + "BS4 does not respect the convertEntities argument to the " + "BeautifulSoup constructor. Entities are always converted " + "to Unicode characters.") + + if 'markupMassage' in kwargs: + del kwargs['markupMassage'] + warnings.warn( + "BS4 does not respect the markupMassage argument to the " + "BeautifulSoup constructor. The tree builder is responsible " + "for any necessary markup massage.") + + if 'smartQuotesTo' in kwargs: + del kwargs['smartQuotesTo'] + warnings.warn( + "BS4 does not respect the smartQuotesTo argument to the " + "BeautifulSoup constructor. Smart quotes are always converted " + "to Unicode characters.") + + if 'selfClosingTags' in kwargs: + del kwargs['selfClosingTags'] + warnings.warn( + "BS4 does not respect the selfClosingTags argument to the " + "BeautifulSoup constructor. The tree builder is responsible " + "for understanding self-closing tags.") + + if 'isHTML' in kwargs: + del kwargs['isHTML'] + warnings.warn( + "BS4 does not respect the isHTML argument to the " + "BeautifulSoup constructor. You can pass in features='html' " + "or features='xml' to get a builder capable of handling " + "one or the other.") + + def deprecated_argument(old_name, new_name): + if old_name in kwargs: + warnings.warn( + 'The "%s" argument to the BeautifulSoup constructor ' + 'has been renamed to "%s."' % (old_name, new_name)) + value = kwargs[old_name] + del kwargs[old_name] + return value + return None + + parse_only = parse_only or deprecated_argument( + "parseOnlyThese", "parse_only") + + from_encoding = from_encoding or deprecated_argument( + "fromEncoding", "from_encoding") + + if len(kwargs) > 0: + arg = kwargs.keys().pop() + raise TypeError( + "__init__() got an unexpected keyword argument '%s'" % arg) + + if builder is None: + if isinstance(features, basestring): + features = [features] + if features is None or len(features) == 0: + features = self.DEFAULT_BUILDER_FEATURES + builder_class = builder_registry.lookup(*features) + if builder_class is None: + raise FeatureNotFound( + "Couldn't find a tree builder with the features you " + "requested: %s. Do you need to install a parser library?" + % ",".join(features)) + builder = builder_class() + self.builder = builder + self.is_xml = builder.is_xml + self.builder.soup = self + + self.parse_only = parse_only + + if hasattr(markup, 'read'): # It's a file-type object. + markup = markup.read() + elif len(markup) <= 256: + # Print out warnings for a couple beginner problems + # involving passing non-markup to Beautiful Soup. + # Beautiful Soup will still parse the input as markup, + # just in case that's what the user really wants. + if (isinstance(markup, unicode) + and not os.path.supports_unicode_filenames): + possible_filename = markup.encode("utf8") + else: + possible_filename = markup + is_file = False + try: + is_file = os.path.exists(possible_filename) + except Exception, e: + # This is almost certainly a problem involving + # characters not valid in filenames on this + # system. Just let it go. + pass + if is_file: + warnings.warn( + '"%s" looks like a filename, not markup. You should probably open this file and pass the filehandle into Beautiful Soup.' % markup) + if markup[:5] == "http:" or markup[:6] == "https:": + # TODO: This is ugly but I couldn't get it to work in + # Python 3 otherwise. + if ((isinstance(markup, bytes) and not b' ' in markup) + or (isinstance(markup, unicode) and not u' ' in markup)): + warnings.warn( + '"%s" looks like a URL. Beautiful Soup is not an HTTP client. You should probably use an HTTP client to get the document behind the URL, and feed that document to Beautiful Soup.' % markup) + + for (self.markup, self.original_encoding, self.declared_html_encoding, + self.contains_replacement_characters) in ( + self.builder.prepare_markup(markup, from_encoding)): + self.reset() + try: + self._feed() + break + except ParserRejectedMarkup: + pass + + # Clear out the markup and remove the builder's circular + # reference to this object. + self.markup = None + self.builder.soup = None + + def _feed(self): + # Convert the document to Unicode. + self.builder.reset() + + self.builder.feed(self.markup) + # Close out any unfinished strings and close all the open tags. + self.endData() + while self.currentTag.name != self.ROOT_TAG_NAME: + self.popTag() + + def reset(self): + Tag.__init__(self, self, self.builder, self.ROOT_TAG_NAME) + self.hidden = 1 + self.builder.reset() + self.current_data = [] + self.currentTag = None + self.tagStack = [] + self.preserve_whitespace_tag_stack = [] + self.pushTag(self) + + def new_tag(self, name, namespace=None, nsprefix=None, **attrs): + """Create a new tag associated with this soup.""" + return Tag(None, self.builder, name, namespace, nsprefix, attrs) + + def new_string(self, s, subclass=NavigableString): + """Create a new NavigableString associated with this soup.""" + navigable = subclass(s) + navigable.setup() + return navigable + + def insert_before(self, successor): + raise NotImplementedError("BeautifulSoup objects don't support insert_before().") + + def insert_after(self, successor): + raise NotImplementedError("BeautifulSoup objects don't support insert_after().") + + def popTag(self): + tag = self.tagStack.pop() + if self.preserve_whitespace_tag_stack and tag == self.preserve_whitespace_tag_stack[-1]: + self.preserve_whitespace_tag_stack.pop() + #print "Pop", tag.name + if self.tagStack: + self.currentTag = self.tagStack[-1] + return self.currentTag + + def pushTag(self, tag): + #print "Push", tag.name + if self.currentTag: + self.currentTag.contents.append(tag) + self.tagStack.append(tag) + self.currentTag = self.tagStack[-1] + if tag.name in self.builder.preserve_whitespace_tags: + self.preserve_whitespace_tag_stack.append(tag) + + def endData(self, containerClass=NavigableString): + if self.current_data: + current_data = u''.join(self.current_data) + # If whitespace is not preserved, and this string contains + # nothing but ASCII spaces, replace it with a single space + # or newline. + if not self.preserve_whitespace_tag_stack: + strippable = True + for i in current_data: + if i not in self.ASCII_SPACES: + strippable = False + break + if strippable: + if '\n' in current_data: + current_data = '\n' + else: + current_data = ' ' + + # Reset the data collector. + self.current_data = [] + + # Should we add this string to the tree at all? + if self.parse_only and len(self.tagStack) <= 1 and \ + (not self.parse_only.text or \ + not self.parse_only.search(current_data)): + return + + o = containerClass(current_data) + self.object_was_parsed(o) + + def object_was_parsed(self, o, parent=None, most_recent_element=None): + """Add an object to the parse tree.""" + parent = parent or self.currentTag + most_recent_element = most_recent_element or self._most_recent_element + o.setup(parent, most_recent_element) + + if most_recent_element is not None: + most_recent_element.next_element = o + self._most_recent_element = o + parent.contents.append(o) + + def _popToTag(self, name, nsprefix=None, inclusivePop=True): + """Pops the tag stack up to and including the most recent + instance of the given tag. If inclusivePop is false, pops the tag + stack up to but *not* including the most recent instqance of + the given tag.""" + #print "Popping to %s" % name + if name == self.ROOT_TAG_NAME: + # The BeautifulSoup object itself can never be popped. + return + + most_recently_popped = None + + stack_size = len(self.tagStack) + for i in range(stack_size - 1, 0, -1): + t = self.tagStack[i] + if (name == t.name and nsprefix == t.prefix): + if inclusivePop: + most_recently_popped = self.popTag() + break + most_recently_popped = self.popTag() + + return most_recently_popped + + def handle_starttag(self, name, namespace, nsprefix, attrs): + """Push a start tag on to the stack. + + If this method returns None, the tag was rejected by the + SoupStrainer. You should proceed as if the tag had not occured + in the document. For instance, if this was a self-closing tag, + don't call handle_endtag. + """ + + # print "Start tag %s: %s" % (name, attrs) + self.endData() + + if (self.parse_only and len(self.tagStack) <= 1 + and (self.parse_only.text + or not self.parse_only.search_tag(name, attrs))): + return None + + tag = Tag(self, self.builder, name, namespace, nsprefix, attrs, + self.currentTag, self._most_recent_element) + if tag is None: + return tag + if self._most_recent_element: + self._most_recent_element.next_element = tag + self._most_recent_element = tag + self.pushTag(tag) + return tag + + def handle_endtag(self, name, nsprefix=None): + #print "End tag: " + name + self.endData() + self._popToTag(name, nsprefix) + + def handle_data(self, data): + self.current_data.append(data) + + def decode(self, pretty_print=False, + eventual_encoding=DEFAULT_OUTPUT_ENCODING, + formatter="minimal"): + """Returns a string or Unicode representation of this document. + To get Unicode, pass None for encoding.""" + + if self.is_xml: + # Print the XML declaration + encoding_part = '' + if eventual_encoding != None: + encoding_part = ' encoding="%s"' % eventual_encoding + prefix = u'\n' % encoding_part + else: + prefix = u'' + if not pretty_print: + indent_level = None + else: + indent_level = 0 + return prefix + super(BeautifulSoup, self).decode( + indent_level, eventual_encoding, formatter) + +# Alias to make it easier to type import: 'from bs4 import _soup' +_s = BeautifulSoup +_soup = BeautifulSoup + +class BeautifulStoneSoup(BeautifulSoup): + """Deprecated interface to an XML parser.""" + + def __init__(self, *args, **kwargs): + kwargs['features'] = 'xml' + warnings.warn( + 'The BeautifulStoneSoup class is deprecated. Instead of using ' + 'it, pass features="xml" into the BeautifulSoup constructor.') + super(BeautifulStoneSoup, self).__init__(*args, **kwargs) + + +class StopParsing(Exception): + pass + +class FeatureNotFound(ValueError): + pass + + +#By default, act as an HTML pretty-printer. +if __name__ == '__main__': + import sys + soup = BeautifulSoup(sys.stdin) + print soup.prettify() diff --git a/bs4/builder/__init__.py b/bs4/builder/__init__.py new file mode 100644 index 0000000..740f5f2 --- /dev/null +++ b/bs4/builder/__init__.py @@ -0,0 +1,321 @@ +from collections import defaultdict +import itertools +import sys +from bs4.element import ( + CharsetMetaAttributeValue, + ContentMetaAttributeValue, + whitespace_re + ) + +__all__ = [ + 'HTMLTreeBuilder', + 'SAXTreeBuilder', + 'TreeBuilder', + 'TreeBuilderRegistry', + ] + +# Some useful features for a TreeBuilder to have. +FAST = 'fast' +PERMISSIVE = 'permissive' +STRICT = 'strict' +XML = 'xml' +HTML = 'html' +HTML_5 = 'html5' + + +class TreeBuilderRegistry(object): + + def __init__(self): + self.builders_for_feature = defaultdict(list) + self.builders = [] + + def register(self, treebuilder_class): + """Register a treebuilder based on its advertised features.""" + for feature in treebuilder_class.features: + self.builders_for_feature[feature].insert(0, treebuilder_class) + self.builders.insert(0, treebuilder_class) + + def lookup(self, *features): + if len(self.builders) == 0: + # There are no builders at all. + return None + + if len(features) == 0: + # They didn't ask for any features. Give them the most + # recently registered builder. + return self.builders[0] + + # Go down the list of features in order, and eliminate any builders + # that don't match every feature. + features = list(features) + features.reverse() + candidates = None + candidate_set = None + while len(features) > 0: + feature = features.pop() + we_have_the_feature = self.builders_for_feature.get(feature, []) + if len(we_have_the_feature) > 0: + if candidates is None: + candidates = we_have_the_feature + candidate_set = set(candidates) + else: + # Eliminate any candidates that don't have this feature. + candidate_set = candidate_set.intersection( + set(we_have_the_feature)) + + # The only valid candidates are the ones in candidate_set. + # Go through the original list of candidates and pick the first one + # that's in candidate_set. + if candidate_set is None: + return None + for candidate in candidates: + if candidate in candidate_set: + return candidate + return None + +# The BeautifulSoup class will take feature lists from developers and use them +# to look up builders in this registry. +builder_registry = TreeBuilderRegistry() + +class TreeBuilder(object): + """Turn a document into a Beautiful Soup object tree.""" + + features = [] + + is_xml = False + preserve_whitespace_tags = set() + empty_element_tags = None # A tag will be considered an empty-element + # tag when and only when it has no contents. + + # A value for these tag/attribute combinations is a space- or + # comma-separated list of CDATA, rather than a single CDATA. + cdata_list_attributes = {} + + + def __init__(self): + self.soup = None + + def reset(self): + pass + + def can_be_empty_element(self, tag_name): + """Might a tag with this name be an empty-element tag? + + The final markup may or may not actually present this tag as + self-closing. + + For instance: an HTMLBuilder does not consider a

tag to be + an empty-element tag (it's not in + HTMLBuilder.empty_element_tags). This means an empty

tag + will be presented as "

", not "

". + + The default implementation has no opinion about which tags are + empty-element tags, so a tag will be presented as an + empty-element tag if and only if it has no contents. + "" will become "", and "bar" will + be left alone. + """ + if self.empty_element_tags is None: + return True + return tag_name in self.empty_element_tags + + def feed(self, markup): + raise NotImplementedError() + + def prepare_markup(self, markup, user_specified_encoding=None, + document_declared_encoding=None): + return markup, None, None, False + + def test_fragment_to_document(self, fragment): + """Wrap an HTML fragment to make it look like a document. + + Different parsers do this differently. For instance, lxml + introduces an empty tag, and html5lib + doesn't. Abstracting this away lets us write simple tests + which run HTML fragments through the parser and compare the + results against other HTML fragments. + + This method should not be used outside of tests. + """ + return fragment + + def set_up_substitutions(self, tag): + return False + + def _replace_cdata_list_attribute_values(self, tag_name, attrs): + """Replaces class="foo bar" with class=["foo", "bar"] + + Modifies its input in place. + """ + if not attrs: + return attrs + if self.cdata_list_attributes: + universal = self.cdata_list_attributes.get('*', []) + tag_specific = self.cdata_list_attributes.get( + tag_name.lower(), None) + for attr in attrs.keys(): + if attr in universal or (tag_specific and attr in tag_specific): + # We have a "class"-type attribute whose string + # value is a whitespace-separated list of + # values. Split it into a list. + value = attrs[attr] + if isinstance(value, basestring): + values = whitespace_re.split(value) + else: + # html5lib sometimes calls setAttributes twice + # for the same tag when rearranging the parse + # tree. On the second call the attribute value + # here is already a list. If this happens, + # leave the value alone rather than trying to + # split it again. + values = value + attrs[attr] = values + return attrs + +class SAXTreeBuilder(TreeBuilder): + """A Beautiful Soup treebuilder that listens for SAX events.""" + + def feed(self, markup): + raise NotImplementedError() + + def close(self): + pass + + def startElement(self, name, attrs): + attrs = dict((key[1], value) for key, value in list(attrs.items())) + #print "Start %s, %r" % (name, attrs) + self.soup.handle_starttag(name, attrs) + + def endElement(self, name): + #print "End %s" % name + self.soup.handle_endtag(name) + + def startElementNS(self, nsTuple, nodeName, attrs): + # Throw away (ns, nodeName) for now. + self.startElement(nodeName, attrs) + + def endElementNS(self, nsTuple, nodeName): + # Throw away (ns, nodeName) for now. + self.endElement(nodeName) + #handler.endElementNS((ns, node.nodeName), node.nodeName) + + def startPrefixMapping(self, prefix, nodeValue): + # Ignore the prefix for now. + pass + + def endPrefixMapping(self, prefix): + # Ignore the prefix for now. + # handler.endPrefixMapping(prefix) + pass + + def characters(self, content): + self.soup.handle_data(content) + + def startDocument(self): + pass + + def endDocument(self): + pass + + +class HTMLTreeBuilder(TreeBuilder): + """This TreeBuilder knows facts about HTML. + + Such as which tags are empty-element tags. + """ + + preserve_whitespace_tags = set(['pre', 'textarea']) + empty_element_tags = set(['br' , 'hr', 'input', 'img', 'meta', + 'spacer', 'link', 'frame', 'base']) + + # The HTML standard defines these attributes as containing a + # space-separated list of values, not a single value. That is, + # class="foo bar" means that the 'class' attribute has two values, + # 'foo' and 'bar', not the single value 'foo bar'. When we + # encounter one of these attributes, we will parse its value into + # a list of values if possible. Upon output, the list will be + # converted back into a string. + cdata_list_attributes = { + "*" : ['class', 'accesskey', 'dropzone'], + "a" : ['rel', 'rev'], + "link" : ['rel', 'rev'], + "td" : ["headers"], + "th" : ["headers"], + "td" : ["headers"], + "form" : ["accept-charset"], + "object" : ["archive"], + + # These are HTML5 specific, as are *.accesskey and *.dropzone above. + "area" : ["rel"], + "icon" : ["sizes"], + "iframe" : ["sandbox"], + "output" : ["for"], + } + + def set_up_substitutions(self, tag): + # We are only interested in tags + if tag.name != 'meta': + return False + + http_equiv = tag.get('http-equiv') + content = tag.get('content') + charset = tag.get('charset') + + # We are interested in tags that say what encoding the + # document was originally in. This means HTML 5-style + # tags that provide the "charset" attribute. It also means + # HTML 4-style tags that provide the "content" + # attribute and have "http-equiv" set to "content-type". + # + # In both cases we will replace the value of the appropriate + # attribute with a standin object that can take on any + # encoding. + meta_encoding = None + if charset is not None: + # HTML 5 style: + # + meta_encoding = charset + tag['charset'] = CharsetMetaAttributeValue(charset) + + elif (content is not None and http_equiv is not None + and http_equiv.lower() == 'content-type'): + # HTML 4 style: + # + tag['content'] = ContentMetaAttributeValue(content) + + return (meta_encoding is not None) + +def register_treebuilders_from(module): + """Copy TreeBuilders from the given module into this module.""" + # I'm fairly sure this is not the best way to do this. + this_module = sys.modules['bs4.builder'] + for name in module.__all__: + obj = getattr(module, name) + + if issubclass(obj, TreeBuilder): + setattr(this_module, name, obj) + this_module.__all__.append(name) + # Register the builder while we're at it. + this_module.builder_registry.register(obj) + +class ParserRejectedMarkup(Exception): + pass + +# Builders are registered in reverse order of priority, so that custom +# builder registrations will take precedence. In general, we want lxml +# to take precedence over html5lib, because it's faster. And we only +# want to use HTMLParser as a last result. +from . import _htmlparser +register_treebuilders_from(_htmlparser) +try: + from . import _html5lib + register_treebuilders_from(_html5lib) +except ImportError: + # They don't have html5lib installed. + pass +try: + from . import _lxml + register_treebuilders_from(_lxml) +except ImportError: + # They don't have lxml installed. + pass diff --git a/bs4/builder/_html5lib.py b/bs4/builder/_html5lib.py new file mode 100644 index 0000000..7de36ae --- /dev/null +++ b/bs4/builder/_html5lib.py @@ -0,0 +1,285 @@ +__all__ = [ + 'HTML5TreeBuilder', + ] + +import warnings +from bs4.builder import ( + PERMISSIVE, + HTML, + HTML_5, + HTMLTreeBuilder, + ) +from bs4.element import NamespacedAttribute +import html5lib +from html5lib.constants import namespaces +from bs4.element import ( + Comment, + Doctype, + NavigableString, + Tag, + ) + +class HTML5TreeBuilder(HTMLTreeBuilder): + """Use html5lib to build a tree.""" + + features = ['html5lib', PERMISSIVE, HTML_5, HTML] + + def prepare_markup(self, markup, user_specified_encoding): + # Store the user-specified encoding for use later on. + self.user_specified_encoding = user_specified_encoding + yield (markup, None, None, False) + + # These methods are defined by Beautiful Soup. + def feed(self, markup): + if self.soup.parse_only is not None: + warnings.warn("You provided a value for parse_only, but the html5lib tree builder doesn't support parse_only. The entire document will be parsed.") + parser = html5lib.HTMLParser(tree=self.create_treebuilder) + doc = parser.parse(markup, encoding=self.user_specified_encoding) + + # Set the character encoding detected by the tokenizer. + if isinstance(markup, unicode): + # We need to special-case this because html5lib sets + # charEncoding to UTF-8 if it gets Unicode input. + doc.original_encoding = None + else: + doc.original_encoding = parser.tokenizer.stream.charEncoding[0] + + def create_treebuilder(self, namespaceHTMLElements): + self.underlying_builder = TreeBuilderForHtml5lib( + self.soup, namespaceHTMLElements) + return self.underlying_builder + + def test_fragment_to_document(self, fragment): + """See `TreeBuilder`.""" + return u'%s' % fragment + + +class TreeBuilderForHtml5lib(html5lib.treebuilders._base.TreeBuilder): + + def __init__(self, soup, namespaceHTMLElements): + self.soup = soup + super(TreeBuilderForHtml5lib, self).__init__(namespaceHTMLElements) + + def documentClass(self): + self.soup.reset() + return Element(self.soup, self.soup, None) + + def insertDoctype(self, token): + name = token["name"] + publicId = token["publicId"] + systemId = token["systemId"] + + doctype = Doctype.for_name_and_ids(name, publicId, systemId) + self.soup.object_was_parsed(doctype) + + def elementClass(self, name, namespace): + tag = self.soup.new_tag(name, namespace) + return Element(tag, self.soup, namespace) + + def commentClass(self, data): + return TextNode(Comment(data), self.soup) + + def fragmentClass(self): + self.soup = BeautifulSoup("") + self.soup.name = "[document_fragment]" + return Element(self.soup, self.soup, None) + + def appendChild(self, node): + # XXX This code is not covered by the BS4 tests. + self.soup.append(node.element) + + def getDocument(self): + return self.soup + + def getFragment(self): + return html5lib.treebuilders._base.TreeBuilder.getFragment(self).element + +class AttrList(object): + def __init__(self, element): + self.element = element + self.attrs = dict(self.element.attrs) + def __iter__(self): + return list(self.attrs.items()).__iter__() + def __setitem__(self, name, value): + "set attr", name, value + self.element[name] = value + def items(self): + return list(self.attrs.items()) + def keys(self): + return list(self.attrs.keys()) + def __len__(self): + return len(self.attrs) + def __getitem__(self, name): + return self.attrs[name] + def __contains__(self, name): + return name in list(self.attrs.keys()) + + +class Element(html5lib.treebuilders._base.Node): + def __init__(self, element, soup, namespace): + html5lib.treebuilders._base.Node.__init__(self, element.name) + self.element = element + self.soup = soup + self.namespace = namespace + + def appendChild(self, node): + string_child = child = None + if isinstance(node, basestring): + # Some other piece of code decided to pass in a string + # instead of creating a TextElement object to contain the + # string. + string_child = child = node + elif isinstance(node, Tag): + # Some other piece of code decided to pass in a Tag + # instead of creating an Element object to contain the + # Tag. + child = node + elif node.element.__class__ == NavigableString: + string_child = child = node.element + else: + child = node.element + + if not isinstance(child, basestring) and child.parent is not None: + node.element.extract() + + if (string_child and self.element.contents + and self.element.contents[-1].__class__ == NavigableString): + # We are appending a string onto another string. + # TODO This has O(n^2) performance, for input like + # "aaa..." + old_element = self.element.contents[-1] + new_element = self.soup.new_string(old_element + string_child) + old_element.replace_with(new_element) + self.soup._most_recent_element = new_element + else: + if isinstance(node, basestring): + # Create a brand new NavigableString from this string. + child = self.soup.new_string(node) + + # Tell Beautiful Soup to act as if it parsed this element + # immediately after the parent's last descendant. (Or + # immediately after the parent, if it has no children.) + if self.element.contents: + most_recent_element = self.element._last_descendant(False) + else: + most_recent_element = self.element + + self.soup.object_was_parsed( + child, parent=self.element, + most_recent_element=most_recent_element) + + def getAttributes(self): + return AttrList(self.element) + + def setAttributes(self, attributes): + if attributes is not None and len(attributes) > 0: + + converted_attributes = [] + for name, value in list(attributes.items()): + if isinstance(name, tuple): + new_name = NamespacedAttribute(*name) + del attributes[name] + attributes[new_name] = value + + self.soup.builder._replace_cdata_list_attribute_values( + self.name, attributes) + for name, value in attributes.items(): + self.element[name] = value + + # The attributes may contain variables that need substitution. + # Call set_up_substitutions manually. + # + # The Tag constructor called this method when the Tag was created, + # but we just set/changed the attributes, so call it again. + self.soup.builder.set_up_substitutions(self.element) + attributes = property(getAttributes, setAttributes) + + def insertText(self, data, insertBefore=None): + if insertBefore: + text = TextNode(self.soup.new_string(data), self.soup) + self.insertBefore(data, insertBefore) + else: + self.appendChild(data) + + def insertBefore(self, node, refNode): + index = self.element.index(refNode.element) + if (node.element.__class__ == NavigableString and self.element.contents + and self.element.contents[index-1].__class__ == NavigableString): + # (See comments in appendChild) + old_node = self.element.contents[index-1] + new_str = self.soup.new_string(old_node + node.element) + old_node.replace_with(new_str) + else: + self.element.insert(index, node.element) + node.parent = self + + def removeChild(self, node): + node.element.extract() + + def reparentChildren(self, new_parent): + """Move all of this tag's children into another tag.""" + element = self.element + new_parent_element = new_parent.element + # Determine what this tag's next_element will be once all the children + # are removed. + final_next_element = element.next_sibling + + new_parents_last_descendant = new_parent_element._last_descendant(False, False) + if len(new_parent_element.contents) > 0: + # The new parent already contains children. We will be + # appending this tag's children to the end. + new_parents_last_child = new_parent_element.contents[-1] + new_parents_last_descendant_next_element = new_parents_last_descendant.next_element + else: + # The new parent contains no children. + new_parents_last_child = None + new_parents_last_descendant_next_element = new_parent_element.next_element + + to_append = element.contents + append_after = new_parent.element.contents + if len(to_append) > 0: + # Set the first child's previous_element and previous_sibling + # to elements within the new parent + first_child = to_append[0] + first_child.previous_element = new_parents_last_descendant + first_child.previous_sibling = new_parents_last_child + + # Fix the last child's next_element and next_sibling + last_child = to_append[-1] + last_child.next_element = new_parents_last_descendant_next_element + last_child.next_sibling = None + + for child in to_append: + child.parent = new_parent_element + new_parent_element.contents.append(child) + + # Now that this element has no children, change its .next_element. + element.contents = [] + element.next_element = final_next_element + + def cloneNode(self): + tag = self.soup.new_tag(self.element.name, self.namespace) + node = Element(tag, self.soup, self.namespace) + for key,value in self.attributes: + node.attributes[key] = value + return node + + def hasContent(self): + return self.element.contents + + def getNameTuple(self): + if self.namespace == None: + return namespaces["html"], self.name + else: + return self.namespace, self.name + + nameTuple = property(getNameTuple) + +class TextNode(Element): + def __init__(self, element, soup): + html5lib.treebuilders._base.Node.__init__(self, None) + self.element = element + self.soup = soup + + def cloneNode(self): + raise NotImplementedError diff --git a/bs4/builder/_htmlparser.py b/bs4/builder/_htmlparser.py new file mode 100644 index 0000000..ca8d8b8 --- /dev/null +++ b/bs4/builder/_htmlparser.py @@ -0,0 +1,258 @@ +"""Use the HTMLParser library to parse HTML files that aren't too bad.""" + +__all__ = [ + 'HTMLParserTreeBuilder', + ] + +from HTMLParser import ( + HTMLParser, + HTMLParseError, + ) +import sys +import warnings + +# Starting in Python 3.2, the HTMLParser constructor takes a 'strict' +# argument, which we'd like to set to False. Unfortunately, +# http://bugs.python.org/issue13273 makes strict=True a better bet +# before Python 3.2.3. +# +# At the end of this file, we monkeypatch HTMLParser so that +# strict=True works well on Python 3.2.2. +major, minor, release = sys.version_info[:3] +CONSTRUCTOR_TAKES_STRICT = ( + major > 3 + or (major == 3 and minor > 2) + or (major == 3 and minor == 2 and release >= 3)) + +from bs4.element import ( + CData, + Comment, + Declaration, + Doctype, + ProcessingInstruction, + ) +from bs4.dammit import EntitySubstitution, UnicodeDammit + +from bs4.builder import ( + HTML, + HTMLTreeBuilder, + STRICT, + ) + + +HTMLPARSER = 'html.parser' + +class BeautifulSoupHTMLParser(HTMLParser): + def handle_starttag(self, name, attrs): + # XXX namespace + attr_dict = {} + for key, value in attrs: + # Change None attribute values to the empty string + # for consistency with the other tree builders. + if value is None: + value = '' + attr_dict[key] = value + attrvalue = '""' + self.soup.handle_starttag(name, None, None, attr_dict) + + def handle_endtag(self, name): + self.soup.handle_endtag(name) + + def handle_data(self, data): + self.soup.handle_data(data) + + def handle_charref(self, name): + # XXX workaround for a bug in HTMLParser. Remove this once + # it's fixed. + if name.startswith('x'): + real_name = int(name.lstrip('x'), 16) + elif name.startswith('X'): + real_name = int(name.lstrip('X'), 16) + else: + real_name = int(name) + + try: + data = unichr(real_name) + except (ValueError, OverflowError), e: + data = u"\N{REPLACEMENT CHARACTER}" + + self.handle_data(data) + + def handle_entityref(self, name): + character = EntitySubstitution.HTML_ENTITY_TO_CHARACTER.get(name) + if character is not None: + data = character + else: + data = "&%s;" % name + self.handle_data(data) + + def handle_comment(self, data): + self.soup.endData() + self.soup.handle_data(data) + self.soup.endData(Comment) + + def handle_decl(self, data): + self.soup.endData() + if data.startswith("DOCTYPE "): + data = data[len("DOCTYPE "):] + elif data == 'DOCTYPE': + # i.e. "" + data = '' + self.soup.handle_data(data) + self.soup.endData(Doctype) + + def unknown_decl(self, data): + if data.upper().startswith('CDATA['): + cls = CData + data = data[len('CDATA['):] + else: + cls = Declaration + self.soup.endData() + self.soup.handle_data(data) + self.soup.endData(cls) + + def handle_pi(self, data): + self.soup.endData() + if data.endswith("?") and data.lower().startswith("xml"): + # "An XHTML processing instruction using the trailing '?' + # will cause the '?' to be included in data." - HTMLParser + # docs. + # + # Strip the question mark so we don't end up with two + # question marks. + data = data[:-1] + self.soup.handle_data(data) + self.soup.endData(ProcessingInstruction) + + +class HTMLParserTreeBuilder(HTMLTreeBuilder): + + is_xml = False + features = [HTML, STRICT, HTMLPARSER] + + def __init__(self, *args, **kwargs): + if CONSTRUCTOR_TAKES_STRICT: + kwargs['strict'] = False + self.parser_args = (args, kwargs) + + def prepare_markup(self, markup, user_specified_encoding=None, + document_declared_encoding=None): + """ + :return: A 4-tuple (markup, original encoding, encoding + declared within markup, whether any characters had to be + replaced with REPLACEMENT CHARACTER). + """ + if isinstance(markup, unicode): + yield (markup, None, None, False) + return + + try_encodings = [user_specified_encoding, document_declared_encoding] + dammit = UnicodeDammit(markup, try_encodings, is_html=True) + yield (dammit.markup, dammit.original_encoding, + dammit.declared_html_encoding, + dammit.contains_replacement_characters) + + def feed(self, markup): + args, kwargs = self.parser_args + parser = BeautifulSoupHTMLParser(*args, **kwargs) + parser.soup = self.soup + try: + parser.feed(markup) + except HTMLParseError, e: + warnings.warn(RuntimeWarning( + "Python's built-in HTMLParser cannot parse the given document. This is not a bug in Beautiful Soup. The best solution is to install an external parser (lxml or html5lib), and use Beautiful Soup with that parser. See http://www.crummy.com/software/BeautifulSoup/bs4/doc/#installing-a-parser for help.")) + raise e + +# Patch 3.2 versions of HTMLParser earlier than 3.2.3 to use some +# 3.2.3 code. This ensures they don't treat markup like

as a +# string. +# +# XXX This code can be removed once most Python 3 users are on 3.2.3. +if major == 3 and minor == 2 and not CONSTRUCTOR_TAKES_STRICT: + import re + attrfind_tolerant = re.compile( + r'\s*((?<=[\'"\s])[^\s/>][^\s/=>]*)(\s*=+\s*' + r'(\'[^\']*\'|"[^"]*"|(?![\'"])[^>\s]*))?') + HTMLParserTreeBuilder.attrfind_tolerant = attrfind_tolerant + + locatestarttagend = re.compile(r""" + <[a-zA-Z][-.a-zA-Z0-9:_]* # tag name + (?:\s+ # whitespace before attribute name + (?:[a-zA-Z_][-.:a-zA-Z0-9_]* # attribute name + (?:\s*=\s* # value indicator + (?:'[^']*' # LITA-enclosed value + |\"[^\"]*\" # LIT-enclosed value + |[^'\">\s]+ # bare value + ) + )? + ) + )* + \s* # trailing whitespace +""", re.VERBOSE) + BeautifulSoupHTMLParser.locatestarttagend = locatestarttagend + + from html.parser import tagfind, attrfind + + def parse_starttag(self, i): + self.__starttag_text = None + endpos = self.check_for_whole_start_tag(i) + if endpos < 0: + return endpos + rawdata = self.rawdata + self.__starttag_text = rawdata[i:endpos] + + # Now parse the data between i+1 and j into a tag and attrs + attrs = [] + match = tagfind.match(rawdata, i+1) + assert match, 'unexpected call to parse_starttag()' + k = match.end() + self.lasttag = tag = rawdata[i+1:k].lower() + while k < endpos: + if self.strict: + m = attrfind.match(rawdata, k) + else: + m = attrfind_tolerant.match(rawdata, k) + if not m: + break + attrname, rest, attrvalue = m.group(1, 2, 3) + if not rest: + attrvalue = None + elif attrvalue[:1] == '\'' == attrvalue[-1:] or \ + attrvalue[:1] == '"' == attrvalue[-1:]: + attrvalue = attrvalue[1:-1] + if attrvalue: + attrvalue = self.unescape(attrvalue) + attrs.append((attrname.lower(), attrvalue)) + k = m.end() + + end = rawdata[k:endpos].strip() + if end not in (">", "/>"): + lineno, offset = self.getpos() + if "\n" in self.__starttag_text: + lineno = lineno + self.__starttag_text.count("\n") + offset = len(self.__starttag_text) \ + - self.__starttag_text.rfind("\n") + else: + offset = offset + len(self.__starttag_text) + if self.strict: + self.error("junk characters in start tag: %r" + % (rawdata[k:endpos][:20],)) + self.handle_data(rawdata[i:endpos]) + return endpos + if end.endswith('/>'): + # XHTML-style empty tag: + self.handle_startendtag(tag, attrs) + else: + self.handle_starttag(tag, attrs) + if tag in self.CDATA_CONTENT_ELEMENTS: + self.set_cdata_mode(tag) + return endpos + + def set_cdata_mode(self, elem): + self.cdata_elem = elem.lower() + self.interesting = re.compile(r'' % self.cdata_elem, re.I) + + BeautifulSoupHTMLParser.parse_starttag = parse_starttag + BeautifulSoupHTMLParser.set_cdata_mode = set_cdata_mode + + CONSTRUCTOR_TAKES_STRICT = True diff --git a/bs4/builder/_lxml.py b/bs4/builder/_lxml.py new file mode 100644 index 0000000..fa5d498 --- /dev/null +++ b/bs4/builder/_lxml.py @@ -0,0 +1,233 @@ +__all__ = [ + 'LXMLTreeBuilderForXML', + 'LXMLTreeBuilder', + ] + +from io import BytesIO +from StringIO import StringIO +import collections +from lxml import etree +from bs4.element import Comment, Doctype, NamespacedAttribute +from bs4.builder import ( + FAST, + HTML, + HTMLTreeBuilder, + PERMISSIVE, + ParserRejectedMarkup, + TreeBuilder, + XML) +from bs4.dammit import EncodingDetector + +LXML = 'lxml' + +class LXMLTreeBuilderForXML(TreeBuilder): + DEFAULT_PARSER_CLASS = etree.XMLParser + + is_xml = True + + # Well, it's permissive by XML parser standards. + features = [LXML, XML, FAST, PERMISSIVE] + + CHUNK_SIZE = 512 + + # This namespace mapping is specified in the XML Namespace + # standard. + DEFAULT_NSMAPS = {'http://www.w3.org/XML/1998/namespace' : "xml"} + + def default_parser(self, encoding): + # This can either return a parser object or a class, which + # will be instantiated with default arguments. + if self._default_parser is not None: + return self._default_parser + return etree.XMLParser( + target=self, strip_cdata=False, recover=True, encoding=encoding) + + def parser_for(self, encoding): + # Use the default parser. + parser = self.default_parser(encoding) + + if isinstance(parser, collections.Callable): + # Instantiate the parser with default arguments + parser = parser(target=self, strip_cdata=False, encoding=encoding) + return parser + + def __init__(self, parser=None, empty_element_tags=None): + # TODO: Issue a warning if parser is present but not a + # callable, since that means there's no way to create new + # parsers for different encodings. + self._default_parser = parser + if empty_element_tags is not None: + self.empty_element_tags = set(empty_element_tags) + self.soup = None + self.nsmaps = [self.DEFAULT_NSMAPS] + + def _getNsTag(self, tag): + # Split the namespace URL out of a fully-qualified lxml tag + # name. Copied from lxml's src/lxml/sax.py. + if tag[0] == '{': + return tuple(tag[1:].split('}', 1)) + else: + return (None, tag) + + def prepare_markup(self, markup, user_specified_encoding=None, + document_declared_encoding=None): + """ + :yield: A series of 4-tuples. + (markup, encoding, declared encoding, + has undergone character replacement) + + Each 4-tuple represents a strategy for parsing the document. + """ + if isinstance(markup, unicode): + # We were given Unicode. Maybe lxml can parse Unicode on + # this system? + yield markup, None, document_declared_encoding, False + + if isinstance(markup, unicode): + # No, apparently not. Convert the Unicode to UTF-8 and + # tell lxml to parse it as UTF-8. + yield (markup.encode("utf8"), "utf8", + document_declared_encoding, False) + + # Instead of using UnicodeDammit to convert the bytestring to + # Unicode using different encodings, use EncodingDetector to + # iterate over the encodings, and tell lxml to try to parse + # the document as each one in turn. + is_html = not self.is_xml + try_encodings = [user_specified_encoding, document_declared_encoding] + detector = EncodingDetector(markup, try_encodings, is_html) + for encoding in detector.encodings: + yield (detector.markup, encoding, document_declared_encoding, False) + + def feed(self, markup): + if isinstance(markup, bytes): + markup = BytesIO(markup) + elif isinstance(markup, unicode): + markup = StringIO(markup) + + # Call feed() at least once, even if the markup is empty, + # or the parser won't be initialized. + data = markup.read(self.CHUNK_SIZE) + try: + self.parser = self.parser_for(self.soup.original_encoding) + self.parser.feed(data) + while len(data) != 0: + # Now call feed() on the rest of the data, chunk by chunk. + data = markup.read(self.CHUNK_SIZE) + if len(data) != 0: + self.parser.feed(data) + self.parser.close() + except (UnicodeDecodeError, LookupError, etree.ParserError), e: + raise ParserRejectedMarkup(str(e)) + + def close(self): + self.nsmaps = [self.DEFAULT_NSMAPS] + + def start(self, name, attrs, nsmap={}): + # Make sure attrs is a mutable dict--lxml may send an immutable dictproxy. + attrs = dict(attrs) + nsprefix = None + # Invert each namespace map as it comes in. + if len(self.nsmaps) > 1: + # There are no new namespaces for this tag, but + # non-default namespaces are in play, so we need a + # separate tag stack to know when they end. + self.nsmaps.append(None) + elif len(nsmap) > 0: + # A new namespace mapping has come into play. + inverted_nsmap = dict((value, key) for key, value in nsmap.items()) + self.nsmaps.append(inverted_nsmap) + # Also treat the namespace mapping as a set of attributes on the + # tag, so we can recreate it later. + attrs = attrs.copy() + for prefix, namespace in nsmap.items(): + attribute = NamespacedAttribute( + "xmlns", prefix, "http://www.w3.org/2000/xmlns/") + attrs[attribute] = namespace + + # Namespaces are in play. Find any attributes that came in + # from lxml with namespaces attached to their names, and + # turn then into NamespacedAttribute objects. + new_attrs = {} + for attr, value in attrs.items(): + namespace, attr = self._getNsTag(attr) + if namespace is None: + new_attrs[attr] = value + else: + nsprefix = self._prefix_for_namespace(namespace) + attr = NamespacedAttribute(nsprefix, attr, namespace) + new_attrs[attr] = value + attrs = new_attrs + + namespace, name = self._getNsTag(name) + nsprefix = self._prefix_for_namespace(namespace) + self.soup.handle_starttag(name, namespace, nsprefix, attrs) + + def _prefix_for_namespace(self, namespace): + """Find the currently active prefix for the given namespace.""" + if namespace is None: + return None + for inverted_nsmap in reversed(self.nsmaps): + if inverted_nsmap is not None and namespace in inverted_nsmap: + return inverted_nsmap[namespace] + return None + + def end(self, name): + self.soup.endData() + completed_tag = self.soup.tagStack[-1] + namespace, name = self._getNsTag(name) + nsprefix = None + if namespace is not None: + for inverted_nsmap in reversed(self.nsmaps): + if inverted_nsmap is not None and namespace in inverted_nsmap: + nsprefix = inverted_nsmap[namespace] + break + self.soup.handle_endtag(name, nsprefix) + if len(self.nsmaps) > 1: + # This tag, or one of its parents, introduced a namespace + # mapping, so pop it off the stack. + self.nsmaps.pop() + + def pi(self, target, data): + pass + + def data(self, content): + self.soup.handle_data(content) + + def doctype(self, name, pubid, system): + self.soup.endData() + doctype = Doctype.for_name_and_ids(name, pubid, system) + self.soup.object_was_parsed(doctype) + + def comment(self, content): + "Handle comments as Comment objects." + self.soup.endData() + self.soup.handle_data(content) + self.soup.endData(Comment) + + def test_fragment_to_document(self, fragment): + """See `TreeBuilder`.""" + return u'\n%s' % fragment + + +class LXMLTreeBuilder(HTMLTreeBuilder, LXMLTreeBuilderForXML): + + features = [LXML, HTML, FAST, PERMISSIVE] + is_xml = False + + def default_parser(self, encoding): + return etree.HTMLParser + + def feed(self, markup): + encoding = self.soup.original_encoding + try: + self.parser = self.parser_for(encoding) + self.parser.feed(markup) + self.parser.close() + except (UnicodeDecodeError, LookupError, etree.ParserError), e: + raise ParserRejectedMarkup(str(e)) + + + def test_fragment_to_document(self, fragment): + """See `TreeBuilder`.""" + return u'%s' % fragment diff --git a/bs4/dammit.py b/bs4/dammit.py new file mode 100644 index 0000000..59640b7 --- /dev/null +++ b/bs4/dammit.py @@ -0,0 +1,829 @@ +# -*- coding: utf-8 -*- +"""Beautiful Soup bonus library: Unicode, Dammit + +This library converts a bytestream to Unicode through any means +necessary. It is heavily based on code from Mark Pilgrim's Universal +Feed Parser. It works best on XML and XML, but it does not rewrite the +XML or HTML to reflect a new encoding; that's the tree builder's job. +""" + +import codecs +from htmlentitydefs import codepoint2name +import re +import logging +import string + +# Import a library to autodetect character encodings. +chardet_type = None +try: + # First try the fast C implementation. + # PyPI package: cchardet + import cchardet + def chardet_dammit(s): + return cchardet.detect(s)['encoding'] +except ImportError: + try: + # Fall back to the pure Python implementation + # Debian package: python-chardet + # PyPI package: chardet + import chardet + def chardet_dammit(s): + return chardet.detect(s)['encoding'] + #import chardet.constants + #chardet.constants._debug = 1 + except ImportError: + # No chardet available. + def chardet_dammit(s): + return None + +# Available from http://cjkpython.i18n.org/. +try: + import iconv_codec +except ImportError: + pass + +xml_encoding_re = re.compile( + '^<\?.*encoding=[\'"](.*?)[\'"].*\?>'.encode(), re.I) +html_meta_re = re.compile( + '<\s*meta[^>]+charset\s*=\s*["\']?([^>]*?)[ /;\'">]'.encode(), re.I) + +class EntitySubstitution(object): + + """Substitute XML or HTML entities for the corresponding characters.""" + + def _populate_class_variables(): + lookup = {} + reverse_lookup = {} + characters_for_re = [] + for codepoint, name in list(codepoint2name.items()): + character = unichr(codepoint) + if codepoint != 34: + # There's no point in turning the quotation mark into + # ", unless it happens within an attribute value, which + # is handled elsewhere. + characters_for_re.append(character) + lookup[character] = name + # But we do want to turn " into the quotation mark. + reverse_lookup[name] = character + re_definition = "[%s]" % "".join(characters_for_re) + return lookup, reverse_lookup, re.compile(re_definition) + (CHARACTER_TO_HTML_ENTITY, HTML_ENTITY_TO_CHARACTER, + CHARACTER_TO_HTML_ENTITY_RE) = _populate_class_variables() + + CHARACTER_TO_XML_ENTITY = { + "'": "apos", + '"': "quot", + "&": "amp", + "<": "lt", + ">": "gt", + } + + BARE_AMPERSAND_OR_BRACKET = re.compile("([<>]|" + "&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)" + ")") + + AMPERSAND_OR_BRACKET = re.compile("([<>&])") + + @classmethod + def _substitute_html_entity(cls, matchobj): + entity = cls.CHARACTER_TO_HTML_ENTITY.get(matchobj.group(0)) + return "&%s;" % entity + + @classmethod + def _substitute_xml_entity(cls, matchobj): + """Used with a regular expression to substitute the + appropriate XML entity for an XML special character.""" + entity = cls.CHARACTER_TO_XML_ENTITY[matchobj.group(0)] + return "&%s;" % entity + + @classmethod + def quoted_attribute_value(self, value): + """Make a value into a quoted XML attribute, possibly escaping it. + + Most strings will be quoted using double quotes. + + Bob's Bar -> "Bob's Bar" + + If a string contains double quotes, it will be quoted using + single quotes. + + Welcome to "my bar" -> 'Welcome to "my bar"' + + If a string contains both single and double quotes, the + double quotes will be escaped, and the string will be quoted + using double quotes. + + Welcome to "Bob's Bar" -> "Welcome to "Bob's bar" + """ + quote_with = '"' + if '"' in value: + if "'" in value: + # The string contains both single and double + # quotes. Turn the double quotes into + # entities. We quote the double quotes rather than + # the single quotes because the entity name is + # """ whether this is HTML or XML. If we + # quoted the single quotes, we'd have to decide + # between ' and &squot;. + replace_with = """ + value = value.replace('"', replace_with) + else: + # There are double quotes but no single quotes. + # We can use single quotes to quote the attribute. + quote_with = "'" + return quote_with + value + quote_with + + @classmethod + def substitute_xml(cls, value, make_quoted_attribute=False): + """Substitute XML entities for special XML characters. + + :param value: A string to be substituted. The less-than sign + will become <, the greater-than sign will become >, + and any ampersands will become &. If you want ampersands + that appear to be part of an entity definition to be left + alone, use substitute_xml_containing_entities() instead. + + :param make_quoted_attribute: If True, then the string will be + quoted, as befits an attribute value. + """ + # Escape angle brackets and ampersands. + value = cls.AMPERSAND_OR_BRACKET.sub( + cls._substitute_xml_entity, value) + + if make_quoted_attribute: + value = cls.quoted_attribute_value(value) + return value + + @classmethod + def substitute_xml_containing_entities( + cls, value, make_quoted_attribute=False): + """Substitute XML entities for special XML characters. + + :param value: A string to be substituted. The less-than sign will + become <, the greater-than sign will become >, and any + ampersands that are not part of an entity defition will + become &. + + :param make_quoted_attribute: If True, then the string will be + quoted, as befits an attribute value. + """ + # Escape angle brackets, and ampersands that aren't part of + # entities. + value = cls.BARE_AMPERSAND_OR_BRACKET.sub( + cls._substitute_xml_entity, value) + + if make_quoted_attribute: + value = cls.quoted_attribute_value(value) + return value + + @classmethod + def substitute_html(cls, s): + """Replace certain Unicode characters with named HTML entities. + + This differs from data.encode(encoding, 'xmlcharrefreplace') + in that the goal is to make the result more readable (to those + with ASCII displays) rather than to recover from + errors. There's absolutely nothing wrong with a UTF-8 string + containg a LATIN SMALL LETTER E WITH ACUTE, but replacing that + character with "é" will make it more readable to some + people. + """ + return cls.CHARACTER_TO_HTML_ENTITY_RE.sub( + cls._substitute_html_entity, s) + + +class EncodingDetector: + """Suggests a number of possible encodings for a bytestring. + + Order of precedence: + + 1. Encodings you specifically tell EncodingDetector to try first + (the override_encodings argument to the constructor). + + 2. An encoding declared within the bytestring itself, either in an + XML declaration (if the bytestring is to be interpreted as an XML + document), or in a tag (if the bytestring is to be + interpreted as an HTML document.) + + 3. An encoding detected through textual analysis by chardet, + cchardet, or a similar external library. + + 4. UTF-8. + + 5. Windows-1252. + """ + def __init__(self, markup, override_encodings=None, is_html=False): + self.override_encodings = override_encodings or [] + self.chardet_encoding = None + self.is_html = is_html + self.declared_encoding = None + + # First order of business: strip a byte-order mark. + self.markup, self.sniffed_encoding = self.strip_byte_order_mark(markup) + + def _usable(self, encoding, tried): + if encoding is not None: + encoding = encoding.lower() + if encoding not in tried: + tried.add(encoding) + return True + return False + + @property + def encodings(self): + """Yield a number of encodings that might work for this markup.""" + tried = set() + for e in self.override_encodings: + if self._usable(e, tried): + yield e + + # Did the document originally start with a byte-order mark + # that indicated its encoding? + if self._usable(self.sniffed_encoding, tried): + yield self.sniffed_encoding + + # Look within the document for an XML or HTML encoding + # declaration. + if self.declared_encoding is None: + self.declared_encoding = self.find_declared_encoding( + self.markup, self.is_html) + if self._usable(self.declared_encoding, tried): + yield self.declared_encoding + + # Use third-party character set detection to guess at the + # encoding. + if self.chardet_encoding is None: + self.chardet_encoding = chardet_dammit(self.markup) + if self._usable(self.chardet_encoding, tried): + yield self.chardet_encoding + + # As a last-ditch effort, try utf-8 and windows-1252. + for e in ('utf-8', 'windows-1252'): + if self._usable(e, tried): + yield e + + @classmethod + def strip_byte_order_mark(cls, data): + """If a byte-order mark is present, strip it and return the encoding it implies.""" + encoding = None + if (len(data) >= 4) and (data[:2] == b'\xfe\xff') \ + and (data[2:4] != '\x00\x00'): + encoding = 'utf-16be' + data = data[2:] + elif (len(data) >= 4) and (data[:2] == b'\xff\xfe') \ + and (data[2:4] != '\x00\x00'): + encoding = 'utf-16le' + data = data[2:] + elif data[:3] == b'\xef\xbb\xbf': + encoding = 'utf-8' + data = data[3:] + elif data[:4] == b'\x00\x00\xfe\xff': + encoding = 'utf-32be' + data = data[4:] + elif data[:4] == b'\xff\xfe\x00\x00': + encoding = 'utf-32le' + data = data[4:] + return data, encoding + + @classmethod + def find_declared_encoding(cls, markup, is_html=False, search_entire_document=False): + """Given a document, tries to find its declared encoding. + + An XML encoding is declared at the beginning of the document. + + An HTML encoding is declared in a tag, hopefully near the + beginning of the document. + """ + if search_entire_document: + xml_endpos = html_endpos = len(markup) + else: + xml_endpos = 1024 + html_endpos = max(2048, int(len(markup) * 0.05)) + + declared_encoding = None + declared_encoding_match = xml_encoding_re.search(markup, endpos=xml_endpos) + if not declared_encoding_match and is_html: + declared_encoding_match = html_meta_re.search(markup, endpos=html_endpos) + if declared_encoding_match is not None: + declared_encoding = declared_encoding_match.groups()[0].decode( + 'ascii') + if declared_encoding: + return declared_encoding.lower() + return None + +class UnicodeDammit: + """A class for detecting the encoding of a *ML document and + converting it to a Unicode string. If the source encoding is + windows-1252, can replace MS smart quotes with their HTML or XML + equivalents.""" + + # This dictionary maps commonly seen values for "charset" in HTML + # meta tags to the corresponding Python codec names. It only covers + # values that aren't in Python's aliases and can't be determined + # by the heuristics in find_codec. + CHARSET_ALIASES = {"macintosh": "mac-roman", + "x-sjis": "shift-jis"} + + ENCODINGS_WITH_SMART_QUOTES = [ + "windows-1252", + "iso-8859-1", + "iso-8859-2", + ] + + def __init__(self, markup, override_encodings=[], + smart_quotes_to=None, is_html=False): + self.smart_quotes_to = smart_quotes_to + self.tried_encodings = [] + self.contains_replacement_characters = False + self.is_html = is_html + + self.detector = EncodingDetector(markup, override_encodings, is_html) + + # Short-circuit if the data is in Unicode to begin with. + if isinstance(markup, unicode) or markup == '': + self.markup = markup + self.unicode_markup = unicode(markup) + self.original_encoding = None + return + + # The encoding detector may have stripped a byte-order mark. + # Use the stripped markup from this point on. + self.markup = self.detector.markup + + u = None + for encoding in self.detector.encodings: + markup = self.detector.markup + u = self._convert_from(encoding) + if u is not None: + break + + if not u: + # None of the encodings worked. As an absolute last resort, + # try them again with character replacement. + + for encoding in self.detector.encodings: + if encoding != "ascii": + u = self._convert_from(encoding, "replace") + if u is not None: + logging.warning( + "Some characters could not be decoded, and were " + "replaced with REPLACEMENT CHARACTER.") + self.contains_replacement_characters = True + break + + # If none of that worked, we could at this point force it to + # ASCII, but that would destroy so much data that I think + # giving up is better. + self.unicode_markup = u + if not u: + self.original_encoding = None + + def _sub_ms_char(self, match): + """Changes a MS smart quote character to an XML or HTML + entity, or an ASCII character.""" + orig = match.group(1) + if self.smart_quotes_to == 'ascii': + sub = self.MS_CHARS_TO_ASCII.get(orig).encode() + else: + sub = self.MS_CHARS.get(orig) + if type(sub) == tuple: + if self.smart_quotes_to == 'xml': + sub = '&#x'.encode() + sub[1].encode() + ';'.encode() + else: + sub = '&'.encode() + sub[0].encode() + ';'.encode() + else: + sub = sub.encode() + return sub + + def _convert_from(self, proposed, errors="strict"): + proposed = self.find_codec(proposed) + if not proposed or (proposed, errors) in self.tried_encodings: + return None + self.tried_encodings.append((proposed, errors)) + markup = self.markup + # Convert smart quotes to HTML if coming from an encoding + # that might have them. + if (self.smart_quotes_to is not None + and proposed in self.ENCODINGS_WITH_SMART_QUOTES): + smart_quotes_re = b"([\x80-\x9f])" + smart_quotes_compiled = re.compile(smart_quotes_re) + markup = smart_quotes_compiled.sub(self._sub_ms_char, markup) + + try: + #print "Trying to convert document to %s (errors=%s)" % ( + # proposed, errors) + u = self._to_unicode(markup, proposed, errors) + self.markup = u + self.original_encoding = proposed + except Exception as e: + #print "That didn't work!" + #print e + return None + #print "Correct encoding: %s" % proposed + return self.markup + + def _to_unicode(self, data, encoding, errors="strict"): + '''Given a string and its encoding, decodes the string into Unicode. + %encoding is a string recognized by encodings.aliases''' + return unicode(data, encoding, errors) + + @property + def declared_html_encoding(self): + if not self.is_html: + return None + return self.detector.declared_encoding + + def find_codec(self, charset): + value = (self._codec(self.CHARSET_ALIASES.get(charset, charset)) + or (charset and self._codec(charset.replace("-", ""))) + or (charset and self._codec(charset.replace("-", "_"))) + or (charset and charset.lower()) + or charset + ) + if value: + return value.lower() + return None + + def _codec(self, charset): + if not charset: + return charset + codec = None + try: + codecs.lookup(charset) + codec = charset + except (LookupError, ValueError): + pass + return codec + + + # A partial mapping of ISO-Latin-1 to HTML entities/XML numeric entities. + MS_CHARS = {b'\x80': ('euro', '20AC'), + b'\x81': ' ', + b'\x82': ('sbquo', '201A'), + b'\x83': ('fnof', '192'), + b'\x84': ('bdquo', '201E'), + b'\x85': ('hellip', '2026'), + b'\x86': ('dagger', '2020'), + b'\x87': ('Dagger', '2021'), + b'\x88': ('circ', '2C6'), + b'\x89': ('permil', '2030'), + b'\x8A': ('Scaron', '160'), + b'\x8B': ('lsaquo', '2039'), + b'\x8C': ('OElig', '152'), + b'\x8D': '?', + b'\x8E': ('#x17D', '17D'), + b'\x8F': '?', + b'\x90': '?', + b'\x91': ('lsquo', '2018'), + b'\x92': ('rsquo', '2019'), + b'\x93': ('ldquo', '201C'), + b'\x94': ('rdquo', '201D'), + b'\x95': ('bull', '2022'), + b'\x96': ('ndash', '2013'), + b'\x97': ('mdash', '2014'), + b'\x98': ('tilde', '2DC'), + b'\x99': ('trade', '2122'), + b'\x9a': ('scaron', '161'), + b'\x9b': ('rsaquo', '203A'), + b'\x9c': ('oelig', '153'), + b'\x9d': '?', + b'\x9e': ('#x17E', '17E'), + b'\x9f': ('Yuml', ''),} + + # A parochial partial mapping of ISO-Latin-1 to ASCII. Contains + # horrors like stripping diacritical marks to turn á into a, but also + # contains non-horrors like turning “ into ". + MS_CHARS_TO_ASCII = { + b'\x80' : 'EUR', + b'\x81' : ' ', + b'\x82' : ',', + b'\x83' : 'f', + b'\x84' : ',,', + b'\x85' : '...', + b'\x86' : '+', + b'\x87' : '++', + b'\x88' : '^', + b'\x89' : '%', + b'\x8a' : 'S', + b'\x8b' : '<', + b'\x8c' : 'OE', + b'\x8d' : '?', + b'\x8e' : 'Z', + b'\x8f' : '?', + b'\x90' : '?', + b'\x91' : "'", + b'\x92' : "'", + b'\x93' : '"', + b'\x94' : '"', + b'\x95' : '*', + b'\x96' : '-', + b'\x97' : '--', + b'\x98' : '~', + b'\x99' : '(TM)', + b'\x9a' : 's', + b'\x9b' : '>', + b'\x9c' : 'oe', + b'\x9d' : '?', + b'\x9e' : 'z', + b'\x9f' : 'Y', + b'\xa0' : ' ', + b'\xa1' : '!', + b'\xa2' : 'c', + b'\xa3' : 'GBP', + b'\xa4' : '$', #This approximation is especially parochial--this is the + #generic currency symbol. + b'\xa5' : 'YEN', + b'\xa6' : '|', + b'\xa7' : 'S', + b'\xa8' : '..', + b'\xa9' : '', + b'\xaa' : '(th)', + b'\xab' : '<<', + b'\xac' : '!', + b'\xad' : ' ', + b'\xae' : '(R)', + b'\xaf' : '-', + b'\xb0' : 'o', + b'\xb1' : '+-', + b'\xb2' : '2', + b'\xb3' : '3', + b'\xb4' : ("'", 'acute'), + b'\xb5' : 'u', + b'\xb6' : 'P', + b'\xb7' : '*', + b'\xb8' : ',', + b'\xb9' : '1', + b'\xba' : '(th)', + b'\xbb' : '>>', + b'\xbc' : '1/4', + b'\xbd' : '1/2', + b'\xbe' : '3/4', + b'\xbf' : '?', + b'\xc0' : 'A', + b'\xc1' : 'A', + b'\xc2' : 'A', + b'\xc3' : 'A', + b'\xc4' : 'A', + b'\xc5' : 'A', + b'\xc6' : 'AE', + b'\xc7' : 'C', + b'\xc8' : 'E', + b'\xc9' : 'E', + b'\xca' : 'E', + b'\xcb' : 'E', + b'\xcc' : 'I', + b'\xcd' : 'I', + b'\xce' : 'I', + b'\xcf' : 'I', + b'\xd0' : 'D', + b'\xd1' : 'N', + b'\xd2' : 'O', + b'\xd3' : 'O', + b'\xd4' : 'O', + b'\xd5' : 'O', + b'\xd6' : 'O', + b'\xd7' : '*', + b'\xd8' : 'O', + b'\xd9' : 'U', + b'\xda' : 'U', + b'\xdb' : 'U', + b'\xdc' : 'U', + b'\xdd' : 'Y', + b'\xde' : 'b', + b'\xdf' : 'B', + b'\xe0' : 'a', + b'\xe1' : 'a', + b'\xe2' : 'a', + b'\xe3' : 'a', + b'\xe4' : 'a', + b'\xe5' : 'a', + b'\xe6' : 'ae', + b'\xe7' : 'c', + b'\xe8' : 'e', + b'\xe9' : 'e', + b'\xea' : 'e', + b'\xeb' : 'e', + b'\xec' : 'i', + b'\xed' : 'i', + b'\xee' : 'i', + b'\xef' : 'i', + b'\xf0' : 'o', + b'\xf1' : 'n', + b'\xf2' : 'o', + b'\xf3' : 'o', + b'\xf4' : 'o', + b'\xf5' : 'o', + b'\xf6' : 'o', + b'\xf7' : '/', + b'\xf8' : 'o', + b'\xf9' : 'u', + b'\xfa' : 'u', + b'\xfb' : 'u', + b'\xfc' : 'u', + b'\xfd' : 'y', + b'\xfe' : 'b', + b'\xff' : 'y', + } + + # A map used when removing rogue Windows-1252/ISO-8859-1 + # characters in otherwise UTF-8 documents. + # + # Note that \x81, \x8d, \x8f, \x90, and \x9d are undefined in + # Windows-1252. + WINDOWS_1252_TO_UTF8 = { + 0x80 : b'\xe2\x82\xac', # € + 0x82 : b'\xe2\x80\x9a', # ‚ + 0x83 : b'\xc6\x92', # ƒ + 0x84 : b'\xe2\x80\x9e', # „ + 0x85 : b'\xe2\x80\xa6', # … + 0x86 : b'\xe2\x80\xa0', # † + 0x87 : b'\xe2\x80\xa1', # ‡ + 0x88 : b'\xcb\x86', # ˆ + 0x89 : b'\xe2\x80\xb0', # ‰ + 0x8a : b'\xc5\xa0', # Š + 0x8b : b'\xe2\x80\xb9', # ‹ + 0x8c : b'\xc5\x92', # Œ + 0x8e : b'\xc5\xbd', # Ž + 0x91 : b'\xe2\x80\x98', # ‘ + 0x92 : b'\xe2\x80\x99', # ’ + 0x93 : b'\xe2\x80\x9c', # “ + 0x94 : b'\xe2\x80\x9d', # ” + 0x95 : b'\xe2\x80\xa2', # • + 0x96 : b'\xe2\x80\x93', # – + 0x97 : b'\xe2\x80\x94', # — + 0x98 : b'\xcb\x9c', # ˜ + 0x99 : b'\xe2\x84\xa2', # ™ + 0x9a : b'\xc5\xa1', # š + 0x9b : b'\xe2\x80\xba', # › + 0x9c : b'\xc5\x93', # œ + 0x9e : b'\xc5\xbe', # ž + 0x9f : b'\xc5\xb8', # Ÿ + 0xa0 : b'\xc2\xa0', #   + 0xa1 : b'\xc2\xa1', # ¡ + 0xa2 : b'\xc2\xa2', # ¢ + 0xa3 : b'\xc2\xa3', # £ + 0xa4 : b'\xc2\xa4', # ¤ + 0xa5 : b'\xc2\xa5', # ¥ + 0xa6 : b'\xc2\xa6', # ¦ + 0xa7 : b'\xc2\xa7', # § + 0xa8 : b'\xc2\xa8', # ¨ + 0xa9 : b'\xc2\xa9', # © + 0xaa : b'\xc2\xaa', # ª + 0xab : b'\xc2\xab', # « + 0xac : b'\xc2\xac', # ¬ + 0xad : b'\xc2\xad', # ­ + 0xae : b'\xc2\xae', # ® + 0xaf : b'\xc2\xaf', # ¯ + 0xb0 : b'\xc2\xb0', # ° + 0xb1 : b'\xc2\xb1', # ± + 0xb2 : b'\xc2\xb2', # ² + 0xb3 : b'\xc2\xb3', # ³ + 0xb4 : b'\xc2\xb4', # ´ + 0xb5 : b'\xc2\xb5', # µ + 0xb6 : b'\xc2\xb6', # ¶ + 0xb7 : b'\xc2\xb7', # · + 0xb8 : b'\xc2\xb8', # ¸ + 0xb9 : b'\xc2\xb9', # ¹ + 0xba : b'\xc2\xba', # º + 0xbb : b'\xc2\xbb', # » + 0xbc : b'\xc2\xbc', # ¼ + 0xbd : b'\xc2\xbd', # ½ + 0xbe : b'\xc2\xbe', # ¾ + 0xbf : b'\xc2\xbf', # ¿ + 0xc0 : b'\xc3\x80', # À + 0xc1 : b'\xc3\x81', # Á + 0xc2 : b'\xc3\x82', #  + 0xc3 : b'\xc3\x83', # à + 0xc4 : b'\xc3\x84', # Ä + 0xc5 : b'\xc3\x85', # Å + 0xc6 : b'\xc3\x86', # Æ + 0xc7 : b'\xc3\x87', # Ç + 0xc8 : b'\xc3\x88', # È + 0xc9 : b'\xc3\x89', # É + 0xca : b'\xc3\x8a', # Ê + 0xcb : b'\xc3\x8b', # Ë + 0xcc : b'\xc3\x8c', # Ì + 0xcd : b'\xc3\x8d', # Í + 0xce : b'\xc3\x8e', # Î + 0xcf : b'\xc3\x8f', # Ï + 0xd0 : b'\xc3\x90', # Ð + 0xd1 : b'\xc3\x91', # Ñ + 0xd2 : b'\xc3\x92', # Ò + 0xd3 : b'\xc3\x93', # Ó + 0xd4 : b'\xc3\x94', # Ô + 0xd5 : b'\xc3\x95', # Õ + 0xd6 : b'\xc3\x96', # Ö + 0xd7 : b'\xc3\x97', # × + 0xd8 : b'\xc3\x98', # Ø + 0xd9 : b'\xc3\x99', # Ù + 0xda : b'\xc3\x9a', # Ú + 0xdb : b'\xc3\x9b', # Û + 0xdc : b'\xc3\x9c', # Ü + 0xdd : b'\xc3\x9d', # Ý + 0xde : b'\xc3\x9e', # Þ + 0xdf : b'\xc3\x9f', # ß + 0xe0 : b'\xc3\xa0', # à + 0xe1 : b'\xa1', # á + 0xe2 : b'\xc3\xa2', # â + 0xe3 : b'\xc3\xa3', # ã + 0xe4 : b'\xc3\xa4', # ä + 0xe5 : b'\xc3\xa5', # å + 0xe6 : b'\xc3\xa6', # æ + 0xe7 : b'\xc3\xa7', # ç + 0xe8 : b'\xc3\xa8', # è + 0xe9 : b'\xc3\xa9', # é + 0xea : b'\xc3\xaa', # ê + 0xeb : b'\xc3\xab', # ë + 0xec : b'\xc3\xac', # ì + 0xed : b'\xc3\xad', # í + 0xee : b'\xc3\xae', # î + 0xef : b'\xc3\xaf', # ï + 0xf0 : b'\xc3\xb0', # ð + 0xf1 : b'\xc3\xb1', # ñ + 0xf2 : b'\xc3\xb2', # ò + 0xf3 : b'\xc3\xb3', # ó + 0xf4 : b'\xc3\xb4', # ô + 0xf5 : b'\xc3\xb5', # õ + 0xf6 : b'\xc3\xb6', # ö + 0xf7 : b'\xc3\xb7', # ÷ + 0xf8 : b'\xc3\xb8', # ø + 0xf9 : b'\xc3\xb9', # ù + 0xfa : b'\xc3\xba', # ú + 0xfb : b'\xc3\xbb', # û + 0xfc : b'\xc3\xbc', # ü + 0xfd : b'\xc3\xbd', # ý + 0xfe : b'\xc3\xbe', # þ + } + + MULTIBYTE_MARKERS_AND_SIZES = [ + (0xc2, 0xdf, 2), # 2-byte characters start with a byte C2-DF + (0xe0, 0xef, 3), # 3-byte characters start with E0-EF + (0xf0, 0xf4, 4), # 4-byte characters start with F0-F4 + ] + + FIRST_MULTIBYTE_MARKER = MULTIBYTE_MARKERS_AND_SIZES[0][0] + LAST_MULTIBYTE_MARKER = MULTIBYTE_MARKERS_AND_SIZES[-1][1] + + @classmethod + def detwingle(cls, in_bytes, main_encoding="utf8", + embedded_encoding="windows-1252"): + """Fix characters from one encoding embedded in some other encoding. + + Currently the only situation supported is Windows-1252 (or its + subset ISO-8859-1), embedded in UTF-8. + + The input must be a bytestring. If you've already converted + the document to Unicode, you're too late. + + The output is a bytestring in which `embedded_encoding` + characters have been converted to their `main_encoding` + equivalents. + """ + if embedded_encoding.replace('_', '-').lower() not in ( + 'windows-1252', 'windows_1252'): + raise NotImplementedError( + "Windows-1252 and ISO-8859-1 are the only currently supported " + "embedded encodings.") + + if main_encoding.lower() not in ('utf8', 'utf-8'): + raise NotImplementedError( + "UTF-8 is the only currently supported main encoding.") + + byte_chunks = [] + + chunk_start = 0 + pos = 0 + while pos < len(in_bytes): + byte = in_bytes[pos] + if not isinstance(byte, int): + # Python 2.x + byte = ord(byte) + if (byte >= cls.FIRST_MULTIBYTE_MARKER + and byte <= cls.LAST_MULTIBYTE_MARKER): + # This is the start of a UTF-8 multibyte character. Skip + # to the end. + for start, end, size in cls.MULTIBYTE_MARKERS_AND_SIZES: + if byte >= start and byte <= end: + pos += size + break + elif byte >= 0x80 and byte in cls.WINDOWS_1252_TO_UTF8: + # We found a Windows-1252 character! + # Save the string up to this point as a chunk. + byte_chunks.append(in_bytes[chunk_start:pos]) + + # Now translate the Windows-1252 character into UTF-8 + # and add it as another, one-byte chunk. + byte_chunks.append(cls.WINDOWS_1252_TO_UTF8[byte]) + pos += 1 + chunk_start = pos + else: + # Go on to the next character. + pos += 1 + if chunk_start == 0: + # The string is unchanged. + return in_bytes + else: + # Store the final chunk. + byte_chunks.append(in_bytes[chunk_start:]) + return b''.join(byte_chunks) + diff --git a/bs4/diagnose.py b/bs4/diagnose.py new file mode 100644 index 0000000..4d0b00a --- /dev/null +++ b/bs4/diagnose.py @@ -0,0 +1,204 @@ +"""Diagnostic functions, mainly for use when doing tech support.""" +import cProfile +from StringIO import StringIO +from HTMLParser import HTMLParser +import bs4 +from bs4 import BeautifulSoup, __version__ +from bs4.builder import builder_registry + +import os +import pstats +import random +import tempfile +import time +import traceback +import sys +import cProfile + +def diagnose(data): + """Diagnostic suite for isolating common problems.""" + print "Diagnostic running on Beautiful Soup %s" % __version__ + print "Python version %s" % sys.version + + basic_parsers = ["html.parser", "html5lib", "lxml"] + for name in basic_parsers: + for builder in builder_registry.builders: + if name in builder.features: + break + else: + basic_parsers.remove(name) + print ( + "I noticed that %s is not installed. Installing it may help." % + name) + + if 'lxml' in basic_parsers: + basic_parsers.append(["lxml", "xml"]) + from lxml import etree + print "Found lxml version %s" % ".".join(map(str,etree.LXML_VERSION)) + + if 'html5lib' in basic_parsers: + import html5lib + print "Found html5lib version %s" % html5lib.__version__ + + if hasattr(data, 'read'): + data = data.read() + elif os.path.exists(data): + print '"%s" looks like a filename. Reading data from the file.' % data + data = open(data).read() + elif data.startswith("http:") or data.startswith("https:"): + print '"%s" looks like a URL. Beautiful Soup is not an HTTP client.' % data + print "You need to use some other library to get the document behind the URL, and feed that document to Beautiful Soup." + return + print + + for parser in basic_parsers: + print "Trying to parse your markup with %s" % parser + success = False + try: + soup = BeautifulSoup(data, parser) + success = True + except Exception, e: + print "%s could not parse the markup." % parser + traceback.print_exc() + if success: + print "Here's what %s did with the markup:" % parser + print soup.prettify() + + print "-" * 80 + +def lxml_trace(data, html=True, **kwargs): + """Print out the lxml events that occur during parsing. + + This lets you see how lxml parses a document when no Beautiful + Soup code is running. + """ + from lxml import etree + for event, element in etree.iterparse(StringIO(data), html=html, **kwargs): + print("%s, %4s, %s" % (event, element.tag, element.text)) + +class AnnouncingParser(HTMLParser): + """Announces HTMLParser parse events, without doing anything else.""" + + def _p(self, s): + print(s) + + def handle_starttag(self, name, attrs): + self._p("%s START" % name) + + def handle_endtag(self, name): + self._p("%s END" % name) + + def handle_data(self, data): + self._p("%s DATA" % data) + + def handle_charref(self, name): + self._p("%s CHARREF" % name) + + def handle_entityref(self, name): + self._p("%s ENTITYREF" % name) + + def handle_comment(self, data): + self._p("%s COMMENT" % data) + + def handle_decl(self, data): + self._p("%s DECL" % data) + + def unknown_decl(self, data): + self._p("%s UNKNOWN-DECL" % data) + + def handle_pi(self, data): + self._p("%s PI" % data) + +def htmlparser_trace(data): + """Print out the HTMLParser events that occur during parsing. + + This lets you see how HTMLParser parses a document when no + Beautiful Soup code is running. + """ + parser = AnnouncingParser() + parser.feed(data) + +_vowels = "aeiou" +_consonants = "bcdfghjklmnpqrstvwxyz" + +def rword(length=5): + "Generate a random word-like string." + s = '' + for i in range(length): + if i % 2 == 0: + t = _consonants + else: + t = _vowels + s += random.choice(t) + return s + +def rsentence(length=4): + "Generate a random sentence-like string." + return " ".join(rword(random.randint(4,9)) for i in range(length)) + +def rdoc(num_elements=1000): + """Randomly generate an invalid HTML document.""" + tag_names = ['p', 'div', 'span', 'i', 'b', 'script', 'table'] + elements = [] + for i in range(num_elements): + choice = random.randint(0,3) + if choice == 0: + # New tag. + tag_name = random.choice(tag_names) + elements.append("<%s>" % tag_name) + elif choice == 1: + elements.append(rsentence(random.randint(1,4))) + elif choice == 2: + # Close a tag. + tag_name = random.choice(tag_names) + elements.append("" % tag_name) + return "" + "\n".join(elements) + "" + +def benchmark_parsers(num_elements=100000): + """Very basic head-to-head performance benchmark.""" + print "Comparative parser benchmark on Beautiful Soup %s" % __version__ + data = rdoc(num_elements) + print "Generated a large invalid HTML document (%d bytes)." % len(data) + + for parser in ["lxml", ["lxml", "html"], "html5lib", "html.parser"]: + success = False + try: + a = time.time() + soup = BeautifulSoup(data, parser) + b = time.time() + success = True + except Exception, e: + print "%s could not parse the markup." % parser + traceback.print_exc() + if success: + print "BS4+%s parsed the markup in %.2fs." % (parser, b-a) + + from lxml import etree + a = time.time() + etree.HTML(data) + b = time.time() + print "Raw lxml parsed the markup in %.2fs." % (b-a) + + import html5lib + parser = html5lib.HTMLParser() + a = time.time() + parser.parse(data) + b = time.time() + print "Raw html5lib parsed the markup in %.2fs." % (b-a) + +def profile(num_elements=100000, parser="lxml"): + + filehandle = tempfile.NamedTemporaryFile() + filename = filehandle.name + + data = rdoc(num_elements) + vars = dict(bs4=bs4, data=data, parser=parser) + cProfile.runctx('bs4.BeautifulSoup(data, parser)' , vars, vars, filename) + + stats = pstats.Stats(filename) + # stats.strip_dirs() + stats.sort_stats("cumulative") + stats.print_stats('_html5lib|bs4', 50) + +if __name__ == '__main__': + diagnose(sys.stdin.read()) diff --git a/bs4/element.py b/bs4/element.py new file mode 100644 index 0000000..da9afdf --- /dev/null +++ b/bs4/element.py @@ -0,0 +1,1611 @@ +import collections +import re +import sys +import warnings +from bs4.dammit import EntitySubstitution + +DEFAULT_OUTPUT_ENCODING = "utf-8" +PY3K = (sys.version_info[0] > 2) + +whitespace_re = re.compile("\s+") + +def _alias(attr): + """Alias one attribute name to another for backward compatibility""" + @property + def alias(self): + return getattr(self, attr) + + @alias.setter + def alias(self): + return setattr(self, attr) + return alias + + +class NamespacedAttribute(unicode): + + def __new__(cls, prefix, name, namespace=None): + if name is None: + obj = unicode.__new__(cls, prefix) + elif prefix is None: + # Not really namespaced. + obj = unicode.__new__(cls, name) + else: + obj = unicode.__new__(cls, prefix + ":" + name) + obj.prefix = prefix + obj.name = name + obj.namespace = namespace + return obj + +class AttributeValueWithCharsetSubstitution(unicode): + """A stand-in object for a character encoding specified in HTML.""" + +class CharsetMetaAttributeValue(AttributeValueWithCharsetSubstitution): + """A generic stand-in for the value of a meta tag's 'charset' attribute. + + When Beautiful Soup parses the markup '', the + value of the 'charset' attribute will be one of these objects. + """ + + def __new__(cls, original_value): + obj = unicode.__new__(cls, original_value) + obj.original_value = original_value + return obj + + def encode(self, encoding): + return encoding + + +class ContentMetaAttributeValue(AttributeValueWithCharsetSubstitution): + """A generic stand-in for the value of a meta tag's 'content' attribute. + + When Beautiful Soup parses the markup: + + + The value of the 'content' attribute will be one of these objects. + """ + + CHARSET_RE = re.compile("((^|;)\s*charset=)([^;]*)", re.M) + + def __new__(cls, original_value): + match = cls.CHARSET_RE.search(original_value) + if match is None: + # No substitution necessary. + return unicode.__new__(unicode, original_value) + + obj = unicode.__new__(cls, original_value) + obj.original_value = original_value + return obj + + def encode(self, encoding): + def rewrite(match): + return match.group(1) + encoding + return self.CHARSET_RE.sub(rewrite, self.original_value) + +class HTMLAwareEntitySubstitution(EntitySubstitution): + + """Entity substitution rules that are aware of some HTML quirks. + + Specifically, the contents of + + +
+

+ FanFictionDownLoader +

+ +
+ + +
+ +
+ +
+

Edit Config

+
+ Editing configuration for {{ nickname }}. +
+
+ +
+
+ +
+ +
+
+ +
+

Default System configuration

+
+{{ defaultsini }}
+
+
+ +
+ Powered by Google App Engine +

+ This is a web front-end to FanFictionDownLoader
+ Copyright © Fanficdownloader team +
+ +
+ + +
+
+ + diff --git a/epubmerge.py b/epubmerge.py new file mode 100644 index 0000000..f7e76b8 --- /dev/null +++ b/epubmerge.py @@ -0,0 +1,25 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# epubmerge.py 1.0 + +# Copyright 2011, Jim Miller + +# 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. + +if __name__ == "__main__": + print(''' +The this utility has been split out into it's own project. +See: http://code.google.com/p/epubmerge/ +...for a CLI epubmerge.py program and calibre plugin. +''') diff --git a/example.ini b/example.ini new file mode 100644 index 0000000..942cd60 --- /dev/null +++ b/example.ini @@ -0,0 +1,103 @@ +## This is an example of what your personal configuration might look +## 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. Uncomment by removing '#' in front of is_adult. In +## commandline version, this should go in your personal.ini, not +## defaults.ini. +#is_adult: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: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. +## 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. Here are a few examples. See defaults.ini +## for the full list. + +[www.twilighted.net] +#username:YourPenname +#password:YourPassword +## default is false +#collect_series: true + +[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 + +[www.twcslibrary.net] +#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 + + +## 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 diff --git a/fanficdownloader.zip b/fanficdownloader.zip new file mode 100644 index 0000000000000000000000000000000000000000..af014804bde29fc47672c845b3d720fc33016ddb GIT binary patch literal 863236 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

B&7qInaT<-w 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 literal 0 HcmV?d00001 diff --git a/fanficdownloader/BeautifulSoup.py b/fanficdownloader/BeautifulSoup.py new file mode 100644 index 0000000..8afb6bf --- /dev/null +++ b/fanficdownloader/BeautifulSoup.py @@ -0,0 +1,2017 @@ +"""Beautiful Soup +Elixir and Tonic +"The Screen-Scraper's Friend" +http://www.crummy.com/software/BeautifulSoup/ + +Beautiful Soup parses a (possibly invalid) XML or HTML document into a +tree representation. It provides methods and Pythonic idioms that make +it easy to navigate, search, and modify the tree. + +A well-formed XML/HTML document yields a well-formed data +structure. An ill-formed XML/HTML document yields a correspondingly +ill-formed data structure. If your document is only locally +well-formed, you can use this library to find and process the +well-formed part of it. + +Beautiful Soup works with Python 2.2 and up. It has no external +dependencies, but you'll have more success at converting data to UTF-8 +if you also install these three packages: + +* chardet, for auto-detecting character encodings + http://chardet.feedparser.org/ +* cjkcodecs and iconv_codec, which add more encodings to the ones supported + by stock Python. + http://cjkpython.i18n.org/ + +Beautiful Soup defines classes for two main parsing strategies: + + * BeautifulStoneSoup, for parsing XML, SGML, or your domain-specific + language that kind of looks like XML. + + * BeautifulSoup, for parsing run-of-the-mill HTML code, be it valid + or invalid. This class has web browser-like heuristics for + obtaining a sensible parse tree in the face of common HTML errors. + +Beautiful Soup also defines a class (UnicodeDammit) for autodetecting +the encoding of an HTML or XML document, and converting it to +Unicode. Much of this code is taken from Mark Pilgrim's Universal Feed Parser. + +For more than you ever wanted to know about Beautiful Soup, see the +documentation: +http://www.crummy.com/software/BeautifulSoup/documentation.html + +Here, have some legalese: + +Copyright (c) 2004-2010, Leonard Richardson + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the the Beautiful Soup Consortium and All + Night Kosher Bakery nor the names of its contributors may be + used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE, DAMMIT. + +""" +from __future__ import generators + +__author__ = "Leonard Richardson (leonardr@segfault.org)" +__version__ = "3.2.0" +__copyright__ = "Copyright (c) 2004-2010 Leonard Richardson" +__license__ = "New-style BSD" + +from sgmllib import SGMLParser, SGMLParseError +import codecs +import markupbase +import types +import re +import sgmllib +try: + from htmlentitydefs import name2codepoint +except ImportError: + name2codepoint = {} +try: + set +except NameError: + from sets import Set as set + +#These hacks make Beautiful Soup able to parse XML with namespaces +sgmllib.tagfind = re.compile('[a-zA-Z][-_.:a-zA-Z0-9]*') +markupbase._declname_match = re.compile(r'[a-zA-Z][-_.:a-zA-Z0-9]*\s*').match + +DEFAULT_OUTPUT_ENCODING = "utf-8" + +def _match_css_class(str): + """Build a RE to match the given CSS class.""" + return re.compile(r"(^|.*\s)%s($|\s)" % str) + +# First, the classes that represent markup elements. + +class PageElement(object): + """Contains the navigational information for some part of the page + (either a tag or a piece of text)""" + + ## Added for FFDL. + bs3=True + + def setup(self, parent=None, previous=None): + """Sets up the initial relations between this element and + other elements.""" + self.parent = parent + self.previous = previous + self.next = None + self.previousSibling = None + self.nextSibling = None + if self.parent and self.parent.contents: + self.previousSibling = self.parent.contents[-1] + self.previousSibling.nextSibling = self + + def replaceWith(self, replaceWith): + oldParent = self.parent + myIndex = self.parent.index(self) + if hasattr(replaceWith, "parent")\ + and replaceWith.parent is self.parent: + # We're replacing this element with one of its siblings. + index = replaceWith.parent.index(replaceWith) + if index and index < myIndex: + # Furthermore, it comes before this element. That + # means that when we extract it, the index of this + # element will change. + myIndex = myIndex - 1 + self.extract() + oldParent.insert(myIndex, replaceWith) + + def replaceWithChildren(self): + myParent = self.parent + myIndex = self.parent.index(self) + self.extract() + reversedChildren = list(self.contents) + reversedChildren.reverse() + for child in reversedChildren: + myParent.insert(myIndex, child) + + def extract(self): + """Destructively rips this element out of the tree.""" + if self.parent: + try: + del self.parent.contents[self.parent.index(self)] + except ValueError: + pass + + #Find the two elements that would be next to each other if + #this element (and any children) hadn't been parsed. Connect + #the two. + lastChild = self._lastRecursiveChild() + nextElement = lastChild.next + + if self.previous: + self.previous.next = nextElement + if nextElement: + nextElement.previous = self.previous + self.previous = None + lastChild.next = None + + self.parent = None + if self.previousSibling: + self.previousSibling.nextSibling = self.nextSibling + if self.nextSibling: + self.nextSibling.previousSibling = self.previousSibling + self.previousSibling = self.nextSibling = None + return self + + def _lastRecursiveChild(self): + "Finds the last element beneath this object to be parsed." + lastChild = self + while hasattr(lastChild, 'contents') and lastChild.contents: + lastChild = lastChild.contents[-1] + return lastChild + + def insert(self, position, newChild): + if isinstance(newChild, basestring) \ + and not isinstance(newChild, NavigableString): + newChild = NavigableString(newChild) + + position = min(position, len(self.contents)) + if hasattr(newChild, 'parent') and newChild.parent is not None: + # We're 'inserting' an element that's already one + # of this object's children. + if newChild.parent is self: + index = self.index(newChild) + if index > position: + # Furthermore we're moving it further down the + # list of this object's children. That means that + # when we extract this element, our target index + # will jump down one. + position = position - 1 + newChild.extract() + + newChild.parent = self + previousChild = None + if position == 0: + newChild.previousSibling = None + newChild.previous = self + else: + previousChild = self.contents[position-1] + newChild.previousSibling = previousChild + newChild.previousSibling.nextSibling = newChild + newChild.previous = previousChild._lastRecursiveChild() + if newChild.previous: + newChild.previous.next = newChild + + newChildsLastElement = newChild._lastRecursiveChild() + + if position >= len(self.contents): + newChild.nextSibling = None + + parent = self + parentsNextSibling = None + while not parentsNextSibling: + parentsNextSibling = parent.nextSibling + parent = parent.parent + if not parent: # This is the last element in the document. + break + if parentsNextSibling: + newChildsLastElement.next = parentsNextSibling + else: + newChildsLastElement.next = None + else: + nextChild = self.contents[position] + newChild.nextSibling = nextChild + if newChild.nextSibling: + newChild.nextSibling.previousSibling = newChild + newChildsLastElement.next = nextChild + + if newChildsLastElement.next: + newChildsLastElement.next.previous = newChildsLastElement + self.contents.insert(position, newChild) + + def append(self, tag): + """Appends the given tag to the contents of this tag.""" + self.insert(len(self.contents), tag) + + def findNext(self, name=None, attrs={}, text=None, **kwargs): + """Returns the first item that matches the given criteria and + appears after this Tag in the document.""" + return self._findOne(self.findAllNext, name, attrs, text, **kwargs) + + def findAllNext(self, name=None, attrs={}, text=None, limit=None, + **kwargs): + """Returns all items that match the given criteria and appear + after this Tag in the document.""" + return self._findAll(name, attrs, text, limit, self.nextGenerator, + **kwargs) + + def findNextSibling(self, name=None, attrs={}, text=None, **kwargs): + """Returns the closest sibling to this Tag that matches the + given criteria and appears after this Tag in the document.""" + return self._findOne(self.findNextSiblings, name, attrs, text, + **kwargs) + + def findNextSiblings(self, name=None, attrs={}, text=None, limit=None, + **kwargs): + """Returns the siblings of this Tag that match the given + criteria and appear after this Tag in the document.""" + return self._findAll(name, attrs, text, limit, + self.nextSiblingGenerator, **kwargs) + fetchNextSiblings = findNextSiblings # Compatibility with pre-3.x + + def findPrevious(self, name=None, attrs={}, text=None, **kwargs): + """Returns the first item that matches the given criteria and + appears before this Tag in the document.""" + return self._findOne(self.findAllPrevious, name, attrs, text, **kwargs) + + def findAllPrevious(self, name=None, attrs={}, text=None, limit=None, + **kwargs): + """Returns all items that match the given criteria and appear + before this Tag in the document.""" + return self._findAll(name, attrs, text, limit, self.previousGenerator, + **kwargs) + fetchPrevious = findAllPrevious # Compatibility with pre-3.x + + def findPreviousSibling(self, name=None, attrs={}, text=None, **kwargs): + """Returns the closest sibling to this Tag that matches the + given criteria and appears before this Tag in the document.""" + return self._findOne(self.findPreviousSiblings, name, attrs, text, + **kwargs) + + def findPreviousSiblings(self, name=None, attrs={}, text=None, + limit=None, **kwargs): + """Returns the siblings of this Tag that match the given + criteria and appear before this Tag in the document.""" + return self._findAll(name, attrs, text, limit, + self.previousSiblingGenerator, **kwargs) + fetchPreviousSiblings = findPreviousSiblings # Compatibility with pre-3.x + + def findParent(self, name=None, attrs={}, **kwargs): + """Returns the closest parent of this Tag that matches the given + criteria.""" + # NOTE: We can't use _findOne because findParents takes a different + # set of arguments. + r = None + l = self.findParents(name, attrs, 1) + if l: + r = l[0] + return r + + def findParents(self, name=None, attrs={}, limit=None, **kwargs): + """Returns the parents of this Tag that match the given + criteria.""" + + return self._findAll(name, attrs, None, limit, self.parentGenerator, + **kwargs) + fetchParents = findParents # Compatibility with pre-3.x + + #These methods do the real heavy lifting. + + def _findOne(self, method, name, attrs, text, **kwargs): + r = None + l = method(name, attrs, text, 1, **kwargs) + if l: + r = l[0] + return r + + def _findAll(self, name, attrs, text, limit, generator, **kwargs): + "Iterates over a generator looking for things that match." + + if isinstance(name, SoupStrainer): + strainer = name + # (Possibly) special case some findAll*(...) searches + elif text is None and not limit and not attrs and not kwargs: + # findAll*(True) + if name is True: + return [element for element in generator() + if isinstance(element, Tag)] + # findAll*('tag-name') + elif isinstance(name, basestring): + return [element for element in generator() + if isinstance(element, Tag) and + element.name == name] + else: + strainer = SoupStrainer(name, attrs, text, **kwargs) + # Build a SoupStrainer + else: + strainer = SoupStrainer(name, attrs, text, **kwargs) + results = ResultSet(strainer) + g = generator() + while True: + try: + i = g.next() + except StopIteration: + break + if i: + found = strainer.search(i) + if found: + results.append(found) + if limit and len(results) >= limit: + break + return results + + #These Generators can be used to navigate starting from both + #NavigableStrings and Tags. + def nextGenerator(self): + i = self + while i is not None: + i = i.next + yield i + + def nextSiblingGenerator(self): + i = self + while i is not None: + i = i.nextSibling + yield i + + def previousGenerator(self): + i = self + while i is not None: + i = i.previous + yield i + + def previousSiblingGenerator(self): + i = self + while i is not None: + i = i.previousSibling + yield i + + def parentGenerator(self): + i = self + while i is not None: + i = i.parent + yield i + + # Utility methods + def substituteEncoding(self, str, encoding=None): + encoding = encoding or "utf-8" + return str.replace("%SOUP-ENCODING%", encoding) + + def toEncoding(self, s, encoding=None): + """Encodes an object to a string in some encoding, or to Unicode. + .""" + if isinstance(s, unicode): + if encoding: + s = s.encode(encoding) + elif isinstance(s, str): + if encoding: + s = s.encode(encoding) + else: + s = unicode(s) + else: + if encoding: + s = self.toEncoding(str(s), encoding) + else: + s = unicode(s) + return s + +class NavigableString(unicode, PageElement): + + def __new__(cls, value): + """Create a new NavigableString. + + When unpickling a NavigableString, this method is called with + the string in DEFAULT_OUTPUT_ENCODING. That encoding needs to be + passed in to the superclass's __new__ or the superclass won't know + how to handle non-ASCII characters. + """ + if isinstance(value, unicode): + return unicode.__new__(cls, value) + return unicode.__new__(cls, value, DEFAULT_OUTPUT_ENCODING) + + def __getnewargs__(self): + return (NavigableString.__str__(self),) + + def __getattr__(self, attr): + """text.string gives you text. This is for backwards + compatibility for Navigable*String, but for CData* it lets you + get the string without the CData wrapper.""" + if attr == 'string': + return self + else: + raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__.__name__, attr) + + def __unicode__(self): + return str(self).decode(DEFAULT_OUTPUT_ENCODING) + + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + if encoding: + return self.encode(encoding) + else: + return self + +class CData(NavigableString): + + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + return "" % NavigableString.__str__(self, encoding) + +class ProcessingInstruction(NavigableString): + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + output = self + if "%SOUP-ENCODING%" in output: + output = self.substituteEncoding(output, encoding) + return "" % self.toEncoding(output, encoding) + +class Comment(NavigableString): + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + return "" % NavigableString.__str__(self, encoding) + +class Declaration(NavigableString): + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + return "" % NavigableString.__str__(self, encoding) + +class Tag(PageElement): + + """Represents a found HTML tag with its attributes and contents.""" + + def _invert(h): + "Cheap function to invert a hash." + i = {} + for k,v in h.items(): + i[v] = k + return i + + XML_ENTITIES_TO_SPECIAL_CHARS = { "apos" : "'", + "quot" : '"', + "amp" : "&", + "lt" : "<", + "gt" : ">" } + + XML_SPECIAL_CHARS_TO_ENTITIES = _invert(XML_ENTITIES_TO_SPECIAL_CHARS) + + def _convertEntities(self, match): + """Used in a call to re.sub to replace HTML, XML, and numeric + entities with the appropriate Unicode characters. If HTML + entities are being converted, any unrecognized entities are + escaped.""" + x = match.group(1) + if self.convertHTMLEntities and x in name2codepoint: + return unichr(name2codepoint[x]) + elif x in self.XML_ENTITIES_TO_SPECIAL_CHARS: + if self.convertXMLEntities: + return self.XML_ENTITIES_TO_SPECIAL_CHARS[x] + else: + return u'&%s;' % x + elif len(x) > 0 and x[0] == '#': + # Handle numeric entities + if len(x) > 1 and x[1] == 'x': + return unichr(int(x[2:], 16)) + else: + return unichr(int(x[1:])) + + elif self.escapeUnrecognizedEntities: + return u'&%s;' % x + else: + return u'&%s;' % x + + def __init__(self, parser, name, attrs=None, parent=None, + previous=None): + "Basic constructor." + + # We don't actually store the parser object: that lets extracted + # chunks be garbage-collected + self.parserClass = parser.__class__ + self.isSelfClosing = parser.isSelfClosingTag(name) + self.name = name + if attrs is None: + attrs = [] + elif isinstance(attrs, dict): + attrs = attrs.items() + self.attrs = attrs + self.contents = [] + self.setup(parent, previous) + self.hidden = False + self.containsSubstitutions = False + self.convertHTMLEntities = parser.convertHTMLEntities + self.convertXMLEntities = parser.convertXMLEntities + self.escapeUnrecognizedEntities = parser.escapeUnrecognizedEntities + + # Convert any HTML, XML, or numeric entities in the attribute values. + convert = lambda(k, val): (k, + re.sub("&(#\d+|#x[0-9a-fA-F]+|\w+);", + self._convertEntities, + val)) + self.attrs = map(convert, self.attrs) + + def getString(self): + if (len(self.contents) == 1 + and isinstance(self.contents[0], NavigableString)): + return self.contents[0] + + def setString(self, string): + """Replace the contents of the tag with a string""" + self.clear() + self.append(string) + + string = property(getString, setString) + + def getText(self, separator=u""): + if not len(self.contents): + return u"" + stopNode = self._lastRecursiveChild().next + strings = [] + current = self.contents[0] + while current is not stopNode: + if isinstance(current, NavigableString): + strings.append(current.strip()) + current = current.next + return separator.join(strings) + + text = property(getText) + + def get(self, key, default=None): + """Returns the value of the 'key' attribute for the tag, or + the value given for 'default' if it doesn't have that + attribute.""" + return self._getAttrMap().get(key, default) + + def clear(self): + """Extract all children.""" + for child in self.contents[:]: + child.extract() + + def index(self, element): + for i, child in enumerate(self.contents): + if child is element: + return i + raise ValueError("Tag.index: element not in tag") + + def has_key(self, key): + return self._getAttrMap().has_key(key) + + def __getitem__(self, key): + """tag[key] returns the value of the 'key' attribute for the tag, + and throws an exception if it's not there.""" + return self._getAttrMap()[key] + + def __iter__(self): + "Iterating over a tag iterates over its contents." + return iter(self.contents) + + def __len__(self): + "The length of a tag is the length of its list of contents." + return len(self.contents) + + def __contains__(self, x): + return x in self.contents + + def __nonzero__(self): + "A tag is non-None even if it has no contents." + return True + + def __setitem__(self, key, value): + """Setting tag[key] sets the value of the 'key' attribute for the + tag.""" + self._getAttrMap() + self.attrMap[key] = value + found = False + for i in range(0, len(self.attrs)): + if self.attrs[i][0] == key: + self.attrs[i] = (key, value) + found = True + if not found: + self.attrs.append((key, value)) + self._getAttrMap()[key] = value + + def __delitem__(self, key): + "Deleting tag[key] deletes all 'key' attributes for the tag." + for item in self.attrs: + if item[0] == key: + self.attrs.remove(item) + #We don't break because bad HTML can define the same + #attribute multiple times. + self._getAttrMap() + if self.attrMap.has_key(key): + del self.attrMap[key] + + def __call__(self, *args, **kwargs): + """Calling a tag like a function is the same as calling its + findAll() method. Eg. tag('a') returns a list of all the A tags + found within this tag.""" + return apply(self.findAll, args, kwargs) + + def __getattr__(self, tag): + #print "Getattr %s.%s" % (self.__class__, tag) + if len(tag) > 3 and tag.rfind('Tag') == len(tag)-3: + return self.find(tag[:-3]) + elif tag.find('__') != 0: + return self.find(tag) + raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__, tag) + + def __eq__(self, other): + """Returns true iff this tag has the same name, the same attributes, + and the same contents (recursively) as the given tag. + + NOTE: right now this will return false if two tags have the + same attributes in a different order. Should this be fixed?""" + if other is self: + return True + if not hasattr(other, 'name') or not hasattr(other, 'attrs') or not hasattr(other, 'contents') or self.name != other.name or self.attrs != other.attrs or len(self) != len(other): + return False + for i in range(0, len(self.contents)): + if self.contents[i] != other.contents[i]: + return False + return True + + def __ne__(self, other): + """Returns true iff this tag is not identical to the other tag, + as defined in __eq__.""" + return not self == other + + def __repr__(self, encoding=DEFAULT_OUTPUT_ENCODING): + """Renders this tag as a string.""" + return self.__str__(encoding) + + def __unicode__(self): + return str(self).decode(DEFAULT_OUTPUT_ENCODING) + + BARE_AMPERSAND_OR_BRACKET = re.compile("([<>]|" + + "&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)" + + ")") + + def _sub_entity(self, x): + """Used with a regular expression to substitute the + appropriate XML entity for an XML special character.""" + return "&" + self.XML_SPECIAL_CHARS_TO_ENTITIES[x.group(0)[0]] + ";" + + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING, + prettyPrint=False, indentLevel=0): + """Returns a string or Unicode representation of this tag and + its contents. To get Unicode, pass None for encoding. + + NOTE: since Python's HTML parser consumes whitespace, this + method is not certain to reproduce the whitespace present in + the original string.""" + + encodedName = self.toEncoding(self.name, encoding) + + attrs = [] + if self.attrs: + for key, val in self.attrs: + fmt = '%s="%s"' + if isinstance(val, basestring): + if self.containsSubstitutions and '%SOUP-ENCODING%' in val: + val = self.substituteEncoding(val, encoding) + + # The attribute value either: + # + # * Contains no embedded double quotes or single quotes. + # No problem: we enclose it in double quotes. + # * Contains embedded single quotes. No problem: + # double quotes work here too. + # * Contains embedded double quotes. No problem: + # we enclose it in single quotes. + # * Embeds both single _and_ double quotes. This + # can't happen naturally, but it can happen if + # you modify an attribute value after parsing + # the document. Now we have a bit of a + # problem. We solve it by enclosing the + # attribute in single quotes, and escaping any + # embedded single quotes to XML entities. + if '"' in val: + fmt = "%s='%s'" + if "'" in val: + # TODO: replace with apos when + # appropriate. + val = val.replace("'", "&squot;") + + # Now we're okay w/r/t quotes. But the attribute + # value might also contain angle brackets, or + # ampersands that aren't part of entities. We need + # to escape those to XML entities too. + val = self.BARE_AMPERSAND_OR_BRACKET.sub(self._sub_entity, val) + + attrs.append(fmt % (self.toEncoding(key, encoding), + self.toEncoding(val, encoding))) + close = '' + closeTag = '' + if self.isSelfClosing: + close = ' /' + else: + closeTag = '' % encodedName + + indentTag, indentContents = 0, 0 + if prettyPrint: + indentTag = indentLevel + space = (' ' * (indentTag-1)) + indentContents = indentTag + 1 + contents = self.renderContents(encoding, prettyPrint, indentContents) + if self.hidden: + s = contents + else: + s = [] + attributeString = '' + if attrs: + attributeString = ' ' + ' '.join(attrs) + if prettyPrint: + s.append(space) + s.append('<%s%s%s>' % (encodedName, attributeString, close)) + if prettyPrint: + s.append("\n") + s.append(contents) + if prettyPrint and contents and contents[-1] != "\n": + s.append("\n") + if prettyPrint and closeTag: + s.append(space) + s.append(closeTag) + if prettyPrint and closeTag and self.nextSibling: + s.append("\n") + s = ''.join(s) + return s + + def decompose(self): + """Recursively destroys the contents of this tree.""" + self.extract() + if len(self.contents) == 0: + return + current = self.contents[0] + while current is not None: + next = current.next + if isinstance(current, Tag): + del current.contents[:] + current.parent = None + current.previous = None + current.previousSibling = None + current.next = None + current.nextSibling = None + current = next + + def prettify(self, encoding=DEFAULT_OUTPUT_ENCODING): + return self.__str__(encoding, True) + + def renderContents(self, encoding=DEFAULT_OUTPUT_ENCODING, + prettyPrint=False, indentLevel=0): + """Renders the contents of this tag as a string in the given + encoding. If encoding is None, returns a Unicode string..""" + s=[] + for c in self: + text = None + if isinstance(c, NavigableString): + text = c.__str__(encoding) + elif isinstance(c, Tag): + s.append(c.__str__(encoding, prettyPrint, indentLevel)) + if text and prettyPrint: + text = text.strip() + if text: + if prettyPrint: + s.append(" " * (indentLevel-1)) + s.append(text) + if prettyPrint: + s.append("\n") + return ''.join(s) + + #Soup methods + + def find(self, name=None, attrs={}, recursive=True, text=None, + **kwargs): + """Return only the first child of this Tag matching the given + criteria.""" + r = None + l = self.findAll(name, attrs, recursive, text, 1, **kwargs) + if l: + r = l[0] + return r + findChild = find + + def findAll(self, name=None, attrs={}, recursive=True, text=None, + limit=None, **kwargs): + """Extracts a list of Tag objects that match the given + criteria. You can specify the name of the Tag and any + attributes you want the Tag to have. + + The value of a key-value pair in the 'attrs' map can be a + string, a list of strings, a regular expression object, or a + callable that takes a string and returns whether or not the + string matches for some custom definition of 'matches'. The + same is true of the tag name.""" + generator = self.recursiveChildGenerator + if not recursive: + generator = self.childGenerator + return self._findAll(name, attrs, text, limit, generator, **kwargs) + findChildren = findAll + + # Pre-3.x compatibility methods + first = find + fetch = findAll + + def fetchText(self, text=None, recursive=True, limit=None): + return self.findAll(text=text, recursive=recursive, limit=limit) + + def firstText(self, text=None, recursive=True): + return self.find(text=text, recursive=recursive) + + #Private methods + + def _getAttrMap(self): + """Initializes a map representation of this tag's attributes, + if not already initialized.""" + if not getattr(self, 'attrMap'): + self.attrMap = {} + for (key, value) in self.attrs: + self.attrMap[key] = value + return self.attrMap + + #Generator methods + def childGenerator(self): + # Just use the iterator from the contents + return iter(self.contents) + + def recursiveChildGenerator(self): + if not len(self.contents): + raise StopIteration + stopNode = self._lastRecursiveChild().next + current = self.contents[0] + while current is not stopNode: + yield current + current = current.next + + +# Next, a couple classes to represent queries and their results. +class SoupStrainer: + """Encapsulates a number of ways of matching a markup element (tag or + text).""" + + def __init__(self, name=None, attrs={}, text=None, **kwargs): + self.name = name + if isinstance(attrs, basestring): + kwargs['class'] = _match_css_class(attrs) + attrs = None + if kwargs: + if attrs: + attrs = attrs.copy() + attrs.update(kwargs) + else: + attrs = kwargs + self.attrs = attrs + self.text = text + + def __str__(self): + if self.text: + return self.text + else: + return "%s|%s" % (self.name, self.attrs) + + def searchTag(self, markupName=None, markupAttrs={}): + found = None + markup = None + if isinstance(markupName, Tag): + markup = markupName + markupAttrs = markup + callFunctionWithTagData = callable(self.name) \ + and not isinstance(markupName, Tag) + + if (not self.name) \ + or callFunctionWithTagData \ + or (markup and self._matches(markup, self.name)) \ + or (not markup and self._matches(markupName, self.name)): + if callFunctionWithTagData: + match = self.name(markupName, markupAttrs) + else: + match = True + markupAttrMap = None + for attr, matchAgainst in self.attrs.items(): + if not markupAttrMap: + if hasattr(markupAttrs, 'get'): + markupAttrMap = markupAttrs + else: + markupAttrMap = {} + for k,v in markupAttrs: + markupAttrMap[k] = v + attrValue = markupAttrMap.get(attr) + if not self._matches(attrValue, matchAgainst): + match = False + break + if match: + if markup: + found = markup + else: + found = markupName + return found + + def search(self, markup): + #print 'looking for %s in %s' % (self, markup) + found = None + # If given a list of items, scan it for a text element that + # matches. + if hasattr(markup, "__iter__") \ + and not isinstance(markup, Tag): + for element in markup: + if isinstance(element, NavigableString) \ + and self.search(element): + found = element + break + # If it's a Tag, make sure its name or attributes match. + # Don't bother with Tags if we're searching for text. + elif isinstance(markup, Tag): + if not self.text: + found = self.searchTag(markup) + # If it's text, make sure the text matches. + elif isinstance(markup, NavigableString) or \ + isinstance(markup, basestring): + if self._matches(markup, self.text): + found = markup + else: + raise Exception, "I don't know how to match against a %s" \ + % markup.__class__ + return found + + def _matches(self, markup, matchAgainst): + #print "Matching %s against %s" % (markup, matchAgainst) + result = False + if matchAgainst is True: + result = markup is not None + elif callable(matchAgainst): + result = matchAgainst(markup) + else: + #Custom match methods take the tag as an argument, but all + #other ways of matching match the tag name as a string. + if isinstance(markup, Tag): + markup = markup.name + if markup and not isinstance(markup, basestring): + markup = unicode(markup) + #Now we know that chunk is either a string, or None. + if hasattr(matchAgainst, 'match'): + # It's a regexp object. + result = markup and matchAgainst.search(markup) + elif hasattr(matchAgainst, '__iter__'): # list-like + result = markup in matchAgainst + elif hasattr(matchAgainst, 'items'): + result = markup.has_key(matchAgainst) + elif matchAgainst and isinstance(markup, basestring): + if isinstance(markup, unicode): + matchAgainst = unicode(matchAgainst) + else: + matchAgainst = str(matchAgainst) + + if not result: + result = matchAgainst == markup + return result + +class ResultSet(list): + """A ResultSet is just a list that keeps track of the SoupStrainer + that created it.""" + def __init__(self, source): + list.__init__([]) + self.source = source + +# Now, some helper functions. + +def buildTagMap(default, *args): + """Turns a list of maps, lists, or scalars into a single map. + Used to build the SELF_CLOSING_TAGS, NESTABLE_TAGS, and + NESTING_RESET_TAGS maps out of lists and partial maps.""" + built = {} + for portion in args: + if hasattr(portion, 'items'): + #It's a map. Merge it. + for k,v in portion.items(): + built[k] = v + elif hasattr(portion, '__iter__'): # is a list + #It's a list. Map each item to the default. + for k in portion: + built[k] = default + else: + #It's a scalar. Map it to the default. + built[portion] = default + return built + +# Now, the parser classes. + +class BeautifulStoneSoup(Tag, SGMLParser): + + """This class contains the basic parser and search code. It defines + a parser that knows nothing about tag behavior except for the + following: + + You can't close a tag without closing all the tags it encloses. + That is, "" actually means + "". + + [Another possible explanation is "", but since + this class defines no SELF_CLOSING_TAGS, it will never use that + explanation.] + + This class is useful for parsing XML or made-up markup languages, + or when BeautifulSoup makes an assumption counter to what you were + expecting.""" + + SELF_CLOSING_TAGS = {} + NESTABLE_TAGS = {} + RESET_NESTING_TAGS = {} + QUOTE_TAGS = {} + PRESERVE_WHITESPACE_TAGS = [] + + MARKUP_MASSAGE = [(re.compile('(<[^<>]*)/>'), + lambda x: x.group(1) + ' />'), + (re.compile(']*)>'), + lambda x: '') + ] + + ROOT_TAG_NAME = u'[document]' + + HTML_ENTITIES = "html" + XML_ENTITIES = "xml" + XHTML_ENTITIES = "xhtml" + # TODO: This only exists for backwards-compatibility + ALL_ENTITIES = XHTML_ENTITIES + + # Used when determining whether a text node is all whitespace and + # can be replaced with a single space. A text node that contains + # fancy Unicode spaces (usually non-breaking) should be left + # alone. + STRIP_ASCII_SPACES = { 9: None, 10: None, 12: None, 13: None, 32: None, } + + def __init__(self, markup="", parseOnlyThese=None, fromEncoding=None, + markupMassage=True, smartQuotesTo=XML_ENTITIES, + convertEntities=None, selfClosingTags=None, isHTML=False): + """The Soup object is initialized as the 'root tag', and the + provided markup (which can be a string or a file-like object) + is fed into the underlying parser. + + sgmllib will process most bad HTML, and the BeautifulSoup + class has some tricks for dealing with some HTML that kills + sgmllib, but Beautiful Soup can nonetheless choke or lose data + if your data uses self-closing tags or declarations + incorrectly. + + By default, Beautiful Soup uses regexes to sanitize input, + avoiding the vast majority of these problems. If the problems + don't apply to you, pass in False for markupMassage, and + you'll get better performance. + + The default parser massage techniques fix the two most common + instances of invalid HTML that choke sgmllib: + +
(No space between name of closing tag and tag close) + (Extraneous whitespace in declaration) + + You can pass in a custom list of (RE object, replace method) + tuples to get Beautiful Soup to scrub your input the way you + want.""" + + self.parseOnlyThese = parseOnlyThese + self.fromEncoding = fromEncoding + self.smartQuotesTo = smartQuotesTo + self.convertEntities = convertEntities + # Set the rules for how we'll deal with the entities we + # encounter + if self.convertEntities: + # It doesn't make sense to convert encoded characters to + # entities even while you're converting entities to Unicode. + # Just convert it all to Unicode. + self.smartQuotesTo = None + if convertEntities == self.HTML_ENTITIES: + self.convertXMLEntities = False + self.convertHTMLEntities = True + self.escapeUnrecognizedEntities = True + elif convertEntities == self.XHTML_ENTITIES: + self.convertXMLEntities = True + self.convertHTMLEntities = True + self.escapeUnrecognizedEntities = False + elif convertEntities == self.XML_ENTITIES: + self.convertXMLEntities = True + self.convertHTMLEntities = False + self.escapeUnrecognizedEntities = False + else: + self.convertXMLEntities = False + self.convertHTMLEntities = False + self.escapeUnrecognizedEntities = False + + self.instanceSelfClosingTags = buildTagMap(None, selfClosingTags) + SGMLParser.__init__(self) + + if hasattr(markup, 'read'): # It's a file-type object. + markup = markup.read() + self.markup = markup + self.markupMassage = markupMassage + try: + self._feed(isHTML=isHTML) + except StopParsing: + pass + self.markup = None # The markup can now be GCed + + def convert_charref(self, name): + """This method fixes a bug in Python's SGMLParser.""" + try: + n = int(name) + except ValueError: + return + if not 0 <= n <= 127 : # ASCII ends at 127, not 255 + return + return self.convert_codepoint(n) + + def _feed(self, inDocumentEncoding=None, isHTML=False): + # Convert the document to Unicode. + markup = self.markup + if isinstance(markup, unicode): + if not hasattr(self, 'originalEncoding'): + self.originalEncoding = None + else: + dammit = UnicodeDammit\ + (markup, [self.fromEncoding, inDocumentEncoding], + smartQuotesTo=self.smartQuotesTo, isHTML=isHTML) + markup = dammit.unicode + self.originalEncoding = dammit.originalEncoding + self.declaredHTMLEncoding = dammit.declaredHTMLEncoding + if markup: + if self.markupMassage: + if not hasattr(self.markupMassage, "__iter__"): + self.markupMassage = self.MARKUP_MASSAGE + for fix, m in self.markupMassage: + markup = fix.sub(m, markup) + # TODO: We get rid of markupMassage so that the + # soup object can be deepcopied later on. Some + # Python installations can't copy regexes. If anyone + # was relying on the existence of markupMassage, this + # might cause problems. + del(self.markupMassage) + self.reset() + + SGMLParser.feed(self, markup) + # Close out any unfinished strings and close all the open tags. + self.endData() + while self.currentTag.name != self.ROOT_TAG_NAME: + self.popTag() + + def __getattr__(self, methodName): + """This method routes method call requests to either the SGMLParser + superclass or the Tag superclass, depending on the method name.""" + #print "__getattr__ called on %s.%s" % (self.__class__, methodName) + + if methodName.startswith('start_') or methodName.startswith('end_') \ + or methodName.startswith('do_'): + return SGMLParser.__getattr__(self, methodName) + elif not methodName.startswith('__'): + return Tag.__getattr__(self, methodName) + else: + raise AttributeError + + def isSelfClosingTag(self, name): + """Returns true iff the given string is the name of a + self-closing tag according to this parser.""" + return self.SELF_CLOSING_TAGS.has_key(name) \ + or self.instanceSelfClosingTags.has_key(name) + + def reset(self): + Tag.__init__(self, self, self.ROOT_TAG_NAME) + self.hidden = 1 + SGMLParser.reset(self) + self.currentData = [] + self.currentTag = None + self.tagStack = [] + self.quoteStack = [] + self.pushTag(self) + + def popTag(self): + tag = self.tagStack.pop() + + #print "Pop", tag.name + if self.tagStack: + self.currentTag = self.tagStack[-1] + return self.currentTag + + def pushTag(self, tag): + #print "Push", tag.name + if self.currentTag: + self.currentTag.contents.append(tag) + self.tagStack.append(tag) + self.currentTag = self.tagStack[-1] + + def endData(self, containerClass=NavigableString): + if self.currentData: + currentData = u''.join(self.currentData) + if (currentData.translate(self.STRIP_ASCII_SPACES) == '' and + not set([tag.name for tag in self.tagStack]).intersection( + self.PRESERVE_WHITESPACE_TAGS)): + if '\n' in currentData: + currentData = '\n' + else: + currentData = ' ' + self.currentData = [] + if self.parseOnlyThese and len(self.tagStack) <= 1 and \ + (not self.parseOnlyThese.text or \ + not self.parseOnlyThese.search(currentData)): + return + o = containerClass(currentData) + o.setup(self.currentTag, self.previous) + if self.previous: + self.previous.next = o + self.previous = o + self.currentTag.contents.append(o) + + + def _popToTag(self, name, inclusivePop=True): + """Pops the tag stack up to and including the most recent + instance of the given tag. If inclusivePop is false, pops the tag + stack up to but *not* including the most recent instqance of + the given tag.""" + #print "Popping to %s" % name + if name == self.ROOT_TAG_NAME: + return + + numPops = 0 + mostRecentTag = None + for i in range(len(self.tagStack)-1, 0, -1): + if name == self.tagStack[i].name: + numPops = len(self.tagStack)-i + break + if not inclusivePop: + numPops = numPops - 1 + + for i in range(0, numPops): + mostRecentTag = self.popTag() + return mostRecentTag + + def _smartPop(self, name): + + """We need to pop up to the previous tag of this type, unless + one of this tag's nesting reset triggers comes between this + tag and the previous tag of this type, OR unless this tag is a + generic nesting trigger and another generic nesting trigger + comes between this tag and the previous tag of this type. + + Examples: +

FooBar *

* should pop to 'p', not 'b'. +

FooBar *

* should pop to 'table', not 'p'. +

Foo

Bar *

* should pop to 'tr', not 'p'. + +

    • *
    • * should pop to 'ul', not the first 'li'. +
  • ** should pop to 'table', not the first 'tr' + tag should + implicitly close the previous tag within the same
    ** should pop to 'tr', not the first 'td' + """ + + nestingResetTriggers = self.NESTABLE_TAGS.get(name) + isNestable = nestingResetTriggers != None + isResetNesting = self.RESET_NESTING_TAGS.has_key(name) + popTo = None + inclusive = True + for i in range(len(self.tagStack)-1, 0, -1): + p = self.tagStack[i] + if (not p or p.name == name) and not isNestable: + #Non-nestable tags get popped to the top or to their + #last occurance. + popTo = name + break + if (nestingResetTriggers is not None + and p.name in nestingResetTriggers) \ + or (nestingResetTriggers is None and isResetNesting + and self.RESET_NESTING_TAGS.has_key(p.name)): + + #If we encounter one of the nesting reset triggers + #peculiar to this tag, or we encounter another tag + #that causes nesting to reset, pop up to but not + #including that tag. + popTo = p.name + inclusive = False + break + p = p.parent + if popTo: + self._popToTag(popTo, inclusive) + + def unknown_starttag(self, name, attrs, selfClosing=0): + #print "Start tag %s: %s" % (name, attrs) + if self.quoteStack: + #This is not a real tag. + #print "<%s> is not real!" % name + attrs = ''.join([' %s="%s"' % (x, y) for x, y in attrs]) + self.handle_data('<%s%s>' % (name, attrs)) + return + self.endData() + + if not self.isSelfClosingTag(name) and not selfClosing: + self._smartPop(name) + + if self.parseOnlyThese and len(self.tagStack) <= 1 \ + and (self.parseOnlyThese.text or not self.parseOnlyThese.searchTag(name, attrs)): + return + + tag = Tag(self, name, attrs, self.currentTag, self.previous) + if self.previous: + self.previous.next = tag + self.previous = tag + self.pushTag(tag) + if selfClosing or self.isSelfClosingTag(name): + self.popTag() + if name in self.QUOTE_TAGS: + #print "Beginning quote (%s)" % name + self.quoteStack.append(name) + self.literal = 1 + return tag + + def unknown_endtag(self, name): + #print "End tag %s" % name + if self.quoteStack and self.quoteStack[-1] != name: + #This is not a real end tag. + #print " is not real!" % name + self.handle_data('' % name) + return + self.endData() + self._popToTag(name) + if self.quoteStack and self.quoteStack[-1] == name: + self.quoteStack.pop() + self.literal = (len(self.quoteStack) > 0) + + def handle_data(self, data): + self.currentData.append(data) + + def _toStringSubclass(self, text, subclass): + """Adds a certain piece of text to the tree as a NavigableString + subclass.""" + self.endData() + self.handle_data(text) + self.endData(subclass) + + def handle_pi(self, text): + """Handle a processing instruction as a ProcessingInstruction + object, possibly one with a %SOUP-ENCODING% slot into which an + encoding will be plugged later.""" + if text[:3] == "xml": + text = u"xml version='1.0' encoding='%SOUP-ENCODING%'" + self._toStringSubclass(text, ProcessingInstruction) + + def handle_comment(self, text): + "Handle comments as Comment objects." + self._toStringSubclass(text, Comment) + + def handle_charref(self, ref): + "Handle character references as data." + if self.convertEntities: + data = unichr(int(ref)) + else: + data = '&#%s;' % ref + self.handle_data(data) + + def handle_entityref(self, ref): + """Handle entity references as data, possibly converting known + HTML and/or XML entity references to the corresponding Unicode + characters.""" + data = None + if self.convertHTMLEntities: + try: + data = unichr(name2codepoint[ref]) + except KeyError: + pass + + if not data and self.convertXMLEntities: + data = self.XML_ENTITIES_TO_SPECIAL_CHARS.get(ref) + + if not data and self.convertHTMLEntities and \ + not self.XML_ENTITIES_TO_SPECIAL_CHARS.get(ref): + # TODO: We've got a problem here. We're told this is + # an entity reference, but it's not an XML entity + # reference or an HTML entity reference. Nonetheless, + # the logical thing to do is to pass it through as an + # unrecognized entity reference. + # + # Except: when the input is "&carol;" this function + # will be called with input "carol". When the input is + # "AT&T", this function will be called with input + # "T". We have no way of knowing whether a semicolon + # was present originally, so we don't know whether + # this is an unknown entity or just a misplaced + # ampersand. + # + # The more common case is a misplaced ampersand, so I + # escape the ampersand and omit the trailing semicolon. + data = "&%s" % ref + if not data: + # This case is different from the one above, because we + # haven't already gone through a supposedly comprehensive + # mapping of entities to Unicode characters. We might not + # have gone through any mapping at all. So the chances are + # very high that this is a real entity, and not a + # misplaced ampersand. + data = "&%s;" % ref + self.handle_data(data) + + def handle_decl(self, data): + "Handle DOCTYPEs and the like as Declaration objects." + self._toStringSubclass(data, Declaration) + + def parse_declaration(self, i): + """Treat a bogus SGML declaration as raw data. Treat a CDATA + declaration as a CData object.""" + j = None + if self.rawdata[i:i+9] == '', i) + if k == -1: + k = len(self.rawdata) + data = self.rawdata[i+9:k] + j = k+3 + self._toStringSubclass(data, CData) + else: + try: + j = SGMLParser.parse_declaration(self, i) + except SGMLParseError: + toHandle = self.rawdata[i:] + self.handle_data(toHandle) + j = i + len(toHandle) + return j + +class BeautifulSoup(BeautifulStoneSoup): + + """This parser knows the following facts about HTML: + + * Some tags have no closing tag and should be interpreted as being + closed as soon as they are encountered. + + * The text inside some tags (ie. 'script') may contain tags which + are not really part of the document and which should be parsed + as text, not tags. If you want to parse the text as tags, you can + always fetch it and parse it explicitly. + + * Tag nesting rules: + + Most tags can't be nested at all. For instance, the occurance of + a

    tag should implicitly close the previous

    tag. + +

    Para1

    Para2 + should be transformed into: +

    Para1

    Para2 + + Some tags can be nested arbitrarily. For instance, the occurance + of a

    tag should _not_ implicitly close the previous +
    tag. + + Alice said:
    Bob said:
    Blah + should NOT be transformed into: + Alice said:
    Bob said:
    Blah + + Some tags can be nested, but the nesting is reset by the + interposition of other tags. For instance, a
    , + but not close a tag in another table. + +
    BlahBlah + should be transformed into: +
    BlahBlah + but, + Blah
    Blah + should NOT be transformed into + Blah
    Blah + + Differing assumptions about tag nesting rules are a major source + of problems with the BeautifulSoup class. If BeautifulSoup is not + treating as nestable a tag your page author treats as nestable, + try ICantBelieveItsBeautifulSoup, MinimalSoup, or + BeautifulStoneSoup before writing your own subclass.""" + + def __init__(self, *args, **kwargs): + if not kwargs.has_key('smartQuotesTo'): + kwargs['smartQuotesTo'] = self.HTML_ENTITIES + kwargs['isHTML'] = True + BeautifulStoneSoup.__init__(self, *args, **kwargs) + + SELF_CLOSING_TAGS = buildTagMap(None, + ('br' , 'hr', 'input', 'img', 'meta', + 'spacer', 'link', 'frame', 'base', 'col')) + + PRESERVE_WHITESPACE_TAGS = set(['pre', 'textarea']) + + QUOTE_TAGS = {'script' : None, 'textarea' : None} + + #According to the HTML standard, each of these inline tags can + #contain another tag of the same type. Furthermore, it's common + #to actually use these tags this way. + NESTABLE_INLINE_TAGS = ('span', 'font', 'q', 'object', 'bdo', 'sub', 'sup', + 'center') + + #According to the HTML standard, these block tags can contain + #another tag of the same type. Furthermore, it's common + #to actually use these tags this way. + NESTABLE_BLOCK_TAGS = ('blockquote', 'div', 'fieldset', 'ins', 'del') + + #Lists can contain other lists, but there are restrictions. + NESTABLE_LIST_TAGS = { 'ol' : [], + 'ul' : [], + 'li' : ['ul', 'ol'], + 'dl' : [], + 'dd' : ['dl'], + 'dt' : ['dl'] } + + #Tables can contain other tables, but there are restrictions. + NESTABLE_TABLE_TAGS = {'table' : [], + 'tr' : ['table', 'tbody', 'tfoot', 'thead'], + 'td' : ['tr'], + 'th' : ['tr'], + 'thead' : ['table'], + 'tbody' : ['table'], + 'tfoot' : ['table'], + } + + NON_NESTABLE_BLOCK_TAGS = ('address', 'form', 'p', 'pre') + + #If one of these tags is encountered, all tags up to the next tag of + #this type are popped. + RESET_NESTING_TAGS = buildTagMap(None, NESTABLE_BLOCK_TAGS, 'noscript', + NON_NESTABLE_BLOCK_TAGS, + NESTABLE_LIST_TAGS, + NESTABLE_TABLE_TAGS) + + NESTABLE_TAGS = buildTagMap([], NESTABLE_INLINE_TAGS, NESTABLE_BLOCK_TAGS, + NESTABLE_LIST_TAGS, NESTABLE_TABLE_TAGS) + + # Used to detect the charset in a META tag; see start_meta + CHARSET_RE = re.compile("((^|;)\s*charset=)([^;]*)", re.M) + + def start_meta(self, attrs): + """Beautiful Soup can detect a charset included in a META tag, + try to convert the document to that charset, and re-parse the + document from the beginning.""" + httpEquiv = None + contentType = None + contentTypeIndex = None + tagNeedsEncodingSubstitution = False + + for i in range(0, len(attrs)): + key, value = attrs[i] + key = key.lower() + if key == 'http-equiv': + httpEquiv = value + elif key == 'content': + contentType = value + contentTypeIndex = i + + if httpEquiv and contentType: # It's an interesting meta tag. + match = self.CHARSET_RE.search(contentType) + if match: + if (self.declaredHTMLEncoding is not None or + self.originalEncoding == self.fromEncoding): + # An HTML encoding was sniffed while converting + # the document to Unicode, or an HTML encoding was + # sniffed during a previous pass through the + # document, or an encoding was specified + # explicitly and it worked. Rewrite the meta tag. + def rewrite(match): + return match.group(1) + "%SOUP-ENCODING%" + newAttr = self.CHARSET_RE.sub(rewrite, contentType) + attrs[contentTypeIndex] = (attrs[contentTypeIndex][0], + newAttr) + tagNeedsEncodingSubstitution = True + else: + # This is our first pass through the document. + # Go through it again with the encoding information. + newCharset = match.group(3) + if newCharset and newCharset != self.originalEncoding: + self.declaredHTMLEncoding = newCharset + self._feed(self.declaredHTMLEncoding) + raise StopParsing + pass + tag = self.unknown_starttag("meta", attrs) + if tag and tagNeedsEncodingSubstitution: + tag.containsSubstitutions = True + +class StopParsing(Exception): + pass + +class ICantBelieveItsBeautifulSoup(BeautifulSoup): + + """The BeautifulSoup class is oriented towards skipping over + common HTML errors like unclosed tags. However, sometimes it makes + errors of its own. For instance, consider this fragment: + + FooBar + + This is perfectly valid (if bizarre) HTML. However, the + BeautifulSoup class will implicitly close the first b tag when it + encounters the second 'b'. It will think the author wrote + "FooBar", and didn't close the first 'b' tag, because + there's no real-world reason to bold something that's already + bold. When it encounters '' it will close two more 'b' + tags, for a grand total of three tags closed instead of two. This + can throw off the rest of your document structure. The same is + true of a number of other tags, listed below. + + It's much more common for someone to forget to close a 'b' tag + than to actually use nested 'b' tags, and the BeautifulSoup class + handles the common case. This class handles the not-co-common + case: where you can't believe someone wrote what they did, but + it's valid HTML and BeautifulSoup screwed up by assuming it + wouldn't be.""" + + I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS = \ + ('em', 'big', 'i', 'small', 'tt', 'abbr', 'acronym', 'strong', + 'cite', 'code', 'dfn', 'kbd', 'samp', 'strong', 'var', 'b', + 'big') + + I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS = ('noscript',) + + NESTABLE_TAGS = buildTagMap([], BeautifulSoup.NESTABLE_TAGS, + I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS, + I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS) + +class MinimalSoup(BeautifulSoup): + """The MinimalSoup class is for parsing HTML that contains + pathologically bad markup. It makes no assumptions about tag + nesting, but it does know which tags are self-closing, that + ")] + data = data[:data.rindex(";")] + data = data.replace('tables:','"tables":') + tables = json.loads(data)['tables'] + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(url) + else: + raise e + + # looks like only one author per story allowed. + author = tables['users'][0] + story = tables['stories'][0] + story_ver = tables['story_versions'][0] + print("story:%s"%story) + + self.story.setMetadata('authorId',author['id']) + self.story.setMetadata('author',author['display_name']) + self.story.setMetadata('authorUrl','https://'+self.host+'/author/'+author['display_name']+'/stories') + + self.story.setMetadata('title',story_ver['title']) + self.setDescription(url,story_ver['description']) + + if not ('assets/story_versions/covers' in story_ver['profile_image_url@2x']): + self.setCoverImage(url,story_ver['profile_image_url@2x']) + + self.story.setMetadata('datePublished',makeDate(story['published_at'], self.dateformat)) + self.story.setMetadata('dateUpdated',makeDate(story['published_at'], self.dateformat)) + + self.story.setMetadata('followers',story['followers_count']) + self.story.setMetadata('comments',story['comments_count']) + self.story.setMetadata('views',story['views_count']) + self.story.setMetadata('likes',int(story['likes'])) # no idea why they floated these. + if 'dislikes' in story: + self.story.setMetadata('dislikes',int(story['dislikes'])) + + if story_ver['is_complete']: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + self.story.setMetadata('rating', story_ver['maturity_level']) + self.story.setMetadata('numWords', unicode(story_ver['word_count'])) + + for i in tables['fandoms']: + self.story.addToList('category',i['name']) + + for i in tables['genres']: + self.story.addToList('genre',i['name']) + + for i in tables['characters']: + self.story.addToList('characters',i['name']) + + for c in tables['chapters']: + chtitle = "Chapter %d"%c['number'] + if c['title']: + chtitle += " - %s"%c['title'] + self.chapterUrls.append((chtitle,c['body_url'])) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + def getChapterText(self, url): + logger.debug('Getting chapter text from: %s' % url) + if not url: + data = u"This chapter has no text." + else: + data = self._fetchUrl(url) + soup = bs.BeautifulSoup(u"
    "+data+u"
    ") + return self.utf8FromSoup(url,soup) + +def getClass(): + return FictionPadSiteAdapter + diff --git a/fanficdownloader/adapters/adapter_fictionpresscom.py b/fanficdownloader/adapters/adapter_fictionpresscom.py new file mode 100644 index 0000000..795ff94 --- /dev/null +++ b/fanficdownloader/adapters/adapter_fictionpresscom.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 +import time + +## They're from the same people and pretty much identical. +from adapter_fanfictionnet import FanFictionNetSiteAdapter + +class FictionPressComSiteAdapter(FanFictionNetSiteAdapter): + + def __init__(self, config, url): + FanFictionNetSiteAdapter.__init__(self, config, url) + self.story.setMetadata('siteabbrev','fpcom') + + @staticmethod + def getSiteDomain(): + return 'www.fictionpress.com' + + @classmethod + def getAcceptDomains(cls): + return ['www.fictionpress.com','m.fictionpress.com'] + + @classmethod + def getSiteExampleURLs(cls): + return "https://www.fictionpress.com/s/1234/1/ https://www.fictionpress.com/s/1234/12/ http://www.fictionpress.com/s/1234/1/Story_Title http://m.fictionpress.com/s/1234/1/" + + def getSiteURLPattern(self): + return r"https?://(www|m)?\.fictionpress\.com/s/\d+(/\d+)?(/|/[a-zA-Z0-9_-]+)?/?$" + +def getClass(): + return FictionPressComSiteAdapter + diff --git a/fanficdownloader/adapters/adapter_ficwadcom.py b/fanficdownloader/adapters/adapter_ficwadcom.py new file mode 100644 index 0000000..7934a13 --- /dev/null +++ b/fanficdownloader/adapters/adapter_ficwadcom.py @@ -0,0 +1,233 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 +import time +import httplib, urllib + +from .. import exceptions as exceptions +from ..htmlcleanup import stripHTML + +from base_adapter import BaseSiteAdapter, makeDate + +class FicwadComSiteAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + self.story.setMetadata('siteabbrev','fw') + + # get storyId from url--url validation guarantees second part is storyId + self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2]) + + self.username = "NoneGiven" + self.password = "" + + @staticmethod + def getSiteDomain(): + return 'ficwad.com' + + @classmethod + def getSiteExampleURLs(cls): + return "http://ficwad.com/story/1234" + + def getSiteURLPattern(self): + return re.escape(r"http://"+self.getSiteDomain())+"/story/\d+?$" + + def performLogin(self,url): + params = {} + + if self.password: + params['username'] = self.username + params['password'] = self.password + else: + params['username'] = self.getConfig("username") + params['password'] = self.getConfig("password") + + loginUrl = 'http://' + self.getSiteDomain() + '/account/login' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['username'])) + d = self._postUrl(loginUrl,params,usecache=False) + + if "Login attempt failed..." in d: + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['username'])) + raise exceptions.FailedToLogin(url,params['username']) + return False + else: + return True + + def use_pagecache(self): + ''' + adapters that will work with the page cache need to implement + this and change it to True. + ''' + return True + + def extractChapterUrlsAndMetadata(self): + + # fetch the chapter. From that we will get almost all the + # metadata and chapter list + + url = self.url + logger.debug("URL: "+url) + + # use BeautifulSoup HTML parser to make everything easier to find. + try: + data = self._fetchUrl(url) + # non-existent/removed story urls get thrown to the front page. + if "

    Welcome to FicWad

    " in data: + raise exceptions.StoryDoesNotExist(self.url) + soup = self.make_soup(data) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + # if blocked, attempt login. + if soup.find("div",{"class":"blocked"}) or soup.find("li",{"class":"blocked"}): + if self.performLogin(url): # performLogin raises + # FailedToLogin if it fails. + soup = self.make_soup(self._fetchUrl(url,usecache=False)) + + divstory = soup.find('div',id='story') + storya = divstory.find('a',href=re.compile("^/story/\d+$")) + if storya : # if there's a story link in the divstory header, this is a chapter page. + # normalize story URL on chapter list. + self.story.setMetadata('storyId',storya['href'].split('/',)[2]) + url = "http://"+self.getSiteDomain()+storya['href'] + logger.debug("Normalizing to URL: "+url) + self._setURL(url) + try: + soup = self.make_soup(self._fetchUrl(url)) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + # if blocked, attempt login. + if soup.find("div",{"class":"blocked"}) or soup.find("li",{"class":"blocked"}): + if self.performLogin(url): # performLogin raises + # FailedToLogin if it fails. + soup = self.make_soup(self._fetchUrl(url,usecache=False)) + + # title - first h4 tag will be title. + titleh4 = soup.find('div',{'class':'storylist'}).find('h4') + self.story.setMetadata('title', stripHTML(titleh4.a)) + + # Find authorid and URL from... author url. + a = soup.find('span',{'class':'author'}).find('a', href=re.compile(r"^/author/\d+")) + self.story.setMetadata('authorId',a['href'].split('/')[2]) + self.story.setMetadata('authorUrl','http://'+self.host+a['href']) + self.story.setMetadata('author',a.string) + + # description + storydiv = soup.find("div",{"id":"story"}) + self.setDescription(url,storydiv.find("blockquote",{'class':'summary'}).p) + #self.story.setMetadata('description', storydiv.find("blockquote",{'class':'summary'}).p.string) + + # most of the meta data is here: + metap = storydiv.find("p",{"class":"meta"}) + self.story.addToList('category',metap.find("a",href=re.compile(r"^/category/\d+")).string) + + # warnings + # [!!] [R] [V] [Y] + spanreq = metap.find("span",{"class":"story-warnings"}) + if spanreq: # can be no warnings. + for a in spanreq.findAll("a"): + self.story.addToList('warnings',a['title']) + + ## perhaps not the most efficient way to parse this, using + ## regexps for each rather than something more complex, but + ## IMO, it's more readable and amenable to change. + metastr = stripHTML(str(metap)).replace('\n',' ').replace('\t',' ').replace(u'\u00a0',' ') + + m = re.match(r".*?Rating: (.+?) -.*?",metastr) + if m: + self.story.setMetadata('rating', m.group(1)) + + m = re.match(r".*?Genres: (.+?) -.*?",metastr) + if m: + for g in m.group(1).split(','): + self.story.addToList('genre',g) + + m = re.match(r".*?Characters: (.*?) -.*?",metastr) + if m: + for g in m.group(1).split(','): + if g: + self.story.addToList('characters',g) + + m = re.match(r".*?Published: ([0-9-]+?) -.*?",metastr) + if m: + self.story.setMetadata('datePublished',makeDate(m.group(1), "%Y-%m-%d")) + + # Updated can have more than one space after it. + m = re.match(r".*?Updated: ([0-9-]+?) +-.*?",metastr) + if m: + self.story.setMetadata('dateUpdated',makeDate(m.group(1), "%Y-%m-%d")) + + m = re.match(r".*? - ([0-9,]+?) words.*?",metastr) + if m: + self.story.setMetadata('numWords',m.group(1)) + + if metastr.endswith("Complete"): + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + # get the chapter list first this time because that's how we + # detect the need to login. + storylistul = soup.find('ul',{'class':'storylist'}) + if not storylistul: + # no list found, so it's a one-chapter story. + self.chapterUrls.append((self.story.getMetadata('title'),url)) + else: + chapterlistlis = storylistul.findAll('li') + for chapterli in chapterlistlis: + if "blocked" in chapterli['class']: + # paranoia check. We should already be logged in by now. + raise exceptions.FailedToLogin(url,self.username) + else: + #print "chapterli.h4.a (%s)"%chapterli.h4.a + self.chapterUrls.append((chapterli.h4.a.string, + u'http://%s%s'%(self.getSiteDomain(), + chapterli.h4.a['href']))) + #print "self.chapterUrls:%s"%self.chapterUrls + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + return + + + def getChapterText(self, url): + logger.debug('Getting chapter text from: %s' % url) + soup = self.make_soup(self._fetchUrl(url)) + + span = soup.find('div', {'id' : 'storytext'}) + + if None == span: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,span) + +def getClass(): + return FicwadComSiteAdapter + diff --git a/fanficdownloader/adapters/adapter_fimfictionnet.py b/fanficdownloader/adapters/adapter_fimfictionnet.py new file mode 100644 index 0000000..f390dc7 --- /dev/null +++ b/fanficdownloader/adapters/adapter_fimfictionnet.py @@ -0,0 +1,338 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 +import cookielib as cl +import json + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return FimFictionNetSiteAdapter + +class FimFictionNetSiteAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + self.story.setMetadata('siteabbrev','fimficnet') + self.story.setMetadata('storyId', self.parsedUrl.path.split('/',)[2]) + self._setURL("http://"+self.getSiteDomain()+"/story/"+self.story.getMetadata('storyId')+"/") + self.is_adult = False + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%d %b %Y" + + @staticmethod + def getSiteDomain(): + return 'www.fimfiction.net' + + @classmethod + def getAcceptDomains(cls): + # mobile.fimifction.com isn't actually a valid domain, but we can still get the story id from URLs anyway + return ['www.fimfiction.net','mobile.fimfiction.net', 'www.fimfiction.com', 'mobile.fimfiction.com'] + + @classmethod + def getSiteExampleURLs(cls): + return "http://www.fimfiction.net/story/1234/story-title-here http://www.fimfiction.net/story/1234/ http://www.fimfiction.com/story/1234/1/ http://mobile.fimfiction.net/story/1234/1/story-title-here/chapter-title-here" + + def getSiteURLPattern(self): + return r"https?://(www|mobile)\.fimfiction\.(net|com)/story/\d+/?.*" + + def use_pagecache(self): + ''' + adapters that will work with the page cache need to implement + this and change it to True. + ''' + return True + + def doExtractChapterUrlsAndMetadata(self,get_cover=True): + + if self.is_adult or self.getConfig("is_adult"): + cookie = cl.Cookie(version=0, name='view_mature', value='true', + port=None, port_specified=False, + domain=self.getSiteDomain(), domain_specified=False, domain_initial_dot=False, + path='/story', path_specified=True, + secure=False, + expires=time.time()+10000, + discard=False, + comment=None, + comment_url=None, + rest={'HttpOnly': None}, + rfc2109=False) + self.cookiejar.set_cookie(cookie) + + ##--------------------------------------------------------------------------------------------------- + ## Get the story's title page. Check if it exists. + + try: + # don't use cache if manual is_adult--should only happen + # if it's an adult story and they don't have is_adult in ini. + data = self.do_fix_blockquotes(self._fetchUrl(self.url, + usecache=(not self.is_adult))) + soup = bs.BeautifulSoup(data) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if "Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result resource" in data: + raise exceptions.StoryDoesNotExist(self.url) + + if "This story has been marked as having adult content. Please click below to confirm you are of legal age to view adult material in your country." in data: + raise exceptions.AdultCheckRequired(self.url) + + if self.password: + params = {} + params['password'] = self.password + data = self._postUrl(self.url, params) + soup = bs.BeautifulSoup(data) + + if not (soup.find('form', {'id' : 'password_form'}) == None): + if self.getConfig('fail_on_password'): + raise exceptions.FailedToDownload("%s requires story password and fail_on_password is true."%self.url) + else: + raise exceptions.FailedToLogin(self.url,"Story requires individual password",passwdonly=True) + + ##---------------------------------------------------------------------------------------------------- + ## Extract metadata + + storyContentBox = soup.find('div', {'class':'story_content_box'}) + + # Title + title = storyContentBox.find('a', {'class':re.compile(r'.*\bstory_name\b.*')}) + self.story.setMetadata('title',stripHTML(title)) + + # Author + author = storyContentBox.find('div', {'class':'author'}).find('a') + self.story.setMetadata("author", stripHTML(author)) + #No longer seems to be a way to access Fimfiction's internal author ID + self.story.setMetadata("authorId", self.story.getMetadata("author")) + self.story.setMetadata("authorUrl", "http://%s/user/%s" % (self.getSiteDomain(), stripHTML(author))) + + #Rating text is replaced with full words for historical compatibility after the site changed + #on 2014-10-27 + rating = stripHTML(storyContentBox.find('a', {'class':re.compile(r'.*\bcontent-rating-.*')})) + rating = rating.replace("E", "Everyone").replace("T", "Teen").replace("M", "Mature") + self.story.setMetadata("rating", rating) + + # Chapters + for chapter in storyContentBox.findAll('a',{'class':'chapter_link'}): + self.chapterUrls.append((stripHTML(chapter), 'http://'+self.host+chapter['href'])) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + # Status + # In the case of Fimfiction, possible statuses are 'Completed', 'Incomplete', 'On Hiatus' and 'Cancelled' + # For the sake of bringing it in line with the other adapters, 'Incomplete' becomes 'In-Progress' + # and 'Complete' becomes 'Completed'. 'Cancelled' and 'On Hiatus' are passed through, it's easy now for users + # to change/remove if they want with replace_metadata + status = stripHTML(storyContentBox.find('span', {'class':re.compile(r'.*\bcompleted-status-.*')})) + status = status.replace("Incomplete", "In-Progress").replace("Complete", "Completed") + self.story.setMetadata("status", status) + + # Genres and Warnings + # warnings were folded into general categories in the 2014-10-27 site update + categories = storyContentBox.findAll('a', {'class':re.compile(r'.*\bstory_category\b.*')}) + for category in categories: + category = stripHTML(category) + if category == "Gore" or category == "Sex": + self.story.addToList('warnings', category) + else: + self.story.addToList('genre', category) + + # Word count + wordCountText = stripHTML(storyContentBox.find('li', {'class':'bottom'}).find('div', {'class':'word_count'})) + self.story.setMetadata("numWords", re.sub(r'[^0-9]', '', wordCountText)) + + # Cover image + storyImage = storyContentBox.find('div', {'class':'story_image'}) + if storyImage: + coverurl = storyImage.find('a')['href'] + if coverurl.startswith('//'): # fix for img urls missing 'http:' + coverurl = "http:"+coverurl + if get_cover: + self.setCoverImage(self.url,coverurl) + + coverSource = storyImage.find('a', {'class':'source'}) + if coverSource: + self.story.setMetadata('coverSourceUrl', coverSource['href']) + #There's no text associated with the cover source link, so just + #reuse the URL. Makes it clear it's an external link leading + #outside of the fanfic site, at least. + self.story.setMetadata('coverSource', coverSource['href']) + + # fimf has started including extra stuff inside the description div. + descdivstr = u"%s"%storyContentBox.find("div", {"class":"description"}) + hrstr=u"
    " + descdivstr = u'
    '+descdivstr[descdivstr.index(hrstr)+len(hrstr):] + self.setDescription(self.url,descdivstr) + + # Find the newest and oldest chapter dates + storyData = storyContentBox.find('div', {'class':'story_data'}) + oldestChapter = None + newestChapter = None + self.newestChapterNum = None # save for comparing during update. + # Scan all chapters to find the oldest and newest, on + # FiMFiction it's possible for authors to insert new chapters + # out-of-order or change the dates of earlier ones by editing + # them--That WILL break epub update. + for index, chapterDate in enumerate(storyData.findAll('span', {'class':'date'})): + chapterDate = self.ordinal_date_string_to_date(chapterDate.contents[1]) + if oldestChapter == None or chapterDate < oldestChapter: + oldestChapter = chapterDate + if newestChapter == None or chapterDate > newestChapter: + newestChapter = chapterDate + self.newestChapterNum = index + + if newestChapter is None: + #this will only be true when updating metadata for stories that have 0 chapters + #there is a "last modified" date given on the page, extract it and use that. + moddatetag = storyContentBox.find('span', {'class':'last_modified'}) + if not moddatetag is None: + newestChapter = self.ordinal_date_string_to_date(moddatetag('span')[1].text) + + # Date updated + self.story.setMetadata("dateUpdated", newestChapter) + + # Date published + # falls back to oldest chapter date for stories that haven't been officially published yet + pubdatetag = storyContentBox.find('span', {'class':'date_approved'}) + if pubdatetag is None: + if oldestChapter is None: + #this will only be true when updating metadata for stories that have 0 chapters + #and that have never been officially published - a rare occurrence. Fall back to last + #modified date as the publication date, it's all that we've got. + self.story.setMetadata("datePublished", newestChapter) + else: + self.story.setMetadata("datePublished", oldestChapter) + else: + pubDate = self.ordinal_date_string_to_date(pubdatetag('span')[1].text) + self.story.setMetadata("datePublished", pubDate) + + # Characters + chars = storyContentBox.find("div", {"class":"extra_story_data"}) + for character in chars.findAll("a", {"class":"character_icon"}): + self.story.addToList("characters", character['title']) + + # Likes and dislikes + storyToolbar = soup.find('div', {'class':'story-toolbar'}) + likes = storyToolbar.find('span', {'class':'likes'}) + if not likes is None: + self.story.setMetadata("likes", stripHTML(likes)) + dislikes = storyToolbar.find('span', {'class':'dislikes'}) + if not dislikes is None: + self.story.setMetadata("dislikes", stripHTML(dislikes)) + + # Highest view for a chapter and total views + viewSpan = storyToolbar.find('span', {'title':re.compile(r'.*\btotal views\b.*')}) + self.story.setMetadata("views", re.sub(r'[^0-9]', '', stripHTML(viewSpan))) + self.story.setMetadata("total_views", re.sub(r'[^0-9]', '', viewSpan['title'])) + + # Comment count + commentSpan = storyToolbar.find('span', {'title':re.compile(r'.*\bcomments\b.*')}) + self.story.setMetadata("comment_count", re.sub(r'[^0-9]', '', stripHTML(commentSpan))) + + # Short description + descriptionMeta = soup.find('meta', {'property':'og:description'}) + self.story.setMetadata("short_description", stripHTML(descriptionMeta['content'])) + + #groups + if soup.find('button', {'id':'button-view-all-groups'}): + groupResponse = self._fetchUrl("http://www.fimfiction.net/ajax/groups/story_groups_list.php?story=%s" % (self.story.getMetadata("storyId"))) + groupData = json.loads(groupResponse) + groupList = bs.BeautifulSoup(groupData["content"]) + else: + groupList = soup.find('ul', {'id':'story-groups-list'}) + + if not (groupList == None): + for groupName in groupList.findAll('a'): + self.story.addToList("groupsUrl", 'http://'+self.host+groupName["href"]) + self.story.addToList("groups",stripHTML(groupName).replace(',', ';')) + + #sequels + sequelStoryHeader = soup.find('h1', {'class':'header-stories'}, text="Sequels") + if not sequelStoryHeader == None: + sequelContainer = sequelStoryHeader.parent.parent + for sequel in sequelContainer.findAll('a', {'class':'story_link'}): + self.story.addToList("sequelsUrl", 'http://'+self.host+sequel["href"]) + self.story.addToList("sequels", stripHTML(sequel).replace(',', ';')) + + #The link to the prequel is embedded in the description text, so erring + #on the side of caution and wrapping this whole thing in a try block. + #If anything goes wrong this probably wasn't a valid prequel link. + try: + description = soup.find('div', {'class':'description'}) + firstHR = description.find("hr") + nextSib = firstHR.nextSibling + if "This story is a sequel to" in nextSib.string: + link = nextSib.nextSibling + if link.name == "a": + self.story.setMetadata("prequelUrl", 'http://'+self.host+link["href"]) + self.story.setMetadata("prequel", stripHTML(link)) + except: + pass + + def ordinal_date_string_to_date(self, datestring): + datestripped=re.sub(r"(\d+)(st|nd|rd|th)", r"\1", datestring.strip()) + return makeDate(datestripped, self.dateformat) + + def hookForUpdates(self,chaptercount): + if self.oldchapters and len(self.oldchapters) > self.newestChapterNum: + print("Existing epub has %s chapters\nNewest chapter is %s. Discarding old chapters from there on."%(len(self.oldchapters), self.newestChapterNum+1)) + self.oldchapters = self.oldchapters[:self.newestChapterNum] + return len(self.oldchapters) + + def do_fix_blockquotes(self,data): + if self.getConfig('fix_fimf_blockquotes'): + #

    + #

    + # include > in re groups so there's always something in the group. + data = re.sub(r']*>\s*)]*>)',r'\s*)

    ',r'',data) + return data + + def getChapterText(self, url): + logger.debug('Getting chapter text from: %s' % url) + + data = self._fetchUrl(url) + + soup = bs.BeautifulSoup(data) + if not (soup.find('form', {'id' : 'password_form'}) == None): + if self.password: + params = {} + params['password'] = self.password + data = self._postUrl(url, params) + else: + print("Chapter %s needed password but no password was present" % url) + + data = self.do_fix_blockquotes(data) + + soup = bs.BeautifulSoup(data,selfClosingTags=('br','hr')).find('div', {'class' : 'chapter_content'}) + if soup == None: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,soup) diff --git a/fanficdownloader/adapters/adapter_finestoriescom.py b/fanficdownloader/adapters/adapter_finestoriescom.py new file mode 100644 index 0000000..6c99f9d --- /dev/null +++ b/fanficdownloader/adapters/adapter_finestoriescom.py @@ -0,0 +1,288 @@ +# -*- coding: utf-8 -*- + +# Copyright 2013 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return FineStoriesComAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class FineStoriesComAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url + self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2].split(':')[0]) + if 'storyInfo' in self.story.getMetadata('storyId'): + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/s/storyInfo.php?id='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','fnst') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%Y-%m-%d" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'finestories.com' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/s/1234 http://"+cls.getSiteDomain()+"/s/1234:4010 http://"+cls.getSiteDomain()+"/library/storyInfo.php?id=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain())+r"/(s|library)?/(storyInfo.php\?id=)?\d+(:\d+)?(;\d+)?$" + + ## Login seems to be reasonably standard across eFiction sites. + def needToLoginCheck(self, data): + if 'Free Registration' in data \ + or "Invalid Password!" in data \ + or "Invalid User Name!" in data: + return True + else: + return False + + def performLogin(self, url): + params = {} + + if self.password: + params['theusername'] = self.username + params['thepassword'] = self.password + else: + params['theusername'] = self.getConfig("username") + params['thepassword'] = self.getConfig("password") + params['rememberMe'] = '1' + params['page'] = 'http://'+self.getSiteDomain()+'/' + params['submit'] = 'Login' + + loginUrl = 'http://' + self.getSiteDomain() + '/login.php' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['theusername'])) + + d = self._fetchUrl(loginUrl, params) + + if "My Account" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['theusername'])) + raise exceptions.FailedToLogin(url,params['theusername']) + return False + else: + return True + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile(r'/s/'+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"/a/\w+")) + self.story.setMetadata('authorId',a['href'].split('/')[2]) + self.story.setMetadata('authorUrl','http://'+self.host+a['href']) + self.story.setMetadata('author',a.text) + + # Find the chapters: + chapters = soup.findAll('a', href=re.compile(r'/s/'+self.story.getMetadata('storyId')+":\d+$")) + if len(chapters) != 0: + for chapter in chapters: + # just in case there's tags, like in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+chapter['href'])) + else: + self.chapterUrls.append((self.story.getMetadata('title'),'http://'+self.host+'/s/'+self.story.getMetadata('storyId'))) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + # surprisingly, the detailed page does not give enough details, so go to author's page + + skip=0 + i=0 + while i == 0: + asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')+"&skip="+str(skip))) + + a = asoup.findAll('td', {'class' : 'lc2'}) + for lc2 in a: + if lc2.find('a')['href'] == '/s/'+self.story.getMetadata('storyId'): + i=1 + break + if a[len(a)-1] == lc2: + skip=skip+10 + + for cat in lc2.findAll('div', {'class' : 'typediv'}): + self.story.addToList('category',cat.text) + + self.story.setMetadata('numWords', lc2.findNext('td', {'class' : 'num'}).text) + + lc4 = lc2.findNext('td', {'class' : 'lc4'}) + + + try: + a = lc4.find('a', href=re.compile(r"/library/show_series.php\?id=\d+")) + i = a.parent.text.split('(')[1].split(')')[0] + self.setSeries(a.text, i) + self.story.setMetadata('seriesUrl','http://'+self.host+a['href']) + except: + pass + try: + a = lc4.find('a', href=re.compile(r"/library/universe.php\?id=\d+")) + self.story.addToList("category",a.text) + except: + pass + + for a in lc4.findAll('span', {'class' : 'help'}): + a.extract() + + self.setDescription('http://'+self.host+'/s/'+self.story.getMetadata('storyId'),lc4.text.split('[More Info')[0]) + + for b in lc4.findAll('b'): + label = b.text + value = b.nextSibling + + if 'For Age' in label: + self.story.setMetadata('rating', value) + + if 'Tags' in label: + for genre in value.split(', '): + self.story.addToList('genre',genre) + + if 'Posted' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value.split('/ (')[0]), self.dateformat)) + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value.split('/ (')[0]), self.dateformat)) + + if 'Concluded' in label: + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value.split('/ (')[0]), self.dateformat)) + + if 'Updated' in label: + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value.split('/ (')[0]), self.dateformat)) + + status = lc4.find('span', {'class' : 'ab'}) + if status != None: + self.story.setMetadata('status', 'In-Progress') + if "Last Activity" in status.text: + self.story.setMetadata('dateUpdated', makeDate(status.text.split('Activity: ')[1].split(')')[0], self.dateformat)) + else: + self.story.setMetadata('status', 'Completed') + + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + # some big chapters are split over several pages + pager = div.find('span', {'class' : 'pager'}) + if pager != None: + urls=pager.findAll('a') + urls=urls[:len(urls)-1] + + + for ur in urls: + soup = bs.BeautifulSoup(self._fetchUrl("http://"+self.getSiteDomain()+ur['href']), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div1 = soup.find('div', {'id' : 'story'}) + + # appending next section + last=div.findAll('p') + next=div1.find('span', {'class' : 'conTag'}).nextSibling + + last[len(last)-1]=last[len(last)-1].append(next) + div.append(div1) + + # removing all the left-over stuff + for a in div.findAll('span'): + a.extract() + + for a in div.findAll('h1'): + a.extract() + for a in div.findAll('h2'): + a.extract() + for a in div.findAll('h3'): + a.extract() + for a in div.findAll('h4'): + a.extract() + for a in div.findAll('br'): + a.extract() + for a in div.findAll('div', {'class' : 'date'}): + a.extract() + + a = div.find('form') + if a != None: + b = a.nextSibling + while b != None: + a.extract() + a=b + b=b.nextSibling + + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_grangerenchantedcom.py b/fanficdownloader/adapters/adapter_grangerenchantedcom.py new file mode 100644 index 0000000..831f08f --- /dev/null +++ b/fanficdownloader/adapters/adapter_grangerenchantedcom.py @@ -0,0 +1,311 @@ +# -*- coding: utf-8 -*- + +# Copyright 2012 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return GrangerEnchantedCom + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class GrangerEnchantedCom(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + self.section=self.parsedUrl.path.split('/',)[1] + + # normalized story URL. + if "malfoymanor" in self.parsedUrl.netloc: + self._setURL('http://malfoymanor.' + self.getSiteDomain() + '/themanor/viewstory.php?sid='+self.story.getMetadata('storyId')) + self.story.addToList("category","The Manor") + else: + self._setURL('http://' + self.getSiteDomain() + '/enchant/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','gech') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%d/%b/%Y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'grangerenchanted.com' + + @classmethod + def getAcceptDomains(cls): + return ['grangerenchanted.com','malfoymanor.grangerenchanted.com'] + + @classmethod + def getSiteExampleURLs(cls): + return "http://grangerenchanted.com/enchant/viewstory.php?sid=1234 http://malfoymanor.grangerenchanted.com/themanor/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return r"http://(malfoymanor.)?grangerenchanted.com/(enchant|themanor)?/viewstory.php\?sid=\d+$" + + ## Login seems to be reasonably standard across eFiction sites. + def needToLoginCheck(self, data): + if 'Registered Users Only' in data \ + or 'There is no such account on our website' in data \ + or "That password doesn't match the one in our database" in data: + return True + else: + return False + + def performLogin(self, url): + params = {} + + if self.password: + params['penname'] = self.username + params['password'] = self.password + else: + params['penname'] = self.getConfig("username") + params['password'] = self.getConfig("password") + params['cookiecheck'] = '1' + params['submit'] = 'Submit' + + if "enchant" in self.section: + loginUrl = 'http://grangerenchanted.com/enchant/user.php?action=login' + else: + loginUrl = 'http://malfoymanor.grangerenchanted.com/themanor/user.php?action=login' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['penname'])) + + d = self._fetchUrl(loginUrl, params) + + if "Member Account" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['penname'])) + raise exceptions.FailedToLogin(url,params['penname']) + return False + else: + return True + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + # Weirdly, different sites use different warning numbers. + # If the title search below fails, there's a good chance + # you need a different number. print data at that point + # and see what the 'click here to continue' url says. + addurl = "&ageconsent=ok&warning=1" + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url) + + m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href']) + self.story.setMetadata('author',a.string) + + # Find the chapters: + for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")): + # just in case there's tags, like in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+self.section+'/'+chapter['href']+addurl)) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + # eFiction sites don't help us out a lot with their meta data + # formating, so it's a little ugly. + + # utility method + def defaultGetattr(d,k): + try: + return d[k] + except: + return "" + + # Rated: NC-17
    etc + labels = soup.findAll('span',{'class':'label'}) + for labelspan in labels: + value = labelspan.nextSibling + label = labelspan.string + + if 'Summary' in label: + ## Everything until the next span class='label' + svalue = "" + while not defaultGetattr(value,'class') == 'label': + svalue += str(value) + value = value.nextSibling + self.setDescription(url,svalue) + #self.story.setMetadata('description',stripHTML(svalue)) + + if 'Rated' in label: + self.story.setMetadata('rating', value) + + if 'Word count' in label: + self.story.setMetadata('numWords', value) + + if 'Read' in label: + self.story.setMetadata('read', value) + + if 'Categories' in label: + cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories')) + for cat in cats: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + for char in chars: + self.story.addToList('characters',char.string) + + if 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=4')) + for genre in genres: + self.story.addToList('genre',genre.string) + + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) + for warning in warnings: + self.story.addToList('warnings',warning.string) + + if 'Completed' in label: + if 'Yes' in value: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label: + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + try: + # Find Series name from series URL. + a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+")) + series_name = a.string + series_url = 'http://'+self.host+'/'+self.section+'/'+a['href'] + + # use BeautifulSoup HTML parser to make everything easier to find. + seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url)) + # can't use ^viewstory...$ in case of higher rated stories with javascript href. + storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+')) + i=1 + for a in storyas: + # skip 'report this' and 'TOC' links + if 'contact.php' not in a['href'] and 'index' not in a['href']: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + except: + # I find it hard to care if the series parsing fails + pass + + try: + self.story.setMetadata('reviews', + stripHTML(soup.find('div',{'id':'sort'}). + findAll('a', href=re.compile(r'^reviews.php'))[1])) + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story1'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_harrypotterfanfictioncom.py b/fanficdownloader/adapters/adapter_harrypotterfanfictioncom.py new file mode 100644 index 0000000..bddf4b6 --- /dev/null +++ b/fanficdownloader/adapters/adapter_harrypotterfanfictioncom.py @@ -0,0 +1,203 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +class HarryPotterFanFictionComSiteAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + self.story.setMetadata('siteabbrev','hp') + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.is_adult=False + + # get storyId from url--url validation guarantees query is only psid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?psid='+self.story.getMetadata('storyId')) + + + @staticmethod + def getSiteDomain(): + return 'www.harrypotterfanfiction.com' + + @classmethod + def getAcceptDomains(cls): + return ['www.harrypotterfanfiction.com','harrypotterfanfiction.com'] + + @classmethod + def getSiteExampleURLs(cls): + return "http://www.harrypotterfanfiction.com/viewstory.php?psid=1234" + + def getSiteURLPattern(self): + return re.escape("http://")+r"(www\.)?"+re.escape("harrypotterfanfiction.com/viewstory.php?psid=")+r"\d+$" + + def needToLoginCheck(self, data): + if 'Registered Users Only' in data \ + or 'There is no such account on our website' in data \ + or "That password doesn't match the one in our database" in data: + return True + else: + return False + + def extractChapterUrlsAndMetadata(self): + + url = self.url+'&index=1' + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + + ## Title + a = soup.find('a', href=re.compile(r'\?psid='+self.story.getMetadata('storyId'))) + self.story.setMetadata('title',stripHTML(a)) + ## javascript:if (confirm('Please note. This story may contain adult themes. By clicking here you are stating that you are over 17. Click cancel if you do not meet this requirement.')) location = '?psid=290995' + if "This story may contain adult themes." in a['href'] and not (self.is_adult or self.getConfig("is_adult")): + raise exceptions.AdultCheckRequired(self.url) + + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?showuid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href']) + self.story.setMetadata('author',a.string) + + ## hpcom doesn't give us total words--but it does give + ## us words/chapter. I'd rather add than fetch and + ## parse another page. + words=0 + for tr in soup.find('table',{'class':'text'}).findAll('tr'): + tdstr = tr.findAll('td')[2].string + if tdstr and tdstr.isdigit(): + words+=int(tdstr) + self.story.setMetadata('numWords',str(words)) + + # Find the chapters: + tablelist = soup.find('table',{'class':'text'}) + for chapter in tablelist.findAll('a', href=re.compile(r'\?chapterid=\d+')): + #javascript:if (confirm('Please note. This story may contain adult themes. By clicking here you are stating that you are over 17. Click cancel if you do not meet this requirement.')) location = '?chapterid=433441&i=1' + # just in case there's tags, like in chapter titles. + chpt=re.sub(r'^.*?(\?chapterid=\d+).*?',r'\1',chapter['href']) + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/viewstory.php'+chpt)) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + ## Finding the metadata is a bit of a pain. Desc is the only thing this color. + desctable= soup.find('table',{'bgcolor':'#f0e8e8'}) + self.setDescription(url,desctable) + #self.story.setMetadata('description',stripHTML(desctable)) + + ## Finding the metadata is a bit of a pain. Most of the meta + ## data is in a center.table without a bgcolor. + #for center in soup.findAll('center'): + table = soup.find('table',{'class':'storymaininfo'}) + if table: + metastr = stripHTML(str(table)).replace('\n',' ').replace('\t',' ') + # Rating: 12+ Story Reviews: 3 + # Chapters: 3 + # Characters: Andromeda, Ted, Bellatrix, R. Lestrange, Lucius, Narcissa, OC + # Genre(s): Fluff, Romance, Young Adult Era: OtherPairings: Other Pairing, Lucius/Narcissa + # Status: Completed + # First Published: 2010.09.02 + # Last Published Chapter: 2010.09.28 + # Last Updated: 2010.09.28 + # Favorite Story Of: 1 users + # Warnings: Scenes of a Mild Sexual Nature + + m = re.match(r".*?Status: Completed.*?",metastr) + if m: + self.story.setMetadata('status','Completed') + else: + self.story.setMetadata('status','In-Progress') + + m = re.match(r".*?Rating: (.+?) Story Reviews.*?",metastr) + if m: + self.story.setMetadata('rating', m.group(1)) + + m = re.match(r".*?Genre\(s\): (.+?) Era.*?",metastr) + if m: + for g in m.group(1).split(','): + self.story.addToList('genre',g) + + m = re.match(r".*?Characters: (.+?) Genre.*?",metastr) + if m: + for g in m.group(1).split(','): + self.story.addToList('characters',g) + + m = re.match(r".*?Warnings: (.+).*?",metastr) + if m: + for w in m.group(1).split(','): + if w != 'Now Warnings': + self.story.addToList('warnings',w) + + m = re.match(r".*?First Published: ([0-9\.]+).*?",metastr) + if m: + self.story.setMetadata('datePublished',makeDate(m.group(1), "%Y.%m.%d")) + + # Updated can have more than one space after it. + m = re.match(r".*?Last Updated: ([0-9\.]+).*?",metastr) + if m: + self.story.setMetadata('dateUpdated',makeDate(m.group(1), "%Y.%m.%d")) + + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + ## most adapters use BeautifulStoneSoup here, but non-Stone + ## allows nested div tags. + soup = bs.BeautifulSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'fluidtext'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) + +def getClass(): + return HarryPotterFanFictionComSiteAdapter + diff --git a/fanficdownloader/adapters/adapter_hennethannunnet.py b/fanficdownloader/adapters/adapter_hennethannunnet.py new file mode 100644 index 0000000..4a5391d --- /dev/null +++ b/fanficdownloader/adapters/adapter_hennethannunnet.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- + +# Copyright 2013 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return HennethAnnunNetAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class HennethAnnunNetAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/stories/chapter.cfm?stid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','htan') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%m/%d/%y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'www.henneth-annun.net' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/stories/chapter.cfm?stid=1234" + + def getSiteURLPattern(self): + return "http://"+self.getSiteDomain()+"/stories/chapter(_view)?.cfm\?stid="+r"\d+$" + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + + if "We're sorry. This story is not available." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: This story is not available.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('h2', {'id':'page_heading'}) + self.story.setMetadata('title',stripHTML(a)) + + # Find the chapters: chapter_view.cfm?stid=6663&spordinal=1" + for chapter in soup.findAll('a', href=re.compile(r'chapter_view.cfm\?stid='+self.story.getMetadata('storyId')+"&spordinal=\d+$")): + # just in case there's tags, like in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/stories/'+chapter['href'])) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + self.story.setMetadata('numWords', soup.find('tr', {'class':'foot'}).findAll('td')[1].text) + + self.setDescription(url,soup.find('div', {'id':'summary'})) + + # Rated: NC-17
    etc + info = soup.find('div', {'id':'storyinformation'}) + labels=info.findAll('b') + for labelspan in labels: + value = labelspan.nextSibling + label = labelspan.string + + if 'Completion' in label: + if 'Complete' in value.string: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Rating' in label: + self.story.setMetadata('rating', value.string) + + if 'Era:' in label: + self.story.addToList('category',value.string) + + if 'Genre' in label: + self.story.addToList('genre',value.string) + + labels=info.findAll('strong') + for labelspan in labels: + value = labelspan.nextSibling + label = labelspan.string + + if 'Author' in label: + value=value.nextSibling + self.story.setMetadata('authorId',value['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+value['href']) + self.story.setMetadata('author',value.string) + + if 'Post' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated:' in label: + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + for char in soup.findAll('a', href=re.compile(r"/resources/bios_view.cfm\?scid=\d+")): + self.story.addToList('characters',stripHTML(char)) + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'class' : 'block chapter'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_hlfictionnet.py b/fanficdownloader/adapters/adapter_hlfictionnet.py new file mode 100644 index 0000000..96deb95 --- /dev/null +++ b/fanficdownloader/adapters/adapter_hlfictionnet.py @@ -0,0 +1,232 @@ +# -*- coding: utf-8 -*- + +# Copyright 2012 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return HLFictionNetAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class HLFictionNetAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','hlf') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%m/%d/%y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'hlfiction.net' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$" + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title and author + a = soup.find('div', {'id' : 'pagetitle'}) + + aut = a.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',aut['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+aut['href']) + self.story.setMetadata('author',aut.string) + aut.extract() + + self.story.setMetadata('title',stripHTML(a)[:(len(a.string)-3)]) + + # Find the chapters: + chapters=soup.find('select') + if chapters != None: + for chapter in chapters.findAll('option'): + # just in case there's tags, like in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/viewstory.php?sid='+self.story.getMetadata('storyId')+'&chapter='+chapter['value'])) + else: + self.chapterUrls.append((self.story.getMetadata('title'),url)) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl'))) + + for list in asoup.findAll('div', {'class' : re.compile('listbox\s+')}): + a = list.find('a') + if ('viewstory.php?sid='+self.story.getMetadata('storyId')) in a['href']: + break + + # eFiction sites don't help us out a lot with their meta data + # formating, so it's a little ugly. + + # utility method + def defaultGetattr(d,k): + try: + return d[k] + except: + return "" + + # Rated: NC-17
    etc + labels = list.findAll('span', {'class' : 'classification'}) + for labelspan in labels: + label = labelspan.string + value = labelspan.nextSibling + + if 'Summary' in label: + ## Everything until the next span class='label' + svalue = "" + while not defaultGetattr(value,'class') == 'classification': + svalue += str(value) + value = value.nextSibling + self.setDescription(url,svalue) + #self.story.setMetadata('description',stripHTML(svalue)) + + if 'Rated' in label: + self.story.setMetadata('rating', value[:len(value)-2]) + + if 'Word count' in label: + self.story.setMetadata('numWords', value) + + if 'Categories' in label: + cats = labelspan.parent.findAll('a',href=re.compile(r'categories.php\?catid=\d+')) + for cat in cats: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + for char in value.string.split(', '): + if not 'None' in char: + self.story.addToList('characters',char) + + if 'Genre' in label: + for genre in value.string.split(', '): + if not 'None' in genre: + self.story.addToList('genre',genre) + + if 'Warnings' in label: + for warning in value.string.split(', '): + if not 'None' in warning: + self.story.addToList('warnings',warning) + + if 'Completed' in label: + if 'Yes' in value: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label: + # there's a stray [ at the end. + #value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + try: + # Find Series name from series URL. + a = list.find('a', href=re.compile(r"series.php\?seriesid=\d+")) + series_name = a.string + series_url = 'http://'+self.host+'/'+a['href'] + + # use BeautifulSoup HTML parser to make everything easier to find. + seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url)) + # can't use ^viewstory...$ in case of higher rated stories with javascript href. + storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+')) + i=1 + for a in storyas: + # skip 'report this' and 'TOC' links + if 'contact.php' not in a['href'] and 'index' not in a['href']: + if ('viewstory.php?sid='+self.story.getMetadata('storyId')) in a['href']: + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_hpfandomnet.py b/fanficdownloader/adapters/adapter_hpfandomnet.py new file mode 100644 index 0000000..16a4cfc --- /dev/null +++ b/fanficdownloader/adapters/adapter_hpfandomnet.py @@ -0,0 +1,233 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +# This function is called by the downloader in all adapter_*.py files +# in this dir to register the adapter class. So it needs to be +# updated to reflect the class below it. That, plus getSiteDomain() +# take care of 'Registering'. +def getClass(): + return HPFandomNetAdapterAdapter # XXX + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class HPFandomNetAdapterAdapter(BaseSiteAdapter): # XXX + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + # XXX Most sites don't have the /eff part. Replace all to remove it usually. + self._setURL('http://' + self.getSiteDomain() + '/eff/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','hpfdm') # XXX + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%Y.%m.%d" # XXX + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'www.hpfandom.net' # XXX + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/eff/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/eff/viewstory.php?sid=")+r"\d+$" + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + url = self.url + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/eff/'+a['href']) + self.story.setMetadata('author',a.string) + + ## Going to get the rest from the author page. + authdata = self._fetchUrl(self.story.getMetadata('authorUrl')) + # fix a typo in the site HTML so I can find the Characters list. + authdata = authdata.replace('
    ','') + + # hpfandom.net only seems to indicate adult-only by javascript on the story/chapter links. + if "javascript:if (confirm('Slash/het fiction which incorporates sexual situations to a somewhat graphic degree and some violence. ')) location = 'viewstory.php?sid=%s'"%self.story.getMetadata('storyId') in authdata \ + and not (self.is_adult or self.getConfig("is_adult")): + raise exceptions.AdultCheckRequired(self.url) + + authsoup = bs.BeautifulSoup(authdata) + + reviewsa = authsoup.find('a', href="reviews.php?sid="+self.story.getMetadata('storyId')+"&a=") + # + # + # + labels = metablock.findAll('td',{'width':'10%'}) + for td in labels: + label = td.string + value = td.nextSibling.string + #print("\nlabel:%s\nvalue:%s\n"%(label,value)) + + if 'Category' in label and value: + cats = td.parent.findAll('a',href=re.compile(r'categories.php')) + catstext = [cat.string for cat in cats] + for cat in catstext: + self.story.addToList('category',cat.string) + + if 'Characters' in label and value: # this site can have Character label with no + # values, apparently. Others as a precaution. + for char in value.split(','): + self.story.addToList('characters',char.strip()) + + if 'Genre' in label and value: + for genre in value.split(','): + self.story.addToList('genre',genre.strip()) + + if 'Warnings' in label and value: + for warning in value.split(','): + if warning.strip() != 'none': + self.story.addToList('warnings',warning.strip()) + + 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)) + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + data = self._fetchUrl(url) + # There's no good wrapper around the chapter text. :-/ + # There are, however, tables with width=100% just above and below the real text. + data = re.sub(r'

    + metablock = reviewsa.findParent("table") + #print("metablock:%s"%metablock) + + ## Title + titlea = metablock.find('a', href=re.compile("viewstory.php")) + #print("titlea:%s"%titlea) + if titlea == None: + raise exceptions.FailedToDownload("Story URL (%s) not found on author's page, can't use chapter URLs"%url) + self.story.setMetadata('title',stripHTML(titlea)) + + # Find the chapters: !!! hpfandom.net differs from every other + # eFiction site--the sid on viewstory for chapters is + # *different* for each chapter + for chapter in soup.findAll('a', {'href':re.compile(r"viewstory.php\?sid=\d+&i=\d+")}): + m = re.match(r'.*?(viewstory.php\?sid=\d+&i=\d+).*?',chapter['href']) + # just in case there's tags, like in chapter titles. + #print("====chapter===%s"%m.group(1)) + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/eff/'+m.group(1))) + + if len(self.chapterUrls) == 0: + self.chapterUrls.append((stripHTML(self.story.getMetadata('title')),url)) + + 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 "" + + summary = metablock.find("td",{"class":"summary"}) + summary.name='span' + self.setDescription(url,summary) + + # words & completed in first row of metablock. + firstrow = stripHTML(metablock.find('tr')) + # A Mother's Love xx Going Grey 1 (G+) by Kiristeen | Reviews - 18 | Words: 27468 | Completed: Yes + m = re.match(r".*?\((?P[^)]+)\).*?Words: (?P\d+).*?Completed: (?PYes|No)",firstrow) + if m != None: + if m.group('rating') != None: + self.story.setMetadata('rating', m.group('rating')) + + if m.group('words') != None: + self.story.setMetadata('numWords', m.group('words')) + + if m.group('status') != None: + if 'Yes' in m.group('status'): + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + + #

    Chapters:4Published:2010.09.29
    Completed:YesUpdated:2010.10.03
    .*?
    ','
    ', + data,count=1,flags=re.DOTALL) + + data = re.sub(r'.*?
    ','
    ', + data,count=1,flags=re.DOTALL) + + soup = bs.BeautifulStoneSoup(data,selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find("div",{'name':'storybody'}) + #print("\n\ndiv:%s\n\n"%div) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_hpfanficarchivecom.py b/fanficdownloader/adapters/adapter_hpfanficarchivecom.py new file mode 100644 index 0000000..b2fa720 --- /dev/null +++ b/fanficdownloader/adapters/adapter_hpfanficarchivecom.py @@ -0,0 +1,224 @@ +# -*- coding: utf-8 -*- + +# Copyright 2012 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return HPFanficArchiveComAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class HPFanficArchiveComAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/stories/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','hpffa') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%B %d, %Y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'hpfanficarchive.com' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/stories/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/stories/viewstory.php?sid=")+r"\d+$" + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/stories/'+a['href']) + self.story.setMetadata('author',a.string) + + # Find the chapters: + for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")): + # just in case there's tags, like in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/stories/'+chapter['href'])) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + # eFiction sites don't help us out a lot with their meta data + # formating, so it's a little ugly. + + # utility method + def defaultGetattr(d,k): + try: + return d[k] + except: + return "" + + # Rated: NC-17
    etc + labels = soup.findAll('span',{'class':'label'}) + for labelspan in labels: + val = labelspan.nextSibling + value = unicode('') + while val and not defaultGetattr(val,'class') == 'label': + value += unicode(val) + val = val.nextSibling + label = labelspan.string + #print("label:%s\nvalue:%s"%(label,value)) + + if 'Summary' in label: + self.setDescription(url,value) + + if 'Rated' in label: + self.story.setMetadata('rating', stripHTML(value)) + + if 'Word count' in label: + self.story.setMetadata('numWords', stripHTML(value)) + + if 'Categories' in label: + cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories')) + for cat in cats: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + for char in chars: + self.story.addToList('characters',char.string) + + if 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) # XXX + for genre in genres: + self.story.addToList('genre',genre.string) + + if 'Pairing' in label: + ships = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=4')) + for ship in ships: + self.story.addToList('ships',ship.string) + + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX + for warning in warnings: + self.story.addToList('warnings',warning.string) + + if 'Completed' in label: + if 'Yes' in stripHTML(value): + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label: + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + try: + # Find Series name from series URL. + a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+")) + series_name = a.string + series_url = 'http://'+self.host+'/stories/'+a['href'] + + # use BeautifulSoup HTML parser to make everything easier to find. + seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url)) + # can't use ^viewstory...$ in case of higher rated stories with javascript href. + storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+')) + i=1 + for a in storyas: + # skip 'report this' and 'TOC' links + if 'contact.php' not in a['href'] and 'index' not in a['href']: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + + soup = bs.BeautifulSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_iketernalnet.py b/fanficdownloader/adapters/adapter_iketernalnet.py new file mode 100644 index 0000000..846aee7 --- /dev/null +++ b/fanficdownloader/adapters/adapter_iketernalnet.py @@ -0,0 +1,283 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return IkEternalNetAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class IkEternalNetAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','ike') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%B %d, %Y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'www.ik-eternal.net' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$" + + ## 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' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['penname'])) + + d = self._fetchUrl(loginUrl, params) + + if "Member Account" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['penname'])) + raise exceptions.FailedToLogin(url,params['penname']) + return False + else: + return True + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + # Weirdly, different sites use different warning numbers. + # If the title search below fails, there's a good chance + # you need a different number. print data at that point + # and see what the 'click here to continue' url says. + addurl = "&warning=1" + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url) + + # 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. + + # Since the warning text can change by warning level, let's + # look for the warning pass url. ksarchive uses + # &warning= -- actually, so do other sites. Must be an + # eFiction book. + + # viewstory.php?sid=1882&warning=4 + # viewstory.php?sid=1654&ageconsent=ok&warning=5 + #print data + #m = re.search(r"'viewstory.php\?sid=1882(&warning=4)'",data) + m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data,selfClosingTags=('p')) #poor formatting of the paragraphs in the title page + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href']) + self.story.setMetadata('author',a.string) + + # Find the chapters: + for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")): + # just in case there's tags, like in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href']+addurl)) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + # eFiction sites don't help us out a lot with their meta data + # formating, so it's a little ugly. + + # utility method + def defaultGetattr(d,k): + try: + return d[k] + except: + return "" + + # Rated: NC-17
    etc + asoup = soup.find('div', {'class': 'listbox'}) + for a in asoup.findAll('p'): + a.name='br' + labels = asoup.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 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) + for genre in genres: + self.story.addToList('genre',genre.string) + + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=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: + # there's a stray [ at the end. + #value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_imagineeficcom.py b/fanficdownloader/adapters/adapter_imagineeficcom.py new file mode 100644 index 0000000..f32c584 --- /dev/null +++ b/fanficdownloader/adapters/adapter_imagineeficcom.py @@ -0,0 +1,290 @@ +# -*- coding: utf-8 -*- + +# Copyright 2013 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return ImagineEFicComAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class ImagineEFicComAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','ime') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%Y.%m.%d" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'imagine.e-fic.com' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$" + + ## 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' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['penname'])) + + d = self._fetchUrl(loginUrl, params) + + if "Member Account" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['penname'])) + raise exceptions.FailedToLogin(url,params['penname']) + return False + else: + return True + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + # Weirdly, different sites use different warning numbers. + # If the title search below fails, there's a good chance + # you need a different number. print data at that point + # and see what the 'click here to continue' url says. + addurl = "&ageconsent=ok&warning=4" + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url) + + m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href']) + self.story.setMetadata('author',a.string) + + # Find the chapters: + for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")): + # just in case there's tags, like in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href']+addurl)) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + # eFiction sites don't help us out a lot with their meta data + # formating, so it's a little ugly. + + # utility method + def defaultGetattr(d,k): + try: + return d[k] + except: + return "" + + + # Rated: NC-17
    etc + labels = soup.findAll('span',{'class':'label'}) + for labelspan in labels: + value = labelspan.nextSibling + label = labelspan.string + + if 'Summary' in label: + ## Everything until the next span class='label' + svalue = "" + while not defaultGetattr(value,'class') == 'label': + svalue += str(value) + value = value.nextSibling + self.setDescription(url,svalue) + #self.story.setMetadata('description',stripHTML(svalue)) + + if 'Rated' in label: + self.story.setMetadata('rating', value) + + if 'Word count' in label: + self.story.setMetadata('numWords', value) + + if 'Categories' in label: + cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories')) + for cat in cats: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + for char in chars: + self.story.addToList('characters',char.string) + + if 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) + for genre in genres: + self.story.addToList('genre',genre.string) + + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=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) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_indeathnet.py b/fanficdownloader/adapters/adapter_indeathnet.py new file mode 100644 index 0000000..5c0175e --- /dev/null +++ b/fanficdownloader/adapters/adapter_indeathnet.py @@ -0,0 +1,200 @@ +# -*- 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 +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return InDeathNetAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class InDeathNetAdapter(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 correct + m = re.match(self.getSiteURLPattern(),url) + if m: + self.story.setMetadata('storyId',m.group('id')) + + # normalized story URL. + self._setURL('http://www.' + self.getSiteDomain() + '/blog/archive/'+self.story.getMetadata('storyId')+'-'+m.group('name')+'/') + else: + raise exceptions.InvalidStoryURL(url, + self.getSiteDomain(), + self.getSiteExampleURLs()) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','idn') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%d %B %Y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'indeath.net' + + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/blog/archive/123-story-in-death/" + + def getSiteURLPattern(self): + # http://www.indeath.net/blog/archive/169-ransom-in-death/ + return re.escape("http://")+re.escape(self.getSiteDomain())+r"/blog/(archive/)?(?P\d+)\-(?P[a-z0-9\-]*)/?$" + + + def getDateFromComponents(self, postmonth, postday): + ym = re.search("Entries\ in\ (?PJanuary|February|March|April|May|June|July|August|September|October|November|December)\ (?P\d{4})",postmonth) + d = re.search("(?P\d{2})\ (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)",postday) + postdate = makeDate(d.group('day')+' '+ym.group('mon')+' '+ym.group('year'),self.dateformat) + return postdate + + def getAuthorData(self): + + mainUrl = self.url.replace("/archive","") + + try: + maindata = self._fetchUrl(mainUrl) + + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.meta) + else: + raise e + + # use BeautifulSoup HTML parser to make everything easier to find. + mainsoup = bs.BeautifulSoup(maindata) + + # find first entry + e = mainsoup.find('div',{'class':"entry"}) + + # get post author as author + d = e.find('div',{'class':"desc"}) + a = d.find('strong') + self.story.setMetadata('author',a.contents[0].string.strip()) + + # Don't seem to be able to get author pages anymore + self.story.setMetadata('authorUrl','http://www.indeath.net/') + self.story.setMetadata('authorId','0') + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + url = self.url + try: + data = self._fetchUrl(url) + + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.meta) + else: + raise e + + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + + # Now go hunting for all the meta data and the chapter list. + + ## Title + h = soup.find('a', id="blog_title") + t = h.find('span') + self.story.setMetadata('title',stripHTML(t.contents[0]).strip()) + + s = t.find('div') + if s != None: + self.setDescription(url,s) + + # Get Author from main blog page since it's not reliably on the archive page + self.getAuthorData() + + # Find the chapters: + chapters=soup.findAll('a', title="View entry", href=re.compile(r'http://www.indeath.net/blog/'+self.story.getMetadata('storyId')+"/entry\-(\d+)\-([^/]*)/$")) + + #reverse the list since newest at the top + chapters.reverse() + + # Get date published & updated from first & last entries + posttable=soup.find('div', id="main_column") + + postmonths=posttable.findAll('th', text=re.compile(r'Entries\ in\ ')) + postmonths.reverse() + + postdates=posttable.findAll('span', _class="desc", text=re.compile('\d{2}\ (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)')) + postdates.reverse() + + self.story.setMetadata('datePublished',self.getDateFromComponents(postmonths[0],postdates[0])) + self.story.setMetadata('dateUpdated',self.getDateFromComponents(postmonths[len(postmonths)-1],postdates[len(postdates)-1])) + + # Process List of Chapters + self.story.setMetadata('numChapters',len(chapters)) + logger.debug("numChapters: (%s)"%self.story.getMetadata('numChapters')) + for x in range(0,len(chapters)): + # just in case there's tags, like in chapter titles. + chapter=chapters[x] + if len(chapters)==1: + self.chapterUrls.append((self.story.getMetadata('title'),chapter['href'])) + else: + ct = stripHTML(chapter) + tnew = re.match("(?i)"+self.story.getMetadata('title')+r" - (?P.*)$",ct) + if tnew: + chaptertitle = tnew.group('newtitle') + else: + chaptertitle = ct + self.chapterUrls.append((chaptertitle,chapter['href'])) + + + + # grab the text for an individual chapter. + def getChapterText(self, url): + logger.debug('Getting chapter text from: %s' % url) + + #chapter=bs.BeautifulSoup('
    ') + data = self._fetchUrl(url) + soup = bs.BeautifulSoup(data,selfClosingTags=('br','hr','span','center')) + + chapter = soup.find("div", "entry_content") + + if None == chapter: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,chapter) + diff --git a/fanficdownloader/adapters/adapter_ksarchivecom.py b/fanficdownloader/adapters/adapter_ksarchivecom.py new file mode 100644 index 0000000..85e47de --- /dev/null +++ b/fanficdownloader/adapters/adapter_ksarchivecom.py @@ -0,0 +1,329 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +# Search for XXX comments--that's where things are most likely to need changing. + +# This function is called by the downloader in all adapter_*.py files +# in this dir to register the adapter class. So it needs to be +# updated to reflect the class below it. That, plus getSiteDomain() +# take care of 'Registering'. +def getClass(): + return KSArchiveComAdapter # XXX + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class KSArchiveComAdapter(BaseSiteAdapter): # XXX + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + # XXX Most sites don't have the /fanfic part. Replace all to remove it usually. + 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','ksa') # 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 + + @classmethod + def getAcceptDomains(cls): + return ['www.ksarchive.com','ksarchive.com'] + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'ksarchive.com' # XXX + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return "http://(www.)?"+re.escape(self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$" + + ## 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. + + # Furthermore, there's a couple sites now with more than + # one warning level for different ratings. And they're + # fussy about it. midnightwhispers has three: 10, 3 & 5. + # we'll try 5 first. + addurl = "&ageconsent=ok&warning=2" # XXX + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + # 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. + + # Since the warning text can change by warning level, let's + # look for the warning pass url. ksarchive uses + # &warning= -- actually, so do other sites. Must be an + # eFiction book. + + # viewstory.php?sid=1882&warning=4 + # viewstory.php?sid=1654&ageconsent=ok&warning=5 + #print data + #m = re.search(r"'viewstory.php\?sid=1882(&warning=4)'",data) + m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) # title's inside a tag. + + # 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',stripHTML(a)) + + # Find the chapters: + for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")): + # just in case there's tags, like in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href']+addurl)) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + # eFiction sites don't help us out a lot with their meta data + # formating, so it's a little ugly. + + # utility method + def defaultGetattr(d,k): + try: + return d[k] + except: + return "" + + # Rated: NC-17
    etc + labels = soup.findAll('span',{'class':'label'}) + for labelspan in labels: + value = labelspan.nextSibling + label = stripHTML(labelspan) + + if 'Summary' in label: + ## Everything until the next span class='label' + svalue = "" + while not defaultGetattr(value,'class') == 'label': + svalue += str(value) + # poor HTML(unclosed

    for one) can cause run on + # over the next label. + if '' in svalue: + svalue = svalue[0:svalue.find('')] + break + else: + 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')) + catstext = [stripHTML(cat) for cat in cats] + for cat in catstext: + # ran across one story with an empty + # tag in the desc once. + if cat and cat.strip() in ('Poetry','Essays'): + self.story.addToList('category',stripHTML(cat)) + + if 'Characters' in label: + self.story.addToList('characters','Kirk') + self.story.addToList('characters','Spock') + chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + charstext = [stripHTML(char) for char in chars] + for char in charstext: + self.story.addToList('characters',stripHTML(char)) + + ## Not all sites use Genre, but there's no harm to + ## leaving it in. Check to make sure the type_id number + ## is correct, though--it's site specific. + if 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) # XXX + genrestext = [stripHTML(genre) for genre in genres] + self.genre = ', '.join(genrestext) + for genre in genrestext: + self.story.addToList('genre',stripHTML(genre)) + + ## In addition to Genre (which is very site specific) KSA + ## has 'Story Type', which is much more what most sites + ## call genre. + if 'Story Type' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=5')) # XXX + genrestext = [stripHTML(genre) for genre in genres] + self.genre = ', '.join(genrestext) + for genre in genrestext: + self.story.addToList('genre',stripHTML(genre)) + + ## Not all sites use Warnings, but there's no harm to + ## leaving it in. Check to make sure the type_id number + ## is correct, though--it's site specific. + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX + warningstext = [stripHTML(warning) for warning in warnings] + self.warning = ', '.join(warningstext) + for warning in warningstext: + self.story.addToList('warnings',stripHTML(warning)) + + if 'Universe' in label: + universes = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=3')) # XXX + universestext = [stripHTML(universe) for universe in universes] + self.universe = ', '.join(universestext) + for universe in universestext: + self.story.addToList('universe',stripHTML(universe)) + + if 'Crossover Fandom' in label: + crossoverfandoms = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=4')) # XXX + crossoverfandomstext = [stripHTML(crossoverfandom) for crossoverfandom in crossoverfandoms] + self.crossoverfandom = ', '.join(crossoverfandomstext) + for crossoverfandom in crossoverfandomstext: + self.story.addToList('crossoverfandom',stripHTML(crossoverfandom)) + + if 'Completed' in label: + if 'Yes' in value: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label: + # there's a stray [ at the end. + #value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + try: + # Find Series name from series URL. + a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+")) + series_name = stripHTML(a) + series_url = 'http://'+self.host+'/'+a['href'] + + # use BeautifulSoup HTML parser to make everything easier to find. + seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url)) + storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+')) + i=1 + for a in storyas: + # skip 'report this' and 'TOC' links + if 'contact.php' not in a['href'] and 'index' not in a['href']: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + data = self._fetchUrl(url) + soup = bs.BeautifulStoneSoup(data, + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + if "A fatal MySQL error was encountered" in data: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Database error on the site reported!" % url) + else: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_libraryofmoriacom.py b/fanficdownloader/adapters/adapter_libraryofmoriacom.py new file mode 100644 index 0000000..3df41bd --- /dev/null +++ b/fanficdownloader/adapters/adapter_libraryofmoriacom.py @@ -0,0 +1,251 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + + +def getClass(): + return LibraryOfMoriaComAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class LibraryOfMoriaComAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/a/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','lom') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%B %d, %Y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'www.libraryofmoria.com' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/a/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/a/viewstory.php?sid=")+r"\d+$" + + + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + addurl = "&ageconsent=ok&warning=3" + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/a/'+a['href']) + self.story.setMetadata('author',a.string) + + # Find the chapters: + for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")): + # just in case there's tags, like in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/a/'+chapter['href']+addurl)) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + # eFiction sites don't help us out a lot with their meta data + # formating, so it's a little ugly. + + # utility method + def defaultGetattr(d,k): + try: + return d[k] + except: + return "" + + # Rated: NC-17
    etc + labels = soup.findAll('span',{'class':'label'}) + for labelspan in labels: + value = labelspan.nextSibling + label = labelspan.string + + if 'Summary' in label: + ## Everything until the next span class='label' + svalue = "" + while not defaultGetattr(value,'class') == 'label': + svalue += str(value) + value = value.nextSibling + self.setDescription(url,svalue) + #self.story.setMetadata('description',stripHTML(svalue)) + + if 'Rated' in label: + self.story.setMetadata('rating', value) + + if 'Word count' in label: + self.story.setMetadata('numWords', value) + + if 'Categories' in label: + cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories')) + catstext = [cat.string for cat in cats] + for cat in catstext: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + charstext = [char.string for char in chars] + for char in charstext: + self.story.addToList('characters',char.string) + + if 'Type' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) + for genre in genres: + self.story.addToList('genre',genre.string) + + if 'Warning' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=5')) + for warning in warnings: + self.story.addToList('warnings',warning.string) + + if 'Completed' in label: + if 'Yes' in value: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label: + # there's a stray [ at the end. + #value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + try: + # Find Series name from series URL. + a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+")) + series_name = a.string + series_url = 'http://'+self.host+'/a/'+a['href'] + + # use BeautifulSoup HTML parser to make everything easier to find. + seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url)) + storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+')) + i=1 + for a in storyas: + # skip 'report this' and 'TOC' links + if 'contact.php' not in a['href'] and 'index' not in a['href']: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_literotica.py b/fanficdownloader/adapters/adapter_literotica.py new file mode 100644 index 0000000..1a757b6 --- /dev/null +++ b/fanficdownloader/adapters/adapter_literotica.py @@ -0,0 +1,267 @@ +# -*- coding: utf-8 -*- + +# Copyright 2013 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 +import urlparse +import time + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +class LiteroticaSiteAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["utf8", + "Windows-1252"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + + self.story.setMetadata('siteabbrev','litero') + + # normalize to first chapter. Not sure if they ever have more than 2 digits. + storyId = self.parsedUrl.path.split('/',)[2] + # replace later chapters with first chapter but don't remove numbers + # from the URL that disambiguate stories with the same title. + storyId = re.sub("-ch-?\d\d", "", storyId) + self.story.setMetadata('storyId', storyId) + + ## accept m(mobile)url, but use www. + url = re.sub("^(www|german|spanish|french|dutch|italian|romanian|portuguese|other)\.i", + "\1", + url) + + ## strip ?page=... + url = re.sub("\?page=.*$", "", url) + + ## set url + self._setURL(url) + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = '%m/%d/%y' + + @staticmethod + def getSiteDomain(): + return 'literotica.com' + + @classmethod + def getAcceptDomains(cls): + return ['www.literotica.com', + 'www.i.literotica.com', + 'german.literotica.com', + 'german.i.literotica.com', + 'spanish.literotica.com', + 'spanish.i.literotica.com', + 'french.literotica.com', + 'french.i.literotica.com', + 'dutch.literotica.com', + 'dutch.i.literotica.com', + 'italian.literotica.com', + 'italian.i.literotica.com', + 'romanian.literotica.com', + 'romanian.i.literotica.com', + 'portuguese.literotica.com', + 'portuguese.i.literotica.com', + 'other.literotica.com', + 'other.i.literotica.com'] + + @classmethod + def getSiteExampleURLs(cls): + return "http://www.literotica.com/s/story-title https://www.literotica.com/s/story-title http://portuguese.literotica.com/s/story-title http://german.literotica.com/s/story-title" + + def getSiteURLPattern(self): + return r"https?://(www|german|spanish|french|dutch|italian|romanian|portuguese|other)(\.i)?\.literotica\.com/s/([a-zA-Z0-9_-]+)" + + def extractChapterUrlsAndMetadata(self): + """ + NOTE: Some stories can have versions, + e.g. /my-story-ch-05-version-10 + NOTE: If two stories share the same title, a running index is added, + e.g.: /my-story-ch-02-1 + Strategy: + * Go to author's page, search for the current story link, + * If it's in a tr.root-story => One-part story + * , get metadata and be done + * If it's in a tr.sl => Chapter in series + * Search up from there until we find a tr.ser-ttl (this is the + story) + * Gather metadata + * Search down from there for all tr.sl until the next + tr.ser-ttl, foreach + * Chapter link is there + """ + + if not (self.is_adult or self.getConfig("is_adult")): + raise exceptions.AdultCheckRequired(self.url) + + logger.debug("Chapter/Story URL: <%s> " % self.url) + try: + data1 = self._fetchUrl(self.url) + soup1 = bs.BeautifulSoup(data1) + #strip comments from soup + [comment.extract() for comment in soup1.findAll(text=lambda text:isinstance(text, bs.Comment))] + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + # author + a = soup1.find("span", "b-story-user-y") + self.story.setMetadata('authorId', urlparse.parse_qs(a.a['href'].split('?')[1])['uid'][0]) + authorurl = a.a['href'] + if authorurl.startswith('//'): + authorurl = self.parsedUrl.scheme+':'+authorurl + self.story.setMetadata('authorUrl', authorurl) + self.story.setMetadata('author', a.text) + + # get the author page + try: + dataAuth = self._fetchUrl(authorurl) + soupAuth = bs.BeautifulSoup(dataAuth) + #strip comments from soup + [comment.extract() for comment in soupAuth.findAll(text=lambda text:isinstance(text, bs.Comment))] + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(authorurl) + else: + raise e + + ## Find link to url in author's page + ## site has started using //domain.name/asdf urls remove https?: from front + storyLink = soupAuth.find('a', href=self.url[self.url.index(':')+1:]) + + if storyLink is not None: + urlTr = storyLink.parent.parent + if urlTr['class'] == "sl": + isSingleStory = False + else: + isSingleStory = True + else: + raise exceptions.FailedToDownload("Couldn't find story <%s> on author's page <%s>" % (url, authorurl)) + + if isSingleStory: + self.story.setMetadata('title', storyLink.text) + self.story.setMetadata('description', urlTr.findAll("td")[1].text) + self.story.addToList('eroticatags', urlTr.findAll("td")[2].text) + date = urlTr.findAll('td')[-1].text + self.story.setMetadata('datePublished', makeDate(date, self.dateformat)) + self.story.setMetadata('dateUpdated',makeDate(date, self.dateformat)) + self.chapterUrls = [(storyLink.text, self.url)] + else: + seriesTr = urlTr.previousSibling + while seriesTr['class'] != 'ser-ttl': + seriesTr = seriesTr.previousSibling + m = re.match("^(?P.*?):\s(?P<numChapters>\d+)\sPart\sSeries$", seriesTr.find("strong").text) + self.story.setMetadata('title', m.group('title')) + + ## Walk the chapters + chapterTr = seriesTr.nextSibling + self.chapterUrls = [] + dates = [] + descriptions = [] + while chapterTr is not None and chapterTr['class'] == 'sl': + descriptions.append(chapterTr.findAll("td")[1].text) + chapterLink = chapterTr.find("td", "fc").find("a") + self.chapterUrls.append((chapterLink.text, "http:" + chapterLink["href"])) + self.story.addToList('eroticatags', chapterTr.findAll("td")[2].text) + dates.append(makeDate(chapterTr.findAll('td')[-1].text, self.dateformat)) + chapterTr = chapterTr.nextSibling + + ## Set description to joint chapter descriptions + self.story.setMetadata('description', " / ".join(descriptions)) + + ## Set the oldest date as publication date, the newest as update date + dates.sort() + self.story.setMetadata('datePublished', dates[0]) + self.story.setMetadata('dateUpdated', dates[-1]) + + # normalize on first chapter URL. + self._setURL(self.chapterUrls[0][1]) + + self.story.setMetadata('numChapters', len(self.chapterUrls)) + + # set storyId to 'title-author' to avoid duplicates + # self.story.setMetadata('storyId', + # re.sub("[^a-z0-9]", "", self.story.getMetadata('title').lower()) + # + "-" + # + re.sub("[^a-z0-9]", "", self.story.getMetadata('author').lower())) + + return + + def getChapterText(self, url): + logger.debug('Getting chapter text from <%s>' % url) + data1 = self._fetchUrl(url) + # brute force approach to replace the wrapping <p> tag. If + # done by changing tag name, it causes problems with nested + # <p> tags. + data1 = data1.replace('<div class="b-story-body-x x-r15"><div><p>','<div class="b-story-body-x x-r15"><div>') + soup1 = bs.BeautifulSoup(data1) + + #strip comments from soup + [comment.extract() for comment in soup1.findAll(text=lambda text:isinstance(text, bs.Comment))] + + # get story text + story1 = soup1.find('div', 'b-story-body-x').div + #print("story1:%s"%story1) + # story1.name='div' + story1.append('<br />') + storytext = self.utf8FromSoup(url,story1) + + # find num pages + pgs = int(soup1.find("span", "b-pager-caption-t r-d45").string.split(' ')[0]) + logger.debug("pages: "+str(pgs)) + + # get all the pages + for i in xrange(2, pgs+1): + try: + logger.debug("fetching page "+str(i)) + time.sleep(0.5) + data2 = self._fetchUrl(url, {'page': i}) + # brute force approach to replace the wrapping <p> tag. If + # done by changing tag name, it causes problems with nested + # <p> tags. + data2 = data2.replace('<div class="b-story-body-x x-r15"><div><p>','<div class="b-story-body-x x-r15"><div>') + soup2 = bs.BeautifulSoup(data2) + [comment.extract() for comment in soup2.findAll(text=lambda text:isinstance(text, bs.Comment))] + story2 = soup2.find('div', 'b-story-body-x').div + # story2.name='div' + story2.append('<br />') + storytext += self.utf8FromSoup(url,story2) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(url) + else: + raise e + return storytext + + +def getClass(): + return LiteroticaSiteAdapter + + + diff --git a/fanficdownloader/adapters/adapter_lotrfanfictioncom.py b/fanficdownloader/adapters/adapter_lotrfanfictioncom.py new file mode 100644 index 0000000..b20c2ec --- /dev/null +++ b/fanficdownloader/adapters/adapter_lotrfanfictioncom.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- + +# Copyright 2014 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +from base_efiction_adapter import BaseEfictionAdapter + +class TheLOTRFanFictionSiteAdapter(BaseEfictionAdapter): + + @staticmethod + def getSiteDomain(): + return 'lotrfanfiction.com' + + @classmethod + def getSiteAbbrev(seluuf): + return 'lotrff' + + @classmethod + def getDateFormat(self): + return "%d/%m/%y" + +def getClass(): + return TheLOTRFanFictionSiteAdapter diff --git a/fanficdownloader/adapters/adapter_lumossycophanthexcom.py b/fanficdownloader/adapters/adapter_lumossycophanthexcom.py new file mode 100644 index 0000000..805d752 --- /dev/null +++ b/fanficdownloader/adapters/adapter_lumossycophanthexcom.py @@ -0,0 +1,238 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return LumosSycophantHexComAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class LumosSycophantHexComAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','lsph') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%m/%d/%Y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'lumos.sycophanthex.com' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$" + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + 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=19" + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + # 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 "Age Consent Required" in data: + 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 + pt = soup.find('div', {'id' : 'pagetitle'}) + a = pt.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = pt.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) + + rating=pt.text.split('(')[1].split(')')[0] + self.story.setMetadata('rating', rating) + + # 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'}) + + value = labels[0].previousSibling + svalue = "" + while value != None: + val = value + value = value.previousSibling + while not defaultGetattr(val,'class') == 'label': + svalue += str(val) + val = val.nextSibling + self.setDescription(url,svalue) + + for labelspan in labels: + value = labelspan.nextSibling + label = labelspan.string + + if 'Word count' in label: + self.story.setMetadata('numWords', value.split(' -')[0]) + + if 'Categories' in label: + cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories')) + for cat in cats: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + for char in chars: + self.story.addToList('characters',char.string) + + if 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) + for genre in genres: + self.story.addToList('genre',genre.string) + + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) + for warning in warnings: + self.story.addToList('warnings',warning.string) + + if 'Complete' 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.split(' -')[0]), self.dateformat)) + + if 'Updated' in label: + # there's a stray [ at the end. + #value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + try: + # Find Series name from series URL. + a = soup.find('a', href=re.compile(r"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)) + storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_mediaminerorg.py b/fanficdownloader/adapters/adapter_mediaminerorg.py new file mode 100644 index 0000000..261b1f7 --- /dev/null +++ b/fanficdownloader/adapters/adapter_mediaminerorg.py @@ -0,0 +1,237 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +class MediaMinerOrgSiteAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + self.story.setMetadata('siteabbrev','mm') + 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. + + # get storyId from url--url validation guarantees query correct + m = re.match(self.getSiteURLPattern(),url) + if m: + self.story.setMetadata('storyId',m.group('id')) + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/fanfic/view_st.php/'+self.story.getMetadata('storyId')) + else: + raise exceptions.InvalidStoryURL(url, + self.getSiteDomain(), + self.getSiteExampleURLs()) + + @staticmethod + def getSiteDomain(): + return 'www.mediaminer.org' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/fanfic/view_st.php/123456 http://"+cls.getSiteDomain()+"/fanfic/view_ch.php/1234123/123444#fic_c" + + def getSiteURLPattern(self): + ## http://www.mediaminer.org/fanfic/view_st.php/76882 + ## http://www.mediaminer.org/fanfic/view_ch.php/167618/594087#fic_c + return re.escape("http://"+self.getSiteDomain())+\ + "/fanfic/view_(st|ch)\.php/"+r"(?P<id>\d+)(/\d+(#fic_c)?)?$" + + def extractChapterUrlsAndMetadata(self): + + url = self.url + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + + # [ A - All Readers ], strip '[' ']' + ## Above title because we remove the smtxt font to get title. + smtxt = soup.find("font",{"class":"smtxt"}) + if not smtxt: + raise exceptions.StoryDoesNotExist(self.url) + rating = smtxt.string[1:-1] + self.story.setMetadata('rating',rating) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"/fanfic/src.php/u/\d+")) + self.story.setMetadata('authorId',a['href'].split('/')[-1]) + self.story.setMetadata('authorUrl','http://'+self.host+a['href']) + self.story.setMetadata('author',a.string) + + ## Title - Good grief. Title varies by chaptered, 1chapter and 'type=one shot'--and even 'one-shot's can have titled chapter. + ## But, if colspan=2, there's no chapter title. + ## <td class="ffh">Atmosphere: Chapter 1</b> <font class="smtxt">[ P - Pre-Teen ]</font></td> + ## <td colspan=2 class="ffh">Hearts of Ice <font class="smtxt">[ P - Pre-Teen ]</font></td> + ## <td colspan=2 class="ffh">Suzaku no Princess <font class="smtxt">[ P - Pre-Teen ]</font></td> + ## <td class="ffh">The Kraut, The Bartender, and The Drunkard: Chapter 1</b> <font class="smtxt">[ P - Pre-Teen ]</font></td> + ## <td class="ffh">Betrayal and Justice: A Cold Heart</b> <font size="-1">( Chapter 1 )</font> <font class="smtxt">[ A - All Readers ]</font></td> + ## <td class="ffh">Question and Answer: Question and Answer</b> <font size="-1">( One-Shot )</font> <font class="smtxt">[ A - All Readers ]</font></td> + title = soup.find('td',{'class':'ffh'}) + for font in title.findAll('font'): + font.extract() # removes 'font' tags from inside the td. + if title.has_key('colspan'): + titlet = stripHTML(title) + else: + ## No colspan, it's part chapter title--even if it's a one-shot. + titlet = ':'.join(stripHTML(title).split(':')[:-1]) # strip trailing 'Chapter X' or chapter title + self.story.setMetadata('title',titlet) + ## The story title is difficult to reliably parse from the + ## story pages. Getting it from the author page is, but costs + ## another fetch. + # authsoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl'))) + # titlea = authsoup.find('a',{'href':'/fanfic/view_st.php/'+self.story.getMetadata('storyId')}) + # self.story.setMetadata('title',titlea.text) + + # save date from first for later. + firstdate=None + + # Find the chapters + select = soup.find('select',{'name':'cid'}) + if not select: + self.chapterUrls.append(( self.story.getMetadata('title'),self.url)) + else: + for option in select.findAll("option"): + chapter = stripHTML(option.string) + ## chapter can be: Chapter 7 [Jan 23, 2011] + ## or: Vigilant Moonlight ( Chapter 1 ) [Jan 30, 2004] + ## or even: Prologue ( Prologue ) [Jul 31, 2010] + m = re.match(r'^(.*?) (\( .*? \) )?\[(.*?)\]$',chapter) + chapter = m.group(1) + # save date from first for later. + if not firstdate: + firstdate = m.group(3) + self.chapterUrls.append((chapter,'http://'+self.host+'/fanfic/view_ch.php/'+self.story.getMetadata('storyId')+'/'+option['value'])) + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + # category + # <a href="/fanfic/src.php/a/567">Ranma 1/2</a> + for a in soup.findAll('a',href=re.compile(r"^/fanfic/src.php/a/")): + self.story.addToList('category',a.string) + + # genre + # <a href="/fanfic/src.php/a/567">Ranma 1/2</a> + for a in soup.findAll('a',href=re.compile(r"^/fanfic/src.php/g/")): + self.story.addToList('genre',a.string) + + # if firstdate, then the block below will only have last updated. + if firstdate: + self.story.setMetadata('datePublished', makeDate(firstdate, "%b %d, %Y")) + # Everything else is in <tr bgcolor="#EEEED4"> + + metastr = stripHTML(soup.find("tr",{"bgcolor":"#EEEED4"})).replace('\n',' ').replace('\r',' ').replace('\t',' ') + # Latest Revision: August 03, 2010 + m = re.match(r".*?(?:Latest Revision|Uploaded On): ([a-zA-Z]+ \d\d, \d\d\d\d)",metastr) + if m: + self.story.setMetadata('dateUpdated', makeDate(m.group(1), "%B %d, %Y")) + if not firstdate: + self.story.setMetadata('datePublished', + self.story.getMetadataRaw('dateUpdated')) + + else: + self.story.setMetadata('dateUpdated', + self.story.getMetadataRaw('datePublished')) + + # Words: 123456 + m = re.match(r".*?\| Words: (\d+) \|",metastr) + if m: + self.story.setMetadata('numWords', m.group(1)) + + # Summary: .... + m = re.match(r".*?Summary: (.*)$",metastr) + if m: + self.setDescription(url, m.group(1)) + #self.story.setMetadata('description', m.group(1)) + + # completed + m = re.match(r".*?Status: Completed.*?",metastr) + if m: + self.story.setMetadata('status','Completed') + else: + self.story.setMetadata('status','In-Progress') + + return + + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + data=self._fetchUrl(url) + soup = bs.BeautifulStoneSoup(data, + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + anchor = soup.find('a',{'name':'fic_c'}) + + if None == anchor: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + ## find divs with align=left, those are paragraphs in newer stories. + divlist = anchor.findAllNext('div',{'align':'left'}) + if divlist: + for div in divlist: + div.name='p' # convert to <p> mediaminer uses div with + # a margin for paragraphs. + anchor.append(div) # cheat! stuff all the content + # divs into anchor just as a + # holder. + del div['style'] + del div['align'] + anchor.name='div' + return self.utf8FromSoup(url,anchor) + + else: + logger.debug('Using kludgey text find for older mediaminer story.') + ## Some older mediaminer stories are unparsable with BeautifulSoup. + ## Really nasty formatting. Sooo... Cheat! Parse it ourselves a bit first. + ## Story stuff falls between: + data = "<div id='HERE'>" + data[data.find('<a name="fic_c">'):] +"</div>" + soup = bs.BeautifulStoneSoup(data, + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + for tag in soup.findAll('td',{'class':'ffh'}) + \ + soup.findAll('div',{'class':'acl'}) + \ + soup.findAll('div',{'class':'footer smtxt'}) + \ + soup.findAll('table',{'class':'tbbrdr'}): + tag.extract() # remove tag from soup. + + return self.utf8FromSoup(url,soup) + + +def getClass(): + return MediaMinerOrgSiteAdapter + diff --git a/fanficdownloader/adapters/adapter_merlinficdtwinscouk.py b/fanficdownloader/adapters/adapter_merlinficdtwinscouk.py new file mode 100644 index 0000000..ea518ea --- /dev/null +++ b/fanficdownloader/adapters/adapter_merlinficdtwinscouk.py @@ -0,0 +1,294 @@ +# -*- coding: utf-8 -*- + +# Copyright 2012 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return 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]) + + + # 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' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$" + + ## 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' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['penname'])) + + d = self._fetchUrl(loginUrl, params) + + if "Member Account" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['penname'])) + raise exceptions.FailedToLogin(url,params['penname']) + return False + else: + return True + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + # Weirdly, different sites use different warning numbers. + # If the title search below fails, there's a good chance + # you need a different number. print data at that point + # and see what the 'click here to continue' url says. + addurl = "&ageconsent=ok&warning=4" + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url) + + m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href']) + self.story.setMetadata('author',a.string) + + # Find the chapters: + for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")): + # just in case there's tags, like <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) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_midnightwhispersca.py b/fanficdownloader/adapters/adapter_midnightwhispersca.py new file mode 100644 index 0000000..ec884d6 --- /dev/null +++ b/fanficdownloader/adapters/adapter_midnightwhispersca.py @@ -0,0 +1,290 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +# Search for XXX comments--that's where things are most likely to need changing. + +# This function is called by the downloader in all adapter_*.py files +# in this dir to register the adapter class. So it needs to be +# updated to reflect the class below it. That, plus getSiteDomain() +# take care of 'Registering'. +def getClass(): + return MidnightwhispersCaAdapter # XXX + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class MidnightwhispersCaAdapter(BaseSiteAdapter): # XXX + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + # XXX Most sites don't have the /fanfic part. Replace all to remove it usually. + 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','mw') # 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 + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'www.midnightwhispers.ca' # XXX + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$" + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + 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. + + # Furthermore, there's a couple sites now with more than + # one warning level for different ratings. And they're + # fussy about it. midnightwhispers has three: 10, 3 & 5. + # we'll try 5 first. + addurl = "&ageconsent=ok&warning=5" # XXX + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + # 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. + + # Since the warning text can change by warning level, let's + # look for the warning pass url. nfacommunity uses + # &warning= -- actually, so do other sites. Must be an + # eFiction book. + + # viewstory.php?sid=1882&warning=4 + # viewstory.php?sid=1654&ageconsent=ok&warning=5 + #print data + #m = re.search(r"'viewstory.php\?sid=1882(&warning=4)'",data) + m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) # title's inside a <b> tag. + + # 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')) + catstext = [cat.string for cat in cats] + for cat in catstext: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + charstext = [char.string for char in chars] + for char in charstext: + self.story.addToList('characters',char.string) + + ## Not all sites use Genre, but there's no harm to + ## leaving it in. Check to make sure the type_id number + ## is correct, though--it's site specific. + if 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX + genrestext = [genre.string for genre in genres] + self.genre = ', '.join(genrestext) + for genre in genrestext: + self.story.addToList('genre',genre.string) + + ## Not all sites use Warnings, but there's no harm to + ## leaving it in. Check to make sure the type_id number + ## is correct, though--it's site specific. + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX + warningstext = [warning.string for warning in warnings] + self.warning = ', '.join(warningstext) + for warning in warningstext: + self.story.addToList('warnings',warning.string) + + if 'Completed' in label: + if 'Yes' in value: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label: + # there's a stray [ at the end. + #value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + try: + # Find Series name from series URL. + a = soup.find('a', href=re.compile(r"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)) + storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + # skip 'report this' and 'TOC' links + if 'contact.php' not in a['href'] and 'index' not in a['href']: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + data = self._fetchUrl(url) + soup = bs.BeautifulStoneSoup(data, + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + if "A fatal MySQL error was encountered" in data: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Database error on the site reported!" % url) + else: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_mugglenetcom.py b/fanficdownloader/adapters/adapter_mugglenetcom.py new file mode 100644 index 0000000..261a623 --- /dev/null +++ b/fanficdownloader/adapters/adapter_mugglenetcom.py @@ -0,0 +1,336 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +# This function is called by the downloader in all adapter_*.py files +# in this dir to register the adapter class. So it needs to be +# updated to reflect the class below it. That, plus getSiteDomain() +# take care of 'Registering'. +def getClass(): + return MuggleNetComAdapter # XXX + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class MuggleNetComAdapter(BaseSiteAdapter): # XXX + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + 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','mgln') # 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 + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. + return 'fanfiction.mugglenet.com' + + @classmethod + def getAcceptDomains(cls): + return ['fanfiction.mugglenet.com','fanfic.mugglenet.com'] + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://")+r"fanfic(tion)?\.mugglenet\.com"+re.escape("/viewstory.php?sid=")+r"\d+$" + + ## Login seems to be reasonably standard across eFiction sites. + def needToLoginCheck(self, data): + if "class='errortext'>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&sid='+self.story.getMetadata('storyId') + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['penname'])) + + d = self._fetchUrl(loginUrl, params) + + if "Member Account" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['penname'])) + raise exceptions.FailedToLogin(url,params['penname']) + return False + else: + return True + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + # Weirdly, different sites use different warning numbers. + # If the title search below fails, there's a good chance + # you need a different number. print data at that point + # and see what the 'click here to continue' url says. + # http://fanfiction.mugglenet.com/viewstory.php?sid=91079&ageconsent=ok&warning=3 + addurl = "&ageconsent=ok&warning=3" # XXX &warning=5 + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url) + #print("\nurl:%s\ndata:\n%s\n"%(url,data)) + + # 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. + + # Since the warning text can change by warning level, let's + # look for the warning pass url. nfacommunity uses + # &warning= -- actually, so do other sites. Must be an + # eFiction book. + + # viewstory.php?sid=1882&warning=4 + # viewstory.php?sid=1654&ageconsent=ok&warning=5 + #print data + #m = re.search(r"'viewstory.php\?sid=1882(&warning=4)'",data) + m = re.search(r"'viewstory.php\?sid=%s((?:&ageconsent=ok)?&warning=\d+)'"%self.story.getMetadata('storyId'),data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href']) + self.story.setMetadata('author',a.string) + + # Find the chapters: + for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")): + # just in case there's tags, like <i> in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href']+addurl)) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + + # Not good enough-- content can contain a ('), which ends the content prematurely. + # metadesc = soup.find('meta',{'name':'description'}) + # print("removeAllEntities(metadesc['content']):\n%s\n"%removeAllEntities(metadesc['content'])) + start='<span class="label">Summary: </span>' + end='<span class="label">Rated:</span>' + summarydata = data[data.index(start)+len(start):data.index(end)] + #print("summarydata:\n%s\n"%summarydata) + self.setDescription(url,bs.BeautifulSoup(summarydata)) + + # 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 + + # not good enough--poorly formated summary html will break it. + # 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')) + catstext = [cat.string for cat in cats] + for cat in catstext: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + charstext = [char.string for char in chars] + for char in charstext: + self.story.addToList('characters',char.string) + + ## Not all sites use Genre, but there's no harm to + ## leaving it in. Check to make sure the type_id number + ## is correct, though--it's site specific. + if 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX + genrestext = [genre.string for genre in genres] + self.genre = ', '.join(genrestext) + for genre in genrestext: + self.story.addToList('genre',genre.string) + + ## Not all sites use Warnings, but there's no harm to + ## leaving it in. Check to make sure the type_id number + ## is correct, though--it's site specific. + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX + warningstext = [warning.string for warning in warnings] + self.warning = ', '.join(warningstext) + for warning in warningstext: + self.story.addToList('warnings',warning.string) + + if 'Completed' in label: + if 'Yes' in value: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label: + # there's a stray [ at the end. + #value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + try: + # Find Series name from series URL. + a = soup.find('a', href=re.compile(r"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)) + storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+')) + i=1 + for a in storyas: + # skip 'report this' and 'TOC' links + if 'contact.php' not in a['href'] and 'index' not in a['href']: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_nationallibrarynet.py b/fanficdownloader/adapters/adapter_nationallibrarynet.py new file mode 100644 index 0000000..c9d7531 --- /dev/null +++ b/fanficdownloader/adapters/adapter_nationallibrarynet.py @@ -0,0 +1,213 @@ +# -*- coding: utf-8 -*- + +# Copyright 2012 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return NationalLibraryNetAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class NationalLibraryNetAdapter(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 storyid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?storyid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','ntlb') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%m-%d-%y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + return 'national-library.net' + + @classmethod + def getAcceptDomains(cls): + return ['www.national-library.net','national-library.net'] + + @classmethod + def getSiteExampleURLs(cls): + # ONLY the stories archived on or after June 17, 2006 and that are hosted on the website: + return "http://"+cls.getSiteDomain()+"/viewstory.php?storyid=1234" + + def getSiteURLPattern(self): + return re.escape("http://")+"(www\.)?"+re.escape(self.getSiteDomain()+"/viewstory.php?storyid=")+r"\d+$" + + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('h1') + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"authorresults.php\?author=\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 p in soup.findAll('p'): + chapters = p.findAll('a', href=re.compile(r'viewstory.php\?storyid='+self.story.getMetadata('storyId')+"&chapnum=\d+$")) + if len(chapters) > 0: + for chapter in chapters: + # just in case there's tags, like <i> in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href'])) + break + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + self.story.setMetadata('status', 'Completed') + + # <span class="label">Rated:</span> NC-17<br /> etc + labels = soup.findAll('b') + for x in range(2,len(labels)): + value = labels[x].nextSibling + label = labels[x].string + + if 'Summary' in label: + self.setDescription(url,value) + #self.story.setMetadata('description',stripHTML(svalue)) + + if 'Rating' in label: + self.story.setMetadata('rating', stripHTML(value.nextSibling)) + + if 'Word Count' in label: + self.story.setMetadata('numWords', value.string) + + if 'Category' in label: + for cat in value.string.split(', '): + self.story.addToList('category',cat) + if 'Crossover Shows' in label: + for cat in value.string.split(', '): + if "No Show" not in cat: + self.story.addToList('category',cat) + + if 'Character' in label: + 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) + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Series' in label: + self.setSeries(stripHTML(value.nextSibling), value.nextSibling.nextSibling.string[2:]) + self.story.setMetadata('seriesUrl','http://'+self.host+'/'+value.nextSibling['href']) + + asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl'))) + story=asoup.find('a', href=re.compile(r'viewstory.php\?storyid='+self.story.getMetadata('storyId'))) + + a=story.findNext(text=re.compile('Genre')).parent.nextSibling.string.split(', ') + for genre in a: + self.story.setMetadata('genre', genre) + + a=story.findNext(text=re.compile('Archived')) + self.story.setMetadata('datePublished', makeDate(stripHTML(a.parent.nextSibling), self.dateformat)) + self.story.setMetadata('dateUpdated', makeDate(stripHTML(a.parent.nextSibling), self.dateformat)) + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div') + + # bit messy since higly inconsistent + for p in soup.findAll('p', {'align' : 'center'}): + p.extract() + p = soup.findAll('p') + for x in range(0,3): + p[x].extract() + if "Chapters: " in stripHTML(p[3]): + p[3].extract() + for x in range(len(p)-2,len(p)-1): + p[x].extract() + + for p in soup.findAll('h1'): + p.extract() + for p in soup.findAll('h3'): + p.extract() + for p in soup.findAll('a'): + p.extract() + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_ncisficcom.py b/fanficdownloader/adapters/adapter_ncisficcom.py new file mode 100644 index 0000000..dbbf3fb --- /dev/null +++ b/fanficdownloader/adapters/adapter_ncisficcom.py @@ -0,0 +1,219 @@ +# -*- coding: utf-8 -*- + +# Copyright 2012 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return NCISFicComAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class NCISFicComAdapter(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 storyid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?storyid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','ncisf') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%m-%d-%y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + return 'ncisfic.com' + + @classmethod + def getAcceptDomains(cls): + return ['www.ncisfic.com','ncisfic.com'] + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?storyid=1234" + + def getSiteURLPattern(self): + return re.escape("http://")+"(www\.)?"+re.escape(self.getSiteDomain()+"/viewstory.php?storyid=")+r"\d+$" + + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('h1') + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"authorresults.php\?author=\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 p in soup.findAll('p'): + chapters = p.findAll('a', href=re.compile(r'viewstory.php\?storyid='+self.story.getMetadata('storyId')+"&chapnum=\d+$")) + if len(chapters) > 0: + for chapter in chapters: + # just in case there's tags, like <i> in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href'])) + break + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + self.story.setMetadata('status', 'Completed') + + # <span class="label">Rated:</span> NC-17<br /> etc + labels = soup.findAll('b') + for x in range(2,len(labels)): + value = labels[x].nextSibling + label = labels[x].string + + if 'Summary' in label: + self.setDescription(url,value) + #self.story.setMetadata('description',stripHTML(svalue)) + + if 'Rating' in label: + self.story.setMetadata('rating', stripHTML(value.nextSibling)) + + if 'Word Count' in label: + self.story.setMetadata('numWords', value.string) + + if 'Category' in label: + for cat in value.string.split(', '): + self.story.addToList('category',cat) + if 'Crossover Shows' in label: + for cat in value.string.split(', '): + if "No Show" not in cat: + self.story.addToList('category',cat) + + if 'Character' in label: + 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) + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Series' in label: + if "No Series" not in value.nextSibling.string: + self.setSeries(stripHTML(value.nextSibling), value.nextSibling.nextSibling.string[2:]) + self.story.setMetadata('seriesUrl','http://'+self.host+'/'+value.nextSibling['href']) + + asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl'))) + story=asoup.find('a', href=re.compile(r'viewstory.php\?storyid='+self.story.getMetadata('storyId'))) + + a=story.findNext('font') + if 'Complete' in a.string: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + a=story.findNext(text=re.compile('Genre')).parent.nextSibling.string.split(', ') + for genre in a: + self.story.setMetadata('genre', genre) + + a=story.findNext(text=re.compile('Archived')) + self.story.setMetadata('datePublished', makeDate(stripHTML(a.parent.nextSibling), self.dateformat)) + self.story.setMetadata('dateUpdated', makeDate(stripHTML(a.parent.nextSibling), self.dateformat)) + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div') + + # bit messy since higly inconsistent + for p in soup.findAll('p', {'align' : 'center'}): + p.extract() + p = soup.findAll('p') + for x in range(0,3): + p[x].extract() + if "Chapters: " in stripHTML(p[3]): + p[3].extract() + for x in range(len(p)-2,len(p)-1): + p[x].extract() + + for p in soup.findAll('h1'): + p.extract() + for p in soup.findAll('h3'): + p.extract() + for p in soup.findAll('a'): + p.extract() + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_ncisfictionnet.py b/fanficdownloader/adapters/adapter_ncisfictionnet.py new file mode 100644 index 0000000..58abedf --- /dev/null +++ b/fanficdownloader/adapters/adapter_ncisfictionnet.py @@ -0,0 +1,210 @@ +# -*- 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 +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return NCISFictionNetAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class NCISFictionNetAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["iso-8859-1", + "Windows-1252"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL("http://"+self.getSiteDomain()\ + +"/chapters.php?stid="+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','ncisfn') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%d/%m/%Y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'www.ncisfiction.net' + + ## Changed from www.ncisfiction.com to www.ncisfiction.net Oct + ## 2012 due to the ncisfiction.com domain expiring. Still accept + ## .com domains for existing updates, etc. + + @classmethod + def getAcceptDomains(cls): + return ['www.ncisfiction.net','www.ncisfiction.com'] + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/story.php?stid=01234 http://"+cls.getSiteDomain()+"/chapters.php?stid=1234" + + def getSiteURLPattern(self): + return r'http://www\.ncisfiction\.(net|com)/(chapters|story)?.php\?stid=\d+' + + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulStoneSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title and author + a = soup.find('div', {'class' : 'main_title'}) + + aut = a.find('a') + self.story.setMetadata('authorId',aut['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+aut['href']) + self.story.setMetadata('author',aut.string) + + aut.extract() + self.story.setMetadata('title',stripHTML(a)[:len(stripHTML(a))-2]) + + # Find the chapters: + i=0 + chapters=soup.findAll('table', {'class' : 'story_table'}) + for chapter in chapters: + ch=chapter.find('a') + # just in case there's tags, like <i> in chapter titles. + self.chapterUrls.append((stripHTML(ch),'http://'+self.host+'/'+ch['href'])) + if i == 0: + self.story.setMetadata('datePublished', makeDate(stripHTML(chapter.find('td')).split('Added: ')[1], self.dateformat)) + if i == len(chapters)-1: + self.story.setMetadata('dateUpdated', makeDate(stripHTML(chapter.find('td')).split('Added: ')[1], self.dateformat)) + i=i+1 + + 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. + + info = soup.find('table', {'class' : 'story_info'}) + + # no convenient way to calculate word count as it is logged differently for stories with and without series + + labels = info.findAll('tr') + for tr in labels: + value = tr.find('td') + label = tr.find('th').string + + if 'Summary' in label: + self.setDescription(url,value) + + if 'Rating' in label: + self.story.setMetadata('rating', value.string) + + if 'Category' in label: + cats = value.findAll('a') + for cat in cats: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + chars = value.findAll('a') + 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: + self.story.addToList('genre',genre.string) + + if 'Warnings' in label: + warnings = value.findAll('a') + for warning in warnings: + self.story.addToList('warnings',warning.string) + + if 'Status' in label: + if 'not completed' in value.text: + self.story.setMetadata('status', 'In-Progress') + else: + self.story.setMetadata('status', 'Completed') + + try: + # Find Series name from series URL. + a = soup.find('div',{'class' : 'sub_header'}) + series_name = a.find('a').string + i = a.text.split('#')[1] + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl','http://'+self.host+'/'+a.find('a')['href']) + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'class' : 'story_text'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_netraptororg.py b/fanficdownloader/adapters/adapter_netraptororg.py new file mode 100644 index 0000000..6d4effb --- /dev/null +++ b/fanficdownloader/adapters/adapter_netraptororg.py @@ -0,0 +1,213 @@ +# -*- coding: utf-8 -*- + +# Copyright 2013 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return NetRaptorOrgAdapter + +class NetRaptorOrgAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/fanfiction/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','netrap') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%d/%m/%y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'netraptor.org' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/fanfiction/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/fanfiction/viewstory.php?sid=")+r"\d+$" + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + url = self.url+'&index=1' + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + pagetitle = soup.find('div',{'id':'pagetitle'}) + a = pagetitle.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = pagetitle.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/fanfiction/'+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+'/fanfiction/'+chapter['href'])) + + 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 "" + + 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')) + catstext = [cat.string for cat in cats] + for cat in catstext: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + charstext = [char.string for char in chars] + for char in charstext: + self.story.addToList('characters',char.string) + + if 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) + for genre in genres: + self.story.addToList('genre',genre.string) + + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX + 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+'/fanfiction/'+a['href'] + + # use BeautifulSoup HTML parser to make everything easier to find. + seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url)) + storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulSoup(self._fetchUrl(url)) + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_nfacommunitycom.py b/fanficdownloader/adapters/adapter_nfacommunitycom.py new file mode 100644 index 0000000..4d428e7 --- /dev/null +++ b/fanficdownloader/adapters/adapter_nfacommunitycom.py @@ -0,0 +1,290 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +# Search for XXX comments--that's where things are most likely to need changing. + +# This function is called by the downloader in all adapter_*.py files +# in this dir to register the adapter class. So it needs to be +# updated to reflect the class below it. That, plus getSiteDomain() +# take care of 'Registering'. +def getClass(): + return NfaCommunityComAdapter # XXX + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class NfaCommunityComAdapter(BaseSiteAdapter): # XXX + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + # XXX Most sites don't have the /fanfic part. Replace all to remove it usually. + 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','nfa') # 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 + + @classmethod + def getAcceptDomains(cls): + return ['www.nfacommunity.com','nfacommunity.com'] + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'nfacommunity.com' # XXX + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return "http://(www.)?"+re.escape(self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$" + + ## 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. + + # Furthermore, there's a couple sites now with more than + # one warning level for different ratings. And they're + # fussy about it. nfacommunity has two: 4 & 5. + # we'll try 5 first. + addurl = "&ageconsent=ok&warning=5" # XXX + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + # 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. + + # Since the warning text can change by warning level, let's + # look for the warning pass url. nfacommunity uses + # &warning= -- actually, so do other sites. Must be an + # eFiction book. + + # viewstory.php?sid=1882&warning=4 + # viewstory.php?sid=1654&ageconsent=ok&warning=5 + #print data + #m = re.search(r"'viewstory.php\?sid=1882(&warning=4)'",data) + m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href']) + self.story.setMetadata('author',a.string) + + # Find the chapters: + for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")): + # just in case there's tags, like <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')) + catstext = [cat.string for cat in cats] + for cat in catstext: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + charstext = [char.string for char in chars] + for char in charstext: + self.story.addToList('characters',char.string) + + ## Not all sites use Genre, but there's no harm to + ## leaving it in. Check to make sure the type_id number + ## is correct, though--it's site specific. + if 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX + genrestext = [genre.string for genre in genres] + self.genre = ', '.join(genrestext) + for genre in genrestext: + self.story.addToList('genre',genre.string) + + ## Not all sites use Warnings, but there's no harm to + ## leaving it in. Check to make sure the type_id number + ## is correct, though--it's site specific. + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX + warningstext = [warning.string for warning in warnings] + self.warning = ', '.join(warningstext) + for warning in warningstext: + self.story.addToList('warnings',warning.string) + + if 'Completed' in label: + if 'Yes' in value: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label: + # there's a stray [ at the end. + #value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + try: + # Find Series name from series URL. + a = soup.find('a', href=re.compile(r"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)) + storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+')) + i=1 + for a in storyas: + # skip 'report this' and 'TOC' links + if 'contact.php' not in a['href'] and 'index' not in a['href']: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_nhamagicalworldsus.py b/fanficdownloader/adapters/adapter_nhamagicalworldsus.py new file mode 100644 index 0000000..bd541cc --- /dev/null +++ b/fanficdownloader/adapters/adapter_nhamagicalworldsus.py @@ -0,0 +1,237 @@ +# -*- coding: utf-8 -*- + +# Copyright 2012 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return NHAMagicalWorldsUsAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class NHAMagicalWorldsUsAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','nha') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = " %d/%m/%y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'nha.magical-worlds.us' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$" + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + url = self.url + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + # 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) + asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl'))) + + try: + # in case link points somewhere other than the first chapter + a = soup.findAll('option')[1]['value'] + self.story.setMetadata('storyId',a.split('=',)[1]) + url = 'http://'+self.host+'/'+a + soup = bs.BeautifulSoup(self._fetchUrl(url)) + except: + pass + + for info in asoup.findAll('table', {'width' : '100%', 'bordercolor' : re.compile(r'#')}): + a = info.find('a') + if 'viewstory.php?sid='+self.story.getMetadata('storyId') == a['href'] or \ + ('viewstory.php?sid='+self.story.getMetadata('storyId')+'&') in a['href']: + self.story.setMetadata('title',stripHTML(a)) + break + + + # Find the chapters: + chapters=soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+'&chapter=\d+$')) + if len(chapters) == 0: + self.chapterUrls.append((self.story.getMetadata('title'),url)) + else: + for chapter in chapters: + # just in case there's tags, like <i> in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href'])) + + 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): + try: + return d.name + except: + return "" + + cats = info.findAll('a',href=re.compile('categories.php')) + for cat in cats: + self.story.addToList('category',cat.string) + + a = info.find('a', href=re.compile(r'viewuser.php')) + val = a.nextSibling + svalue = "" + while not defaultGetattr(val) == 'br': + val = val.nextSibling + val = val.nextSibling + while not defaultGetattr(val) == 'br': + svalue += unicode(val) + val = val.nextSibling + self.setDescription(url,svalue) + + #does not provide convenient way to get word count + labels = info.findAll('i') + for labelspan in labels: + value = labelspan.nextSibling + label = stripHTML(labelspan) + + if 'Rating' in label: + self.story.setMetadata('rating', value.split(' -')[0]) + + if 'Genres' in label: + genres = value.string.split(', ') + for genre in genres: + if 'None' not in genre: + self.story.addToList('genre',genre.split(' -')[0]) + + if 'Characters' in label: + chars = value.string.split(', ') + for char in chars: + if 'None' not in char: + self.story.addToList('characters',char.split(' -')[0]) + + if 'Warnings' in label: + warnings = value.string.split(', ') + for warning in warnings: + if 'None' not in warning: + self.story.addToList('warnings',warning.split(' -')[0]) + + 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(value.split(' -')[0], self.dateformat)) + + if 'Updated' in label: + # there's a stray [ at the end. + #value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(value.split(' -')[0], self.dateformat)) + + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + data = self._fetchUrl(url) + + soup = bs.BeautifulSoup(data, selfClosingTags=('br','hr','span','center')) # some chapters seem to be hanging up on those tags, so it is safer to close them + + story = soup.find('div', {"id" : "story"}) + + if None == story: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,story) diff --git a/fanficdownloader/adapters/adapter_nickandgregnet.py b/fanficdownloader/adapters/adapter_nickandgregnet.py new file mode 100644 index 0000000..cd0fd48 --- /dev/null +++ b/fanficdownloader/adapters/adapter_nickandgregnet.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- + +# Copyright 2013 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return NickAndGregNetAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class NickAndGregNetAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + # XXX Most sites don't have the /fanfic part. Replace all to remove it usually. + self._setURL('http://' + self.getSiteDomain() + '/desert_archive/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','nag') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%Y/%m/%d" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'www.nickandgreg.net' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/desert_archive/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/desert_archive/viewstory.php?sid=")+r"\d+$" + + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&i=1' + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/desert_archive/'+a['href']) + self.story.setMetadata('author',a.string) + + # Find the chapters: + chapters = soup.find('select') + for chapter in chapters.findAll('option'): + if chapter.text != 'Story Index' and chapter.text != 'Chapters': + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/desert_archive/'+chapter['value'])) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl'))) + + for div in asoup.findAll('td', {'class' : 'tblborder6'}): + a = div.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + if a != None: + break + + self.setDescription(url,div.find('br').nextSibling) + + a=div.text.split('Rating:') + if len(a) == 2: self.story.setMetadata('rating', a[1].split(' -')[0]) + + a=div.text.split('Characters:') + if len(a) == 2: + for char in a[1].split(' -')[0].split(', '): + self.story.addToList('characters',char) + + a=div.text.split('Genres:') + if len(a) == 2: + for genre in a[1].split(' -')[0].split(', '): + self.story.addToList('genre',genre) + + a=div.text.split('Warnings:') + if len(a) == 2: + for warn in a[1].split(' -')[0].split(', '): + if 'none' not in warn: + self.story.addToList('warnings',warn) + + a=div.text.split('Completed:') + if len(a) ==2: + if 'Yes' in a[1]: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + a=div.text.split('Published:') + if len(a) == 2: self.story.setMetadata('datePublished', makeDate(stripHTML(a[1].split(' -')[0]), self.dateformat)) + + a=div.text.split('Updated:') + if len(a) == 2: self.story.setMetadata('dateUpdated', makeDate(stripHTML(a[1].split(' -')[0]), self.dateformat)) + + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + # wrap a div around it. + divsoup = bs.BeautifulStoneSoup('<div class="story"></div>', + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + div = divsoup.find('div') + div.append(soup.find('table', {'class' : 'tblborder6'})) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_nocturnallightnet.py b/fanficdownloader/adapters/adapter_nocturnallightnet.py new file mode 100644 index 0000000..214e38b --- /dev/null +++ b/fanficdownloader/adapters/adapter_nocturnallightnet.py @@ -0,0 +1,177 @@ +import re +import urllib2 +import urlparse + +from .. import BeautifulSoup + +from base_adapter import BaseSiteAdapter, makeDate +from .. import exceptions + + +def getClass(): + return NocturnalLightNetAdapter + + +# yields Tag _and_ NavigableString siblings from the given tag. The +# BeautifulSoup findNextSiblings() method for some reasons only returns either +# NavigableStrings _or_ Tag objects, not both. +def _yield_next_siblings(tag): + sibling = tag.nextSibling + while sibling: + yield sibling + sibling = sibling.nextSibling + + +class NocturnalLightNetAdapter(BaseSiteAdapter): + SITE_ABBREVIATION = 'nln' + SITE_DOMAIN = 'nocturnal-light.net' + BASE_URL = 'http://' + SITE_DOMAIN + '/fanfiction/' + STORY_URL_TEMPLATE = BASE_URL + 'story/%s' + AUTHORS_URL_TEMPLATE = BASE_URL + 'authors/%s' + + DATETIME_FORMAT = '%m-%d-%y' + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + url_tokens = self.parsedUrl.path.split('/') + story_id = url_tokens[url_tokens.index('story') + 1] + + self.story.setMetadata('storyId', story_id) + self._setURL(self.STORY_URL_TEMPLATE % story_id) + self.story.setMetadata('siteabbrev', self.SITE_ABBREVIATION) + + def _customized_fetch_url(self, url, exception=None, parameters=None): + if exception: + try: + data = self._fetchUrl(url, parameters) + except urllib2.HTTPError: + raise exception(self.url) + # Just let self._fetchUrl throw the exception, don't catch and + # customize it. + else: + data = self._fetchUrl(url, parameters) + + return BeautifulSoup.BeautifulSoup(data) + + @staticmethod + def getSiteDomain(): + return NocturnalLightNetAdapter.SITE_DOMAIN + + @classmethod + def getSiteExampleURLs(cls): + return cls.STORY_URL_TEMPLATE % 1234 + + def getSiteURLPattern(self): + return re.escape(self.STORY_URL_TEMPLATE[:-2]) + r'\d+.*$' + + def extractChapterUrlsAndMetadata(self): + soup = self._customized_fetch_url(self.url) + + # Since no 404 error code we have to raise the exception ourselves. + # A title that is just 'by' indicates that there is no author name + # and no story title available. + if soup.title.string.strip() == 'by': + raise exceptions.StoryDoesNotExist(self.url) + + # "storycontent" is found in a single-chapter story + author_anchor = soup.find('div', id=lambda id: id in ('main', 'storycontent')).h1.a + self.story.setMetadata('author', author_anchor.string) + + url_tokens = author_anchor['href'].split('/') + author_id = url_tokens[url_tokens.index('authors')+1] + self.story.setMetadata('authorId', author_id) + self.story.setMetadata('authorUrl', self.AUTHORS_URL_TEMPLATE % author_id) + + chapter_anchors = soup('a', href=lambda href: href and href.startswith('/fanfiction/story/')) + for chapter_anchor in chapter_anchors: + url = urlparse.urljoin(self.BASE_URL, chapter_anchor['href']) + self.chapterUrls.append((chapter_anchor.string, url)) + + author_url = urlparse.urljoin(self.BASE_URL, author_anchor['href']) + soup = self._customized_fetch_url(author_url) + story_id = self.story.getMetadata('storyId') + for listbox in soup('div', {'class': 'listbox'}): + url_tokens = listbox.a['href'].split('/') + # Found the div containing the story's metadata; break the loop and + # parse the element + if story_id == url_tokens[url_tokens.index('story')+1]: + break + else: + raise exceptions.FailedToDownload(self.url) + + title = listbox.a.string + self.story.setMetadata('title', title) + + # No chapter anchors found in the original story URL, so the story has + # only a single chapter. + if not chapter_anchors: + self.chapterUrls.append((title, self.url)) + + for b_tag in listbox('b'): + key = b_tag.string.strip(':') + try: + value = b_tag.nextSibling.string.replace('•', '').strip(': ') + # This can happen with some fancy markup in the summary. Just + # ignore this error and set value to None, the summary parsing + # takes care of this + except AttributeError: + value = None + + if key == 'Summary': + contents = [] + keep_summary_html = self.getConfig('keep_summary_html') + + for sibling in _yield_next_siblings(b_tag): + if isinstance(sibling, BeautifulSoup.Tag): + if sibling.name == 'b' and sibling.findPreviousSibling().name == 'br': + break + + if keep_summary_html: + contents.append(self.utf8FromSoup(author_url, sibling)) + else: + contents.append(''.join(sibling(text=True))) + else: + contents.append(sibling) + + # Pop last break line tag + contents.pop() + self.story.setMetadata('description', ''.join(contents)) + + elif key == 'Category': + for sibling in b_tag.findNextSiblings(['a', 'b']): + if sibling.name == 'b': + break + + self.story.addToList('category', sibling.string) + + elif key == 'Rating': + self.story.setMetadata('rating', value) + + elif key == 'Chapters': + self.story.setMetadata('numChapters', int(value)) + + # Also parse reviews number which lies right after the chapters + # section + reviews_anchor = b_tag.findNextSibling('a') + reviews = reviews_anchor.string.split(' ')[1].strip('()') + self.story.setMetadata('reviews', reviews) + + elif key == 'Completed': + self.story.setMetadata('status', 'Completed' if value == 'Yes' else 'In-Progress') + + elif key == 'Date Added': + self.story.setMetadata('datePublished', makeDate(value, self.DATETIME_FORMAT)) + + elif key == 'Last Updated': + self.story.setMetadata('dateUpdated', makeDate(value, self.DATETIME_FORMAT)) + + elif key == 'Read': + self.story.setMetadata('readings', value.split()[0]) + + if self.story.getMetadata('rating') == 'NC-17' and not (self.is_adult or self.getConfig('is_adult')): + raise exceptions.AdultCheckRequired(self.url) + + def getChapterText(self, url): + soup = self._customized_fetch_url(url) + return self.utf8FromSoup(url, soup.find('div', id='storytext')) diff --git a/fanficdownloader/adapters/adapter_occlumencysycophanthexcom.py b/fanficdownloader/adapters/adapter_occlumencysycophanthexcom.py new file mode 100644 index 0000000..87008a5 --- /dev/null +++ b/fanficdownloader/adapters/adapter_occlumencysycophanthexcom.py @@ -0,0 +1,263 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return OcclumencySycophantHexComAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class OcclumencySycophantHexComAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','osph') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%m/%d/%Y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'occlumency.sycophanthex.com' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$" + + ## Login seems to be reasonably standard across eFiction sites. + def needToLoginCheck(self, data): + if 'This story contains adult content and/or themes.' 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['rememberme'] = '1' + params['sid'] = '' + params['intent'] = '' + params['submit'] = 'Submit' + + loginUrl = 'http://' + self.getSiteDomain() + '/user.php' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['penname'])) + + d = self._fetchUrl(loginUrl, params) + + if "Logout" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['penname'])) + raise exceptions.FailedToLogin(url,params['penname']) + return False + else: + return True + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + + # 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) + asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl'))) + + try: + # in case link points somewhere other than the first chapter + a = soup.findAll('option')[1]['value'] + self.story.setMetadata('storyId',a.split('=',)[1]) + url = 'http://'+self.host+'/'+a + soup = bs.BeautifulSoup(self._fetchUrl(url)) + except: + pass + + for info in asoup.findAll('table', {'class' : 'border'}): + a = info.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + if a != None: + self.story.setMetadata('title',stripHTML(a)) + break + + + # Find the chapters: + chapters=soup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+&i=1$')) + if len(chapters) == 0: + self.chapterUrls.append((self.story.getMetadata('title'),url)) + else: + for chapter in chapters: + # just in case there's tags, like <i> in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href'])) + + 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): + try: + return d.name + except: + return "" + + cats = info.findAll('a',href=re.compile('categories.php')) + for cat in cats: + self.story.addToList('category',cat.string) + + + a = info.find('a', href=re.compile(r'reviews.php\?sid='+self.story.getMetadata('storyId'))) + val = a.nextSibling + svalue = "" + while not defaultGetattr(val) == 'br': + val = val.nextSibling + val = val.nextSibling + while not defaultGetattr(val) == 'table': + svalue += str(val) + val = val.nextSibling + self.setDescription(url,svalue) + + # <span class="label">Rated:</span> NC-17<br /> etc + labels = info.findAll('b') + for labelspan in labels: + value = labelspan.nextSibling + label = stripHTML(labelspan) + + if 'Rating' in label: + self.story.setMetadata('rating', value) + + if 'Word Count' in label: + self.story.setMetadata('numWords', value) + + if 'Genres' in label: + genres = value.string.split(', ') + for genre in genres: + if genre != 'none': + self.story.addToList('genre',genre) + + if 'Characters' in label: + chars = value.string.split(', ') + for char in chars: + if char != 'none': + self.story.addToList('characters',char) + + if 'Warnings' in label: + warnings = value.string.split(', ') + for warning in warnings: + if warning != ' none': + self.story.addToList('warnings',warning) + + if 'Completed' in label: + if 'Yes' in value: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label: + # there's a stray [ at the end. + #value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + data = self._fetchUrl(url) + data = data.replace('<div align="left"', '<div align="left">') + + soup = bs.BeautifulSoup(data, selfClosingTags=('br','hr')) + + story = soup.find('div', {"align" : "left"}) + + if None == story: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,story) diff --git a/fanficdownloader/adapters/adapter_onedirectionfanfictioncom.py b/fanficdownloader/adapters/adapter_onedirectionfanfictioncom.py new file mode 100644 index 0000000..d4ae909 --- /dev/null +++ b/fanficdownloader/adapters/adapter_onedirectionfanfictioncom.py @@ -0,0 +1,270 @@ +# -*- coding: utf-8 -*- + +# Copyright 2012 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return OneDirectionFanfictionComAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class OneDirectionFanfictionComAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','odf') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%m/%d/%y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'onedirectionfanfiction.com' + + @classmethod + def getAcceptDomains(cls): + return ['www.onedirectionfanfiction.com','onedirectionfanfiction.com'] + + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://")+"(www\.)?"+re.escape(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' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['penname'])) + + d = self._fetchUrl(loginUrl, params) + + if "Member Account" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['penname'])) + raise exceptions.FailedToLogin(url,params['penname']) + return False + else: + return True + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + # Weirdly, different sites use different warning numbers. + # If the title search below fails, there's a good chance + # you need a different number. print data at that point + # and see what the 'click here to continue' url says. + addurl = "&ageconsent=ok&warning=4" + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url) + + # 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 "Age Consent Required" in data: + 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) + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href']) + self.story.setMetadata('author',a.string) + + # Find the chapters: + for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")): + # just in case there's tags, like <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 value and not defaultGetattr(value,'class') == 'label': + svalue += str(value) + value = value.nextSibling + self.setDescription(url,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 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=6')) + for warning in warnings: + self.story.addToList('warnings',warning.string) + + if 'Completed' in label: + if 'Yes' in value: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label: + # there's a stray [ at the end. + #value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + try: + # Find Series name from series URL. + a = soup.find('a', href=re.compile(r"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)) + storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_phoenixsongnet.py b/fanficdownloader/adapters/adapter_phoenixsongnet.py new file mode 100644 index 0000000..d8918d0 --- /dev/null +++ b/fanficdownloader/adapters/adapter_phoenixsongnet.py @@ -0,0 +1,240 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2, urllib, cookielib + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return PhoenixSongNetAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class PhoenixSongNetAdapter(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.path.split('/',)[3]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/fanfiction/story/' +self.story.getMetadata('storyId')+'/') + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','phs') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%B %d %Y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'www.phoenixsong.net' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/fanfiction/story/1234/" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/fanfiction/story/")+r"\d+/?$" + + ## Login seems to be reasonably standard across eFiction sites. + def needToLoginCheck(self, data): + if 'Please login to continue.' in data: + return True + else: + return False + + def performLogin(self, url): + params = {} + + if self.password: + params['txtusername'] = self.username + params['txtpassword'] = self.password + else: + params['txtusername'] = self.getConfig("username") + params['txtpassword'] = self.getConfig("password") + #params['remember'] = '1' + params['login'] = 'Login' + + loginUrl = 'http://' + self.getSiteDomain() + '/users/processlogin.php' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['txtusername'])) + d = self._fetchUrl(loginUrl, params) + + if 'Please login to continue.' in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['txtusername'])) + raise exceptions.FailedToLogin(url,params['txtusername']) + return False + else: + return True + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url + logger.debug("URL: "+url) + + try: + if self.getConfig('force_login'): + self.performLogin(url) + 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) + + # 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 + b = soup.find('div', {'id' : 'nav25'}) + a = b.find('a', href=re.compile(r'fanfiction/story/'+self.story.getMetadata('storyId')+"/$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. /fanfiction/stories.php?psid=125 + a = b.find('a', href=re.compile(r"/fanfiction/stories.php\?psid=\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: + chapters = soup.find('select') + if chapters == None: + self.chapterUrls.append((self.story.getMetadata('title'),url)) + for b in soup.findAll('b'): + if b.text == "Updated": + date = b.nextSibling.string.split(': ')[1].split(',') + self.story.setMetadata('datePublished', makeDate(date[0]+date[1], self.dateformat)) + self.story.setMetadata('dateUpdated', makeDate(date[0]+date[1], self.dateformat)) + else: + i = 0 + chapters = chapters.findAll('option') + for chapter in chapters: + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+chapter['value'])) + if i == 0: + self.story.setMetadata('storyId',chapter['value'].split('/')[3]) + head = bs.BeautifulSoup(self._fetchUrl('http://'+self.host+chapter['value'])).findAll('b') + for b in head: + if b.text == "Updated": + date = b.nextSibling.string.split(': ')[1].split(',') + self.story.setMetadata('datePublished', makeDate(date[0]+date[1], self.dateformat)) + + if i == (len(chapters)-1): + head = bs.BeautifulSoup(self._fetchUrl('http://'+self.host+chapter['value'])).findAll('b') + for b in head: + if b.text == "Updated": + date = b.nextSibling.string.split(': ')[1].split(',') + self.story.setMetadata('dateUpdated', makeDate(date[0]+date[1], self.dateformat)) + i = i+1 + + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl'))) + + info = asoup.find('a', href=re.compile(r'fanfiction/story/'+self.story.getMetadata('storyId')+"/$")) + while info != None: + info = info.findNext('div') + b = info.find('b') + val = b.nextSibling + + if 'Rating' in b.string: + self.story.setMetadata('rating', val.string.split(': ')[1]) + + if 'Words' in b.string: + self.story.setMetadata('numWords', val.string.split(': ')[1]) + + if 'Setting' in b.string: + self.story.addToList('category', val.string.split(': ')[1]) + + if 'Status' in b.string: + if 'Completed' in val: + val = 'Completed' + else: + val = 'In-Progress' + self.story.setMetadata('status', val) + + if 'Summary' in b.string: + b.extract() + info.find('br').extract() + self.setDescription(url,info) + break + + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + chapter=bs.BeautifulSoup('<div class="story"></div>') + for p in soup.findAll('p'): + if "This is for problems with the formatting or the layout of the chapter." in stripHTML(p): + break + chapter.append(p) + + for a in chapter.findAll('div'): + a.extract() + for a in chapter.findAll('table'): + a.extract() + for a in chapter.findAll('script'): + a.extract() + for a in chapter.findAll('form'): + a.extract() + for a in chapter.findAll('textarea'): + a.extract() + + + if None == chapter: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,chapter) diff --git a/fanficdownloader/adapters/adapter_pommedesangcom.py b/fanficdownloader/adapters/adapter_pommedesangcom.py new file mode 100644 index 0000000..4d553a0 --- /dev/null +++ b/fanficdownloader/adapters/adapter_pommedesangcom.py @@ -0,0 +1,301 @@ +# -*- coding: utf-8 -*- + +# Copyright 2013 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return PommeDeSangComAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class PommeDeSangComAdapter(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]) + + + # pommedesang.com has two 'sections', shown in URL as + # 'efiction' and 'sds' that change how things should be + # handled. + # http://pommedesang.com/efiction/viewstory.php?sid=1234 + # http://pommedesang.com/sds/viewstory.php?sid=1234 + self.section=self.parsedUrl.path.split('/',)[1] + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/'+self.section+'/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','pmds') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + if 'efiction' in self.section: + self.dateformat = "%b %d, %Y" + else: + self.dateformat = "%m/%d/%y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'pommedesang.com' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/efiction/viewstory.php?sid=1234 http://"+cls.getSiteDomain()+"/sds/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return r"http://"+self.getSiteDomain()+"/(efiction|sds)?/viewstory.php\?sid=\d+$" + + ## Login seems to be reasonably standard across eFiction sites. + def needToLoginCheck(self, data): + if 'Registered Users Only' in data \ + or 'There is no such account on our website' in data \ + or "That password doesn't match the one in our database" in data: + return True + else: + return False + + def performLogin(self, url): + params = {} + + if self.password: + params['penname'] = self.username + params['password'] = self.password + else: + params['penname'] = self.getConfig("username") + params['password'] = self.getConfig("password") + params['cookiecheck'] = '1' + params['submit'] = 'Submit' + + loginUrl = 'http://' + self.getSiteDomain() + '/'+self.section+'/user.php?action=login' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['penname'])) + + d = self._fetchUrl(loginUrl, params) + + if "Member Account" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['penname'])) + raise exceptions.FailedToLogin(url,params['penname']) + return False + else: + return True + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + # Weirdly, different sites use different warning numbers. + # If the title search below fails, there's a good chance + # you need a different number. print data at that point + # and see what the 'click here to continue' url says. + addurl = "&ageconsent=ok&warning=5" + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url) + + m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile('viewstory.php\?sid=\d+')) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href']) + self.story.setMetadata('author',a.string) + + # Find the chapters: + for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")): + # just in case there's tags, like <i> in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+self.section+'/'+chapter['href']+addurl)) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + # eFiction sites don't help us out a lot with their meta data + # formating, so it's a little ugly. + + # utility method + def defaultGetattr(d,k): + try: + return d[k] + except: + return "" + +# summary, rated, word count, categories, characters, genre, warnings, completed, published, updated, seires + + # <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 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) # XXX + for genre in genres: + self.story.addToList('genre',genre.string) + + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX + for warning in warnings: + self.story.addToList('warnings',warning.string) + + if 'Completed' in label: + if 'Yes' in value: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label: + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + try: + # Find Series name from series URL. + a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+")) + series_name = a.string + series_url = 'http://'+self.host+'/'+self.section+'/'+a['href'] + + # use BeautifulSoup HTML parser to make everything easier to find. + seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url)) + # can't use ^viewstory...$ in case of higher rated stories with javascript href. + storyas = seriessoup.findAll('a', href=re.compile('viewstory.php\?sid=\d+')) + i=1 + for a in storyas: + # skip 'report this' and 'TOC' links + if 'contact.php' not in a['href'] and 'index' not in a['href']: + if ('viewstory.php?sid='+self.story.getMetadata('storyId')) in a['href']: + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_ponyfictionarchivenet.py b/fanficdownloader/adapters/adapter_ponyfictionarchivenet.py new file mode 100644 index 0000000..eceb332 --- /dev/null +++ b/fanficdownloader/adapters/adapter_ponyfictionarchivenet.py @@ -0,0 +1,249 @@ +# -*- coding: utf-8 -*- + +# Copyright 2012 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return PonyFictionArchiveNetAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class PonyFictionArchiveNetAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + # normalized story URL. + if "explicit" in self.parsedUrl.netloc: + self._setURL('http://explicit.' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId')) + self.dateformat = "%d/%b/%y" + else: + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId')) + self.dateformat = "%d %b %Y" + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','pffa') + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'ponyfictionarchive.net' + + @classmethod + def getAcceptDomains(cls): + return ['www.ponyfictionarchive.net','ponyfictionarchive.net','explicit.ponyfictionarchive.net'] + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234 http://explicit."+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://")+"(www\.|explicit\.)?"+re.escape(self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$" + + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + # Weirdly, different sites use different warning numbers. + # If the title search below fails, there's a good chance + # you need a different number. print data at that point + # and see what the 'click here to continue' url says. + addurl = "&warning=9" + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + + m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href']) + self.story.setMetadata('author',a.string) + + # Find the chapters: + for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")): + # just in case there's tags, like <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 "" + + genres = soup.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) + for genre in genres: + self.story.addToList('genre',genre.string) + + warnings = soup.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=3')) + for warning in warnings: + self.story.addToList('warnings',warning.string) + + status = soup.find('a',href=re.compile(r'browse.php\?type=class&type_id=2')) + self.story.setMetadata('status',status.string) + + section = soup.findAll('span', {'class' : 'General'})[1] + + self.story.setMetadata('rating', section.previousSibling.previousSibling.string) + + value = section.nextSibling + svalue = "" + while not defaultGetattr(value,'class') == 'label': + svalue += str(value) + value = value.nextSibling + self.setDescription(url,svalue) + + # <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 'Word count' in label: + self.story.setMetadata('numWords', value) + + 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 '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) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulSoup(self._fetchUrl(url)) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_portkeyorg.py b/fanficdownloader/adapters/adapter_portkeyorg.py new file mode 100644 index 0000000..0a8e417 --- /dev/null +++ b/fanficdownloader/adapters/adapter_portkeyorg.py @@ -0,0 +1,282 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 +import cookielib as cl + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +# Search for XXX comments--that's where things are most likely to need changing. + +# This function is called by the downloader in all adapter_*.py files +# in this dir to register the adapter class. So it needs to be +# updated to reflect the class below it. That, plus getSiteDomain() +# take care of 'Registering'. +def getClass(): + return PortkeyOrgAdapter # XXX + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class PortkeyOrgAdapter(BaseSiteAdapter): # XXX + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/story/'+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','prtky') # 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 + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'fanfiction.portkey.org' # XXX + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/story/1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/story/")+r"\d+(/\d+)?$" + + def use_pagecache(self): + ''' + adapters that will work with the page cache need to implement + this and change it to True. + ''' + return True + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + url = self.url + logger.debug("URL: "+url) + + # portkey screws around with using a different URL to set the + # cookie and it's a pain. So... cheat! + if self.is_adult or self.getConfig("is_adult"): + cookie = cl.Cookie(version=0, name='verify17', value='1', + port=None, port_specified=False, + domain=self.getSiteDomain(), domain_specified=False, domain_initial_dot=False, + path='/', path_specified=True, + secure=False, + expires=time.time()+10000, + discard=False, + comment=None, + comment_url=None, + rest={'HttpOnly': None}, + rfc2109=False) + self.cookiejar.set_cookie(cookie) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if "You must be over 18 years of age to view it" in data: # XXX + raise exceptions.AdultCheckRequired(self.url) + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + #print data + + # Now go hunting for all the meta data and the chapter list. + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"/profile/\d+")) + #print("======a:%s"%a) + self.story.setMetadata('authorId',a['href'].split('/')[-1]) + self.story.setMetadata('authorUrl','http://'+self.host+a['href']) + self.story.setMetadata('author',a.string) + + ## Going to get the rest from the author page. + authsoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl'))) + + ## Title + titlea = authsoup.find('a', href=re.compile(r'/story/'+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(titlea)) + metablock = titlea.parent + + # Find the chapters: + for chapter in soup.find('select',{'name':'select5'}).findAll('option', {'value':re.compile(r'/story/'+self.story.getMetadata('storyId')+"/\d+$")}): + # just in case there's tags, like <i> in chapter titles. + chtitle = stripHTML(chapter) + if not chtitle: + chtitle = "(Untitled Chapter)" + self.chapterUrls.append((chtitle,'http://'+self.host+chapter['value'])) + + if len(self.chapterUrls) == 0: + self.chapterUrls.append((stripHTML(self.story.getMetadata('title')),url)) + + 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="dark-small-bold">Contents:</SPAN> <SPAN class="small-grey">NC17 </SPAN> + # <SPAN class="dark-small-bold">Published: </SPAN><SPAN class="small-grey">12/11/07</SPAN> + # <SPAN class="dark-small-bold"><BR> + # Description:</SPAN> <SPAN class="small-black">A special book helps Harry tap into the power the Dark Lord knows not. Of course it’s a book on sex magic and rituals… but Harry’s not complaining. Spurned on by the ghost of a pervert founder, Harry leads his friends in the hunt for Voldemort’s Horcruxes. + # EROTIC COMEDY! Loads of crude humor and sexual situations! + # </SPAN> + labels = metablock.findAll('span',{'class':'dark-small-bold'}) + for labelspan in labels: + value = labelspan.findNext('span').string + label = stripHTML(labelspan) +# print("\nlabel:%s\nlabel:%s\nvalue:%s\n"%(labelspan,label,value)) + + if 'Description' in label: + self.setDescription(url,value) + + if 'Contents' in label: + self.story.setMetadata('rating', value) + + if 'Words' in label: + self.story.setMetadata('numWords', value) + + # if 'Categories' in label: + # cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories')) + # catstext = [cat.string for cat in cats] + # for cat in catstext: + # self.story.addToList('category',cat.string) + + # if 'Characters' in label: + # chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + # charstext = [char.string for char in chars] + # for char in charstext: + # self.story.addToList('characters',char.string) + + if 'Genre' in label: + # genre is typo'ed on the site--it falls between the + # dark-small-bold label and dark-small-bold content + # spans. + svalue = "" + value = labelspan.nextSibling + while not defaultGetattr(value,'class') == 'dark-small-bold': + svalue += str(value) + value = value.nextSibling + + for genre in svalue.split("/"): + genre = genre.strip() + if genre != 'None': + self.story.addToList('genre',genre) + + ## Not all sites use Warnings, but there's no harm to + ## leaving it in. Check to make sure the type_id number + ## is correct, though--it's site specific. + # if 'Warnings' in label: + # warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX + # warningstext = [warning.string for warning in warnings] + # self.warning = ', '.join(warningstext) + # for warning in warningstext: + # self.story.addToList('warnings',warning.string) + + if 'Status' in label: + if 'Completed' 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 = metablock.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)) + # storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + # i=1 + # for a in storyas: + # 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): + + logger.debug('Getting chapter text from: %s' % url) + data = self._fetchUrl(url) + + data = data.replace("HTML>","div>") + + soup = bs.BeautifulSoup(data) + + #print("soup:%s"%soup) + tag = soup.find('td', {'class' : 'story'}) + if tag == None and "<center><b>Chapter does not exist!</b></center>" in data: + print("Chapter is missing at: %s"%url) + return self.utf8FromSoup(url,bs.BeautifulStoneSoup("<div><p><center><b>Chapter does not exist!</b></center></p><p>Chapter is missing at: <a href='%s'>%s</a></p></div>"%(url,url))) + tag.name='div' # force to be a div to avoid problems with nook. + + centers = tag.findAll('center') + # first two and last two center tags are some script, 'report + # story', 'report story' and an ad. + centers[0].extract() + centers[1].extract() + centers[-1].extract() + centers[-2].extract() + + if None == tag: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,tag) diff --git a/fanficdownloader/adapters/adapter_potionsandsnitchesnet.py b/fanficdownloader/adapters/adapter_potionsandsnitchesnet.py new file mode 100644 index 0000000..da9e845 --- /dev/null +++ b/fanficdownloader/adapters/adapter_potionsandsnitchesnet.py @@ -0,0 +1,211 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +class PotionsAndSnitchesNetSiteAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + self.story.setMetadata('siteabbrev','pns') + 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. + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/fanfiction/viewstory.php?sid='+self.story.getMetadata('storyId')) + + + @staticmethod + def getSiteDomain(): + return 'www.potionsandsnitches.net' + + @classmethod + def getAcceptDomains(cls): + return ['www.potionsandsnitches.net','potionsandsnitches.net'] + + @classmethod + def getSiteExampleURLs(cls): + return "http://www.potionsandsnitches.net/fanfiction/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://")+r"(www\.)?"+re.escape("potionsandsnitches.net/fanfiction/viewstory.php?sid=")+r"\d+$" + + def extractChapterUrlsAndMetadata(self): + + url = self.url+'&index=1' + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/fanfiction/'+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+'/fanfiction/'+chapter['href'])) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + ## <meta name='description' content='<p>Description</p> ...' > + ## Summary, strangely, is in the content attr of a <meta name='description'> tag + ## which is escaped HTML. Unfortunately, we can't use it because they don't + ## escape (') chars in the desc, breakin the tag. + #meta_desc = soup.find('meta',{'name':'description'}) + #metasoup = bs.BeautifulStoneSoup(meta_desc['content']) + #self.story.setMetadata('description',stripHTML(metasoup)) + + 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 div class='listbox' + svalue = "" + while not defaultGetattr(value,'class') == 'listbox': + 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')) + catstext = [cat.string for cat in cats] + for cat in catstext: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + 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 "Snape and Harry (required)" in char: + self.story.addToList('characters',"Snape") + self.story.addToList('characters',"Harry") + else: + self.story.addToList('characters',char.string) + + if 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class')) + genrestext = [genre.string for genre in genres] + self.genre = ', '.join(genrestext) + for genre in genrestext: + self.story.addToList('genre',genre.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: + # limit date values, there's some extra chars. + self.story.setMetadata('datePublished', makeDate(stripHTML(value[:12]), "%d %b %Y")) + + if 'Updated' in label: + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value[:12]), "%d %b %Y")) + + 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+'/fanfiction/'+a['href'] + + # use BeautifulSoup HTML parser to make everything easier to find. + seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url)) + storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) + +def getClass(): + return PotionsAndSnitchesNetSiteAdapter + diff --git a/fanficdownloader/adapters/adapter_potterficscom.py b/fanficdownloader/adapters/adapter_potterficscom.py new file mode 100644 index 0000000..c837927 --- /dev/null +++ b/fanficdownloader/adapters/adapter_potterficscom.py @@ -0,0 +1,281 @@ +# -*- coding: utf-8 -*- + +# Copyright 2013 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import datetime +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter + +# This function is called by the downloader in all adapter_*.py files +# in this dir to register the adapter class. So it needs to be +# updated to reflect the class below it. That, plus getSiteDomain() +# take care of 'Registering'. +def getClass(): + return PotterFicsComAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class PotterFicsComAdapter(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 correct + m = re.match(self.getSiteURLPattern(),url) + if m: + self.story.setMetadata('storyId',m.group('id')) + + # normalized story URL. gets rid of chapter if there, left with chapter index URL + nurl = "http://"+self.getSiteDomain()+"/historias/"+self.story.getMetadata('storyId') + self._setURL(nurl) + else: + raise exceptions.InvalidStoryURL(url, + self.getSiteDomain(), + self.getSiteExampleURLs()) + + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','potficscom') + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'www.potterfics.com' + + @classmethod + def getSiteExampleURLs(cls): + return "http://www.potterfics.com/historias/12345 http://www.potterfics.com/historias/12345/capitulo-1 " + + def getSiteURLPattern(self): + #http://www.potterfics.com/historias/127583 + #http://www.potterfics.com/historias/127583/capitulo-1 + #http://www.potterfics.com/historias/127583/capitulo-4 + #http://www.potterfics.com/historias/92810 -> Complete story + #http://www.potterfics.com/historias/111194 -> Complete, single chap + p = re.escape("http://"+self.getSiteDomain()+"/historias/")+\ + r"(?P<id>\d+)(/capitulo-(?P<ch>\d+))?/?$" + return p + + def needToLoginCheck(self, data): + # partials used to avoid having to figure out what was wrong + # with included utf8 higher chars. + if 'Para ver esta historia, por favor inicia tu sesi' in data \ + or '<script>alert("El nombre de usuario o contrase' in data: + return True + else: + return False + + def performLogin(self,url): + params = {} + + if self.password: + params['login_usuario'] = self.username + params['login_password'] = self.password + else: + params['login_usuario'] = self.getConfig("username") + params['login_password'] = self.getConfig("password") + params['login_ck'] = '1' + + loginUrl = 'http://www.potterfics.com/secciones/usuarios/login.php' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['login_usuario'])) + d = self._postUrl(loginUrl,params) + + #print("d:%s"%d) + if '<script>alert("El nombre de usuario o contrase' in d: + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['login_usuario'])) + raise exceptions.FailedToLogin(url,params['login_usuario']) + return False + else: + return True + + def extractChapterUrlsAndMetadata(self): + + #this converts '/historias/12345' to 'http://www.potterfics.com/historias/12345' + def makeAbsoluteURL(url): + if url[0] == '/': + url = 'http://'+self.getSiteDomain()+url + return url + + #use this to get month numbers from Spanish months + SpanishMonths = { + 'enero' : '01', + 'febrero' : '02', + 'marzo' : '03', + 'abril' : '04', + 'mayo' : '05', + 'junio' : '06', + 'julio' : '07', + 'agosto' : '08', + 'septiembre' : '09', + 'octubre' : '10', + 'noviembre' : '11', + 'diciembre' : '12' + } + + url = self.url + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + #print data + + #deal with adult content login + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url,usecache=False) + + #set constant meta for this site: + #Set Language = Spanish + self.story.setMetadata('language', 'Spanish') + #Set Category = Harry Potter + # This is better done in plugin-defaults.ini and defaults.ini + # by adding a section for this site with the line: + # extracategories:Harry Potter + #self.story.addToList('category','Harry Potter') + + #get the rest of the meta + # use BeautifulSoup HTML parser to make everything easier to find. + #self closing br and img present! + soup = bs.BeautifulSoup(data,selfClosingTags=('br','img')) + + #we want the second table directly under the body, contains all the metadata + table = soup.html.body.findAll('table', recursive=False)[1] + #within that, we want the second row, first cell + cell = table.tr.findNextSibling('tr').td + + #find first metadata block--isn't first if logged in + mb = cell.div.findNextSibling('div',{'align':'left'}) + #Get meta... + self.story.setMetadata('title', stripHTML(mb.b)) + #strip out brackets on rating + self.story.setMetadata('rating', mb.span.string[1:-1]) + #Completion status is denoted by the presence of this image: + if mb.find('img',title="Historia terminada"): + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + #find next metadata block + #author details + mb = mb.findNextSibling('div') + self.story.setMetadata('author', mb.b.a.string.strip()) + self.story.setMetadata('authorUrl', makeAbsoluteURL(mb.b.a['href'])) + self.story.setMetadata('authorId', self.story.getMetadata('authorUrl').split('/')[4]) + #dates and times + mb = mb.find('span') + #posted/published = Escrita + date = mb.find(text=re.compile('Escrita el ')).strip().split() + year = int(date[7][:-1]) # need to remove the last char from year, it is a comma + month = int(SpanishMonths[date[5].lower()]) + day = int(date[3]) + time = date[8].split(':') + hour = int(time[0]) + minute = int(time[1]) + self.story.setMetadata('datePublished', datetime.datetime(year, month, day, hour, minute)) + #updated = Actualizada + date = mb.find(text=re.compile('Actualizada el ')).strip().split() + year = int(date[7][:-1]) # need to remove the last char from year, it is a comma + month = int(SpanishMonths[date[5].lower()]) + day = int(date[3]) + time = date[8].split(':') + hour = int(time[0]) + minute = int(time[1]) + self.story.setMetadata('dateUpdated', datetime.datetime(year, month, day, hour, minute)) + + mb = mb.span.findNextSibling('span').findNextSibling('span') + wc = mb.find(text=re.compile(' palabras en total')).strip() + self.story.setMetadata('numWords', wc.split()[0]) + + #then we come to categories and genres. Oh dear. On this site, categories hold everything from genre, to ships, to crossovers. + #To make things worse, there is also another genre field, which often holds similar/duplicate info. Links to genre pages do not work + #though, so perhaps those will be phased out? + #for now, put them all into the genre list + links = mb.findAll('a',href=re.compile('/(categorias|generos)/\d+')) + genlist = [i.string.strip() for i in links] + self.story.extendList('genre',genlist) + + #get the chapter urls + #we can go back to the table cell we found before + #get its last element and work backwards to find the last ordered list on the page + list = cell.contents[len(cell)-1].findPrevious('ol') + chapters = [] + revs = 0 + chnum = 0 + for li in list: + chnum += 1 + chTitle = str(chnum) + '. ' + li.a.b.string.strip() + chURL = makeAbsoluteURL(li.a['href']) + chapters.append((chTitle,chURL)) + #Get reviews, add to total + revs += int(li.div.a.string.split()[0]) + + self.chapterUrls.extend(chapters) + self.story.setMetadata('numChapters', len(chapters)) + self.story.setMetadata('reviews', revs) + + #Now for the description... this may be tricky... + #if it is there (doesn't have to be), it will be before the chapter list, + #separated by a horizontal rule, and after the google ad bar + + #get list's parent div + mb = list.parent + #get the div before that, will either be the description, or the google ad bar + mb = mb.findPreviousSibling('div') + if 'google_ad_client' in str(mb): + #couldn't find description, leaving it blank + pass + else: + self.setDescription(url,mb) + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulSoup(self._fetchUrl(url), + selfClosingTags=('br','hr','img')) + + div = soup.find('div', {'id' : 'cuerpoHistoria'}) + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_potterheadsanonymouscom.py b/fanficdownloader/adapters/adapter_potterheadsanonymouscom.py new file mode 100644 index 0000000..c8f061c --- /dev/null +++ b/fanficdownloader/adapters/adapter_potterheadsanonymouscom.py @@ -0,0 +1,299 @@ +# -*- coding: utf-8 -*- + +# Copyright 2013 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return PotterHeadsAnonymousComAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class PotterHeadsAnonymousComAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','pha') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%d %b %Y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'fanfic.potterheadsanonymous.com' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$" + + ## 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' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['penname'])) + + d = self._fetchUrl(loginUrl, params) + + if "Member Account" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['penname'])) + raise exceptions.FailedToLogin(url,params['penname']) + return False + else: + return True + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + # Weirdly, different sites use different warning numbers. + # If the title search below fails, there's a good chance + # you need a different number. print data at that point + # and see what the 'click here to continue' url says. + addurl = "&ageconsent=ok&warning=4" + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url) + + # Since the warning text can change by warning level, let's + # look for the warning pass url. ksarchive uses + # &warning= -- actually, so do other sites. Must be an + # eFiction book. + + # viewstory.php?sid=1882&warning=4 + # viewstory.php?sid=1654&ageconsent=ok&warning=5 + #print data + #m = re.search(r"'viewstory.php\?sid=1882(&warning=4)'",data) + m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + pagetitle = soup.find('div',{'id':'pagetitle'}) + + ## Title + a = pagetitle.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = pagetitle.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href']) + self.story.setMetadata('author',a.string) + + # Find the chapters: + for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")): + # just in case there's tags, like <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 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) + for genre in genres: + self.story.addToList('genre',genre.string) + + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=3')) + for warning in warnings: + self.story.addToList('warnings',warning.string) + + if 'Completed' in label: + if 'Yes' in value: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label: + # there's a stray [ at the end. + #value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + try: + # Find Series name from series URL. + a = soup.find('a', href=re.compile(r"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)) + storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_pretendercentrecom.py b/fanficdownloader/adapters/adapter_pretendercentrecom.py new file mode 100644 index 0000000..5e9590a --- /dev/null +++ b/fanficdownloader/adapters/adapter_pretendercentrecom.py @@ -0,0 +1,254 @@ +# -*- coding: utf-8 -*- + +# Copyright 2012 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return PretenderCenterComAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class PretenderCenterComAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/missingpieces/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','ptdc') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%d/%m/%y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'pretendercentre.com' + + @classmethod + def getAcceptDomains(cls): + return ['www.pretendercentre.com','pretendercentre.com'] + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/missingpieces/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://")+"(www\.)?"+re.escape(self.getSiteDomain()+"/missingpieces/viewstory.php?sid=")+r"\d+$" + + ## 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 + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/missingpieces/'+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+'/missingpieces/'+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 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) # XXX + for genre in genres: + self.story.addToList('genre',genre.string) + + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX + for warning in warnings: + self.story.addToList('warnings',warning.string) + + if 'Completed' in label: + if 'Yes' in 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+'/missingpieces/'+a['href'] + + # use BeautifulSoup HTML parser to make everything easier to find. + seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url)) + # can't use ^viewstory...$ in case of higher rated stories with javascript href. + storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+')) + i=1 + for a in storyas: + # skip 'report this' and 'TOC' links + if 'contact.php' not in a['href'] and 'index' not in a['href']: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.findAll('div', {'id' : 'story'})[1] + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_psychficcom.py b/fanficdownloader/adapters/adapter_psychficcom.py new file mode 100644 index 0000000..fa53815 --- /dev/null +++ b/fanficdownloader/adapters/adapter_psychficcom.py @@ -0,0 +1,249 @@ +# -*- coding: utf-8 -*- + +# Copyright 2013 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return PsychFicComAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class PsychFicComAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','psyf') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%B %d, %Y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'www.psychfic.com' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$" + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + 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 + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href']) + self.story.setMetadata('author',a.string) + + # Find the chapters: + for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")): + # just in case there's tags, like <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.text + + 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 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) + for genre in genres: + self.story.addToList('genre',genre.string) + + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=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) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_qafficcom.py b/fanficdownloader/adapters/adapter_qafficcom.py new file mode 100644 index 0000000..c60c330 --- /dev/null +++ b/fanficdownloader/adapters/adapter_qafficcom.py @@ -0,0 +1,265 @@ +# -*- coding: utf-8 -*- + +# Copyright 2012 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return QafFicComAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class QafFicComAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/atp/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','atp') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%m/%d/%y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'www.qaf-fic.com' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/atp/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/atp/viewstory.php?sid=")+r"\d+$" + + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + # Weirdly, different sites use different warning numbers. + # If the title search below fails, there's a good chance + # you need a different number. print data at that point + # and see what the 'click here to continue' url says. + addurl = "&warning=NC-17" + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\s+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title and author + a = soup.find('div', {'id' : 'pagetitle'}) + + aut = a.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',aut['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/atp/'+aut['href']) + self.story.setMetadata('author',aut.string) + aut.extract() + + self.story.setMetadata('title',stripHTML(a)[:(len(a.string)-3)]) + + # Find the chapters: + chapters=soup.find('select') + if chapters != None: + for chapter in chapters.findAll('option'): + # just in case there's tags, like <i> in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/atp/viewstory.php?sid='+self.story.getMetadata('storyId')+'&chapter='+chapter['value'])) + else: + self.chapterUrls.append((self.story.getMetadata('title'),url)) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl'))) + for list in asoup.findAll('div', {'class' : re.compile('listbox\s+')}): + a = list.find('a') + if ('viewstory.php?sid='+self.story.getMetadata('storyId')) in a['href']: + break + + # eFiction sites don't help us out a lot with their meta data + # formating, so it's a little ugly. + + # utility method + def defaultGetattr(d,k): + try: + return d[k] + except: + return "" + + # <span class="label">Rated:</span> NC-17<br /> etc + labels = list.findAll('span', {'class' : 'classification'}) + for labelspan in labels: + label = labelspan.string + value = labelspan.nextSibling + + if 'Summary' in label: + ## Everything until the next span class='label' + svalue = "" + while not defaultGetattr(value,'class') == 'classification' and value != None: + if "Featured Stories" not in value: + svalue += str(value) + value = value.nextSibling + self.setDescription(url,svalue) + #self.story.setMetadata('description',stripHTML(svalue)) + + if 'Rated' in label: + self.story.setMetadata('rating', value[:len(value)-2]) + + if 'Word count' in label: + self.story.setMetadata('numWords', value) + + if 'Categories' in label: + cats = labelspan.parent.findAll('a',href=re.compile(r'categories.php\?catid=\d+')) + for cat in cats: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + for char in value.string.split(', '): + if not 'None' in char: + self.story.addToList('characters',char) + + if 'Genre' in label: + for genre in value.string.split(', '): + if not 'None' in genre: + self.story.addToList('genre',genre) + + if 'Warnings' in label: + for warning in value.string.split(', '): + if not 'None' in warning: + self.story.addToList('warnings',warning) + + if 'Completed' in label: + if 'Yes' in value: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value.split(' ::')[0]), self.dateformat)) + + if 'Updated' in label: + # there's a stray [ at the end. + #value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + try: + if list.find('a', href=re.compile(r"series.php")) != None: + for series in asoup.findAll('a', href=re.compile(r"series.php\?seriesid=\d+")): + # Find Series name from series URL. + series_url = 'http://'+self.host+'/atp/'+series['href'] + # use BeautifulSoup HTML parser to make everything easier to find. + seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url)) + storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + name=seriessoup.find('div', {'id' : 'pagetitle'}) + name.find('a').extract() + self.setSeries(name.text.split(' by[')[0], i) + self.story.setMetadata('seriesUrl',series_url) + i=0 + break + i+=1 + if i == 0: + break + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_restrictedsectionorg.py b/fanficdownloader/adapters/adapter_restrictedsectionorg.py new file mode 100644 index 0000000..f03b4e3 --- /dev/null +++ b/fanficdownloader/adapters/adapter_restrictedsectionorg.py @@ -0,0 +1,265 @@ +# -*- coding: utf-8 -*- + +# Copyright 2013 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 +import cookielib as cl +from datetime import datetime + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return RestrictedSectionOrgSiteAdapter + +class RestrictedSectionOrgSiteAdapter(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 = "" + + # normalized story URL. + # get story/file and storyId from url--url validation guarantees query correct + m = re.match(self.getSiteURLPattern(),url) + if m: + self.story.setMetadata('storyId',m.group('id')) + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/' + m.group('filestory') + '.php?' + m.group('filestory') + '=' + self.story.getMetadata('storyId')) + else: + raise exceptions.InvalidStoryURL(url, + self.getSiteDomain(), + self.getSiteExampleURLs()) + + self.story.setMetadata('siteabbrev','ressec') + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%d %b %Y" # 20 Nov 2005 + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + return 'www.restrictedsection.org' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/story.php?story=1234 http://"+cls.getSiteDomain()+"/file.php?file=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain())+r"/(?P<filestory>file|story).php\?(file|story)=(?P<id>\d+)$" + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + url = self.url + logger.debug("URL: "+url) + + # one-shot stories use file url instead of story. 'Luckily', + # we don't have to worry about one-shots becoming + # multi-chapter because ressec is frozen. Still need 'story' + # url for metadata, however. + try: + if 'file' in url: + data = self._postUrlUP(url) + soup = bs.BeautifulSoup(data) + storya = soup.find('a',href=re.compile(r"^story.php\?story=\d+")) + url = 'http://'+self.host+'/'+storya['href'].split('&')[0] # strip rs_session + + fileas = soup.find('a',href=re.compile(r"^file.php\?file=\d+")) + if fileas: + for filea in fileas: + if 'Previous Chapter' in filea.string or 'Next Chapter' in filea.string: + raise exceptions.FailedToDownload(self.getSiteDomain() +" Cannot use chapter url with multi-chapter stories on this site.") + + logger.debug("metadata URL: "+url) + data = self._fetchUrl(url) + # print data + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + if "Story not found" in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Story not found.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + + # check user/pass on a chapter for multi-chapter + if 'file' not in self.url: + self._postUrlUP('http://'+self.host+'/'+soup.find('a', href=re.compile(r"^file.php\?file=\d+"))['href']) + + ## Title + h2 = soup.find('h2') + + # Find authorid and URL from... author url. + a = h2.find('a') + ahref = a['href'].split('&')[0] # strip rs_session + + self.story.setMetadata('authorId',ahref.split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+ahref) + self.story.setMetadata('author',stripHTML(a)) + + # title, remove byauthorname. + auth=stripHTML(a) + title=stripHTML(h2) + self.story.setMetadata('title',title[:title.index(" by "+auth)]) + + dates = soup.findAll('span', {'class':'date'}) + if dates: # only for multi-chapter + self.story.setMetadata('datePublished', makeDate(stripHTML(dates[0]), self.dateformat)) + self.story.setMetadata('dateUpdated', makeDate(stripHTML(dates[-1]), self.dateformat)) + + words = soup.findAll('span', {'class':'size'}) + wordcount=0 + for w in words: + wordcount = wordcount + int(w.string[:-6].replace(',','')) + + self.story.setMetadata('numWords',"%s"%wordcount) + + self.story.setMetadata('rating', soup.find('a',href=re.compile(r"^rating.php\?rating=\d+")).string) + + # other tags + + labels = soup.find('table', {'class':'info'}).findAll('th') + for labelspan in labels: + value = labelspan.nextSibling + label = labelspan.string + + if label != None: + + if 'Categories' in label: + for g in stripHTML(value).split('\n'): + self.story.addToList('genre',g) + + if 'Pairings' in label: + for g in stripHTML(value).split('\n'): + self.story.addToList('ships',g) + + if 'Summary' in label: + self.setDescription(url,stripHTML(value).replace("\n"," ").replace("\r","")) + value.extract() # remove summary incase it contains file URLs. + + if 'Updated' in label: # one-shots only. + print "value:%s"%value + value.find('sup').extract() # remove 'st', 'nd', 'th' ordinals + print "value:%s"%value + date = makeDate(stripHTML(value), '%d %B %Y') # full month name + self.story.setMetadata('datePublished', date) + + if 'Length' in label: # one-shots only. + self.story.setMetadata('numWords',value.string[:-6]) + + # one-shot. + if 'file' in self.url: + self.chapterUrls.append((self.story.getMetadata('title'),self.url)) + else: # multi-chapter + # Find the chapters: 'library_storyview.php?chapterid=3 + chapters=soup.findAll('a', href=re.compile(r"^file.php\?file=\d+")) + if len(chapters)==0: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: No chapters found.") + else: + for chapter in chapters: + chhref = chapter['href'].split('&')[0] # strip rs_session + # just in case there's tags, like <i> in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chhref)) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + + + def _postUrlUP(self, url): + params = {} + if self.password: + params['username'] = self.username + params['password'] = self.password + else: + params['username'] = self.getConfig("username") + params['password'] = self.getConfig("password") + params['accept.x'] = 1 + params['accept.y'] = 1 + + excpt=None + for sleeptime in [0.5, 1.5, 4, 9]: + time.sleep(sleeptime) + try: + data = self._postUrl(url, params) + if data == "Unable to connect to the database": + raise exceptions.FailedToDownload("Site reported 'Unable to connect to the database'") + if "I certify that I am over the age of 18 and that accessing the following story will not violate the laws of my country or local ordinances." in data: + raise exceptions.FailedToLogin(url,params['username']) + return data + except exceptions.FailedToLogin, ftl: + # no need to retry these. + raise(ftl) + except Exception, e: + excpt=e + logger.warn("Caught an exception reading URL: %s Exception %s."%(unicode(url),unicode(e))) + + logger.error("Giving up on %s" %url) + logger.exception(excpt) + raise(excpt) + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + data = self._postUrlUP(url) + #print("data:%s"%data) + + # some stories have html that confuses the parser. For story + # text we don't care about anything before '<table id="page"' + # and seems to clear the issue. + data = data[data.index('<table id="page"'):] + + soup = bs.BeautifulSoup(data) + + div = soup.find('td',{'id':'page_content'}) + div.name='div' + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + ## Remove stuff from page_content + + # Remove all tags before the first <hr> after class=info table (including hr) + hr = div.find('table',{'class':'info'}).findNext('hr') + for tag in hr.findAllPrevious(): + tag.extract() + hr.extract() + + # Remove all tags after the last <hr> (including hr) + hr = div.findAll('hr')[-1] + for tag in hr.findAllNext(): + tag.extract() + hr.extract() + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_samandjacknet.py b/fanficdownloader/adapters/adapter_samandjacknet.py new file mode 100644 index 0000000..dbfa209 --- /dev/null +++ b/fanficdownloader/adapters/adapter_samandjacknet.py @@ -0,0 +1,342 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +# By virtue of being recent and requiring both is_adult and user/pass, +# adapter_fanficcastletvnet.py is the best choice for learning to +# write adapters--especially for sites that use the eFiction system. +# Most sites that have ".../viewstory.php?sid=123" in the story URL +# are eFiction. + +# For non-eFiction sites, it can be considerably more complex, but +# this is still a good starting point. + +# In general an 'adapter' needs to do these five things: + +# - 'Register' correctly with the downloader +# - Site Login (if needed) +# - 'Are you adult?' check (if needed--some do one, some the other, some both) +# - Grab the chapter list +# - Grab the story meta-data (some (non-eFiction) adapters have to get it from the author page) +# - Grab the chapter texts + +# Search for XXX comments--that's where things are most likely to need changing. + +# This function is called by the downloader in all adapter_*.py files +# in this dir to register the adapter class. So it needs to be +# updated to reflect the class below it. That, plus getSiteDomain() +# take care of 'Registering'. +def getClass(): + return SamAndJackNetAdapter # XXX + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class SamAndJackNetAdapter(BaseSiteAdapter): # XXX + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + # XXX Most sites don't have the /fanfic part. Replace all to remove it usually. + self._setURL('http://' + self.getSiteDomain() + '/fanfics/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','sjn') # 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 + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'samandjack.net' # XXX + + @classmethod + def getSiteExampleURLs(self): + return "http://"+self.getSiteDomain()+"/fanfics/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/fanfics/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() + '/fanfics/user.php?action=login' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['penname'])) + + d = self._fetchUrl(loginUrl, params) + + if "Member Account" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['penname'])) + raise exceptions.FailedToLogin(url,params['penname']) + return False + else: + return True + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + # Weirdly, different sites use different warning numbers. + # If the title search below fails, there's a good chance + # you need a different number. print data at that point + # and see what the 'click here to continue' url says. + + # Furthermore, there's a couple sites now with more than + # one warning level for different ratings. And they're + # fussy about it. midnightwhispers has three: 10, 3 & 5. + # we'll try 5 first. + addurl = "&ageconsent=ok&warning=5" # XXX + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + # 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. + + # Since the warning text can change by warning level, let's + # look for the warning pass url. nfacommunity uses + # &warning= -- actually, so do other sites. Must be an + # eFiction book. + + # viewstory.php?sid=1882&warning=4 + # viewstory.php?sid=1654&ageconsent=ok&warning=5 + #print data + #m = re.search(r"'viewstory.php\?sid=1882(&warning=4)'",data) + m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + pagetitle = soup.find('div',{'id':'pagetitle'}) + ## Title + a = pagetitle.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + # (fetch multiple authors) + alist = soup.findAll('a', href=re.compile(r"viewuser.php\?uid=\d+")) + for a in alist: + self.story.addToList('authorId',a['href'].split('=')[1]) + self.story.addToList('authorUrl','http://'+self.host+'/fanfics/'+a['href']) + self.story.addToList('author',a.string) + + # Reviews + reviewdata = soup.find('div', {'id' : 'sort'}) + a = reviewdata.findAll('a', href=re.compile(r'reviews.php\?type=ST&(amp;)?item='+self.story.getMetadata('storyId')+"$"))[1] # second one. + self.story.setMetadata('reviews',stripHTML(a)) + + # 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+'/fanfics/'+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: + self.setDescription(url,value) + + 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')) + catstext = [cat.string for cat in cats] + for cat in catstext: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + charstext = [char.string for char in chars] + for char in charstext: + self.story.addToList('characters',char.string) + + ## Not all sites use Genre, but there's no harm to + ## leaving it in. Check to make sure the type_id number + ## is correct, though--it's site specific. + if 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) # XXX + genrestext = [genre.string for genre in genres] + self.genre = ', '.join(genrestext) + for genre in genrestext: + self.story.addToList('genre',genre.string) + + ## Not all sites use Warnings, but there's no harm to + ## leaving it in. Check to make sure the type_id number + ## is correct, though--it's site specific. + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX + warningstext = [warning.string for warning in warnings] + self.warning = ', '.join(warningstext) + for warning in warningstext: + 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: + value=value.replace(' | ','') + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label: + # there's a stray [ at the end. + #value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + + try: + # Find Series name from series URL. + a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+")) + series_name = a.string + series_url = 'http://'+self.host+'/fanfics/'+a['href'] + + # use BeautifulSoup HTML parser to make everything easier to find. + seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url)) + storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_samdeanarchivenu.py b/fanficdownloader/adapters/adapter_samdeanarchivenu.py new file mode 100644 index 0000000..398a2bc --- /dev/null +++ b/fanficdownloader/adapters/adapter_samdeanarchivenu.py @@ -0,0 +1,233 @@ +# -*- coding: utf-8 -*- + +# Copyright 2012 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return SamDeanArchiveNuAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class SamDeanArchiveNuAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','sda') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%m/%d/%y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'samdean.archive.nu' + + @classmethod + def getAcceptDomains(cls): + return ['www.samdean.archive.nu','samdean.archive.nu'] + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://")+r"(www\.)?"+re.escape(self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$" + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1' + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title and author + a = soup.find('div', {'id' : 'pagetitle'}) + + aut = a.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',aut['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+aut['href']) + self.story.setMetadata('author',aut.string) + aut.extract() + + self.story.setMetadata('title',stripHTML(a)[:(len(stripHTML(a))-3)]) + + # Find the chapters: + chapters=soup.find('select') + if chapters != None: + for chapter in chapters.findAll('option'): + # just in case there's tags, like <i> in chapter titles. http://samdean.archive.nu/viewstory.php?sid=4317&chapter=2 + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/viewstory.php?sid='+self.story.getMetadata('storyId')+'&chapter='+chapter['value'])) + else: + self.chapterUrls.append((self.story.getMetadata('title'),url)) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl'))) + + for list in asoup.findAll('div', {'class' : re.compile('listbox\s+')}): + a = list.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + if a != None: + break + + # eFiction sites don't help us out a lot with their meta data + # formating, so it's a little ugly. + + # utility method + def defaultGetattr(d,k): + try: + return d[k] + except: + return "" + + # <span class="label">Rated:</span> NC-17<br /> etc + labels = list.findAll('span', {'class' : 'classification'}) + for labelspan in labels: + label = labelspan.string + value = labelspan.nextSibling + + if 'Summary' in label: + ## Everything until the next span class='label' + svalue = "" + while not defaultGetattr(value,'class') == 'classification': + svalue += str(value) + value = value.nextSibling + self.setDescription(url,svalue) + #self.story.setMetadata('description',stripHTML(svalue)) + + if 'Rated' in label: + self.story.setMetadata('rating', value[:len(value)-2]) + + if 'Word count' in label: + self.story.setMetadata('numWords', value) + + if 'Categories' in label: + cats = labelspan.parent.findAll('a',href=re.compile(r'categories.php\?catid=\d+')) + for cat in cats: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + for char in value.string.split(', '): + if not 'None' in char: + self.story.addToList('characters',char) + + if 'Genre' in label: + for genre in value.string.split(', '): + if not 'None' in genre: + self.story.addToList('genre',genre) + + if 'Warnings' in label: + for warning in value.string.split(', '): + if not 'None' in warning: + self.story.addToList('warnings',warning) + + if 'Completed' in label: + if 'Yes' in value: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label: + # there's a stray [ at the end. + #value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + try: + # Find Series name from series URL. + a = list.find('a', href=re.compile(r"series.php\?seriesid=\d+")) + series_name = a.string + series_url = 'http://'+self.host+'/'+a['href'] + + # use BeautifulSoup HTML parser to make everything easier to find. + seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url)) + storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_scarheadnet.py b/fanficdownloader/adapters/adapter_scarheadnet.py new file mode 100644 index 0000000..8ba6044 --- /dev/null +++ b/fanficdownloader/adapters/adapter_scarheadnet.py @@ -0,0 +1,300 @@ +# -*- coding: utf-8 -*- + +# Copyright 2013 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return ScarHeadNetAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class ScarHeadNetAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','shn') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%d/%m/%y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'scarhead.net' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$" + + ## 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' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['penname'])) + + d = self._fetchUrl(loginUrl, params) + + if "Member Account" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['penname'])) + raise exceptions.FailedToLogin(url,params['penname']) + return False + else: + return True + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + # Weirdly, different sites use different warning numbers. + # If the title search below fails, there's a good chance + # you need a different number. print data at that point + # and see what the 'click here to continue' url says. + addurl = "&ageconsent=ok&warning=5" + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url) + + # Since the warning text can change by warning level, let's + # look for the warning pass url. ksarchive uses + # &warning= -- actually, so do other sites. Must be an + # eFiction book. + + # viewstory.php?sid=1882&warning=4 + # viewstory.php?sid=1654&ageconsent=ok&warning=5 + #print data + #m = re.search(r"'viewstory.php\?sid=1882(&warning=4)'",data) + m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + pagetitle = soup.find('tr',{'valign':'top'}) + + ## Title + a = pagetitle.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = pagetitle.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href']) + self.story.setMetadata('author',a.string) + + # Find the chapters: + for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")): + # just in case there's tags, like <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. + + cats = soup.findAll('a',href=re.compile(r'browse.php\?type=categories')) + for cat in cats: + if '/' == cat.string[0]: + self.story.addToList('ships','Harry Potter'+cat.string.split('(')[0]) + elif 'Harry' in cat.string: + self.story.addToList('ships',cat.string.split('(')[0]) + else: + self.story.addToList('category',cat.string) + if '(' in cat.string: + self.story.addToList('category',cat.string.split('(')[1].split(')')[0]) + + + + + chars = soup.findAll('a',href=re.compile(r'browse.php\?type=characters')) + for char in chars: + self.story.addToList('characters',char.string) + + genres = soup.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) + for genre in genres: + self.story.addToList('genre',genre.string) + + warnings = soup.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) + for warning in warnings: + self.story.addToList('warnings',warning.string) + + textsoup = stripHTML(soup) + + a = textsoup.split('Published: ')[1].split(' ')[0] + self.story.setMetadata('datePublished', makeDate(stripHTML(a), self.dateformat)) + a = textsoup.split('Updated: ')[1].split(' ')[0] + self.story.setMetadata('dateUpdated', makeDate(stripHTML(a), self.dateformat)) + a = textsoup.split('Rating: ')[1].split(' ')[0] + self.story.setMetadata('rating', a) + a = textsoup.split('Length: ')[1].split('(')[1].split(' ')[0] + self.story.setMetadata('numWords', a) + a = textsoup.split('Completed: ')[1].split(' ')[0] + if 'Yes' in a: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + #a = textsoup.split('Summary: ')[1].split('Add Story to Favorites')[0] + #self.setDescription(url,a) + + + + a=soup.find(text=re.compile("Summary: ")) + i=0 + svalue = "" + while i == 0: + try: + b = str(a) + svalue += b.split('Summary: ')[1] + except: + svalue += str(a) + if a.nextSibling != None: + a = a.nextSibling + else: + a = a.parent.nextSibling + if 'Disclaimer: ' in stripHTML(a): + i=1 + self.setDescription(url,svalue) + + 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)) + storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_scarvesandcoffeenet.py b/fanficdownloader/adapters/adapter_scarvesandcoffeenet.py new file mode 100644 index 0000000..f90a77e --- /dev/null +++ b/fanficdownloader/adapters/adapter_scarvesandcoffeenet.py @@ -0,0 +1,248 @@ +# -*- coding: utf-8 -*- + +# Copyright 2012 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return ScarvesAndCoffeeNetAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class ScarvesAndCoffeeNetAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','scacf') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%m/%d/%Y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'www.scarvesandcoffee.net' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$" + + ## 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 + + ## 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=20" + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('div',{"id":"pagetitle"}).find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href']) + self.story.setMetadata('author',a.string) + + # Find the chapters: + for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")): + # just in case there's tags, like <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('genre',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 '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) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_sg1heliopoliscom.py b/fanficdownloader/adapters/adapter_sg1heliopoliscom.py new file mode 100644 index 0000000..f36edae --- /dev/null +++ b/fanficdownloader/adapters/adapter_sg1heliopoliscom.py @@ -0,0 +1,259 @@ +# -*- coding: utf-8 -*- + +# Copyright 2012 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return SG1HeliopolisComAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class SG1HeliopolisComAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + self.section=self.parsedUrl.path.split('/',)[1] + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/'+self.section+'/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','sghp') + + # 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. Can't use extracategories, could be Atlantis or SG-1 + if 'atlantis' in self.section: + self.story.addToList("category","Stargate: Atlantis") + else: + 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 = "%Y.%m.%d" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'sg1-heliopolis.com' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/archive/viewstory.php?sid=1234 http://"+cls.getSiteDomain()+"/adult/viewstory.php?sid=1234 http://"+cls.getSiteDomain()+"/atlantis/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return r"http://sg1-heliopolis.com/(archive|adult|atlantis)?/viewstory.php\?sid=\d+$" + + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + # Weirdly, different sites use different warning numbers. + # If the title search below fails, there's a good chance + # you need a different number. print data at that point + # and see what the 'click here to continue' url says. + addurl = "&warning=4" + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href']) + self.story.setMetadata('author',a.string) + + # Find the chapters: + for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")): + # just in case there's tags, like <i> in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+self.section+'/'+chapter['href']+addurl)) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + # eFiction sites don't help us out a lot with their meta data + # formating, so it's a little ugly. + + # utility method + def defaultGetattr(d,k): + try: + return d[k] + except: + return "" + + # <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 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) + for genre in genres: + self.story.addToList('genre',genre.string) + + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) + for warning in warnings: + self.story.addToList('warnings',warning.string) + + if 'Completed' in label: + if 'Yes' in value: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label: + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + try: + # Find Series name from series URL. + a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+")) + series_name = a.string + series_url = 'http://'+self.host+'/'+self.section+'/'+a['href'] + + # use BeautifulSoup HTML parser to make everything easier to find. + seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url)) + # can't use ^viewstory...$ in case of higher rated stories with javascript href. + storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+')) + i=1 + for a in storyas: + # skip 'report this' and 'TOC' links + if 'contact.php' not in a['href'] and 'index' not in a['href']: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_sheppardweircom.py b/fanficdownloader/adapters/adapter_sheppardweircom.py new file mode 100644 index 0000000..ddce071 --- /dev/null +++ b/fanficdownloader/adapters/adapter_sheppardweircom.py @@ -0,0 +1,317 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +# By virtue of being recent and requiring both is_adult and user/pass, +# adapter_fanficcastletvnet.py is the best choice for learning to +# write adapters--especially for sites that use the eFiction system. +# Most sites that have ".../viewstory.php?sid=123" in the story URL +# are eFiction. + +# For non-eFiction sites, it can be considerably more complex, but +# this is still a good starting point. + +# In general an 'adapter' needs to do these five things: + +# - 'Register' correctly with the downloader +# - Site Login (if needed) +# - 'Are you adult?' check (if needed--some do one, some the other, some both) +# - Grab the chapter list +# - Grab the story meta-data (some (non-eFiction) adapters have to get it from the author page) +# - Grab the chapter texts + +# Search for XXX comments--that's where things are most likely to need changing. + +# This function is called by the downloader in all adapter_*.py files +# in this dir to register the adapter class. So it needs to be +# updated to reflect the class below it. That, plus getSiteDomain() +# take care of 'Registering'. +def getClass(): + return SheppardWeirComAdapter # XXX + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class SheppardWeirComAdapter(BaseSiteAdapter): # XXX + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + # XXX Most sites don't have the /fanfic part. Replace all to remove it usually. + self._setURL('http://' + self.getSiteDomain() + '/fanfics/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','swf') # 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 + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'sheppardweir.com' # XXX + + @classmethod + def getSiteExampleURLs(self): + return "http://"+self.getSiteDomain()+"/fanfics/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/fanfics/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() + '/fanfics/user.php?action=login' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['penname'])) + + d = self._fetchUrl(loginUrl, params) + + if "Member Account" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['penname'])) + raise exceptions.FailedToLogin(url,params['penname']) + return False + else: + return True + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + # Weirdly, different sites use different warning numbers. + # If the title search below fails, there's a good chance + # you need a different number. print data at that point + # and see what the 'click here to continue' url says. + addurl = "&ageconsent=ok&warning=4" # XXX + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url) + + # 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 "Age Consent Required" in data: # XXX + 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. + + pagetitle = soup.find('div',{'id':'pagetitle'}) + ## Title + a = pagetitle.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + # (fetch multiple authors) + alist = soup.findAll('a', href=re.compile(r"viewuser.php\?uid=\d+")) + for a in alist: + self.story.addToList('authorId',a['href'].split('=')[1]) + self.story.addToList('authorUrl','http://'+self.host+'/fanfics/'+a['href']) + self.story.addToList('author',a.string) + + + # Reviews + reviewdata = soup.find('div', {'id' : 'sort'}) + a = reviewdata.findAll('a', href=re.compile(r'reviews.php\?type=ST&(amp;)?item='+self.story.getMetadata('storyId')+"$"))[1] # second one. + self.story.setMetadata('reviews',stripHTML(a)) + + + # 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+'/fanfics/'+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 "" + + # Summary + summarydata = unicode(soup.find('div',{'class':'content'})) + start='<span class="label">Summary: </span>' + end='</div>' + summarydata = summarydata[summarydata.index(start)+len(start):summarydata.rindex(end)] + self.setDescription(url,bs.BeautifulSoup(summarydata)) + + # <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 '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')) + catstext = [cat.string for cat in cats] + for cat in catstext: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + charstext = [char.string for char in chars] + for char in charstext: + self.story.addToList('characters',char.string) + + ## Not all sites use Genre, but there's no harm to + ## leaving it in. Check to make sure the type_id number + ## is correct, though--it's site specific. + if 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) # XXX + genrestext = [genre.string for genre in genres] + self.genre = ', '.join(genrestext) + for genre in genrestext: + self.story.addToList('genre',genre.string) + + ## Not all sites use Warnings, but there's no harm to + ## leaving it in. Check to make sure the type_id number + ## is correct, though--it's site specific. + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX + warningstext = [warning.string for warning in warnings] + self.warning = ', '.join(warningstext) + for warning in warningstext: + 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: + value=value.replace(' - ','') + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label: + # there's a stray [ at the end. + #value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + try: + # Find Series name from series URL. + a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+")) + series_name = a.string + series_url = 'http://'+self.host+'/fanfics/'+a['href'] + + # use BeautifulSoup HTML parser to make everything easier to find. + seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url)) + storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_simplyundeniablecom.py b/fanficdownloader/adapters/adapter_simplyundeniablecom.py new file mode 100644 index 0000000..131b8be --- /dev/null +++ b/fanficdownloader/adapters/adapter_simplyundeniablecom.py @@ -0,0 +1,220 @@ +# -*- coding: utf-8 -*- + +# Copyright 2013 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return SimplyUndeniableComAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class SimplyUndeniableComAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','sud') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%m/%d/%Y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'www.simplyundeniable.com' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$" + + ## Login seems to be reasonably standard across eFiction sites. + def needToLoginCheck(self, data): + if 'Please log in now' 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' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['penname'])) + + d = self._fetchUrl(loginUrl, params) + + if "My Account" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['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): + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url) + + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + if "Please log in now" in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: You need to have access to the restricted section.") + + # 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('h1') + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href']) + self.story.setMetadata('author',a.string) + asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl'))) + + for info in asoup.findAll('table', {'cellpadding' : '5'}): + a = info.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + if a != None: + self.story.setMetadata('title',stripHTML(a)) + break + + # Find the chapters: + if "Disclaimer" in data: + self.chapterUrls.append((self.story.getMetadata('title'),url)) + else: + for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+&i=1$')): + # just in case there's tags, like <i> in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href'])) + + 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. + + a = info.find('td', {'valign' : 'top'}).find('p') + self.setDescription(url,a) + + a = info.find('td', {'class' : 'greysm'}).findAll('b') + self.story.setMetadata('datePublished', makeDate(stripHTML(a[0].nextSibling), self.dateformat)) + self.story.setMetadata('dateUpdated', makeDate(stripHTML(a[1].nextSibling), self.dateformat)) + if 'Yes' in a[2].nextSibling: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + self.story.setMetadata('rating', a[3].nextSibling) + self.story.setMetadata('numWords', a[4].nextSibling) + + + warnings = info.find('td', {'width' : '45'}).nextSibling.nextSibling.text.split(', ') + for warning in warnings: + if 'none' not in warning: + self.story.addToList('warnings',warning) + chars = info.find('td', {'width' : '51'}).nextSibling.nextSibling.text.split(', ') + for char in chars: + if '&' in char: + self.story.addToList('ships',char) + else: + self.story.addToList('characters',char) + genres = info.find('td', {'width' : '36'}).nextSibling.nextSibling.text.split(', ') + for genre in genres: + self.story.addToList('genre',genre) + + cat = info.find('a', href=re.compile(r'categories.php')) + self.story.addToList('category',cat.string) + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('span', {'class' : 'style'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + div.name='div' + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_sinfuldesireorg.py b/fanficdownloader/adapters/adapter_sinfuldesireorg.py new file mode 100644 index 0000000..2187c68 --- /dev/null +++ b/fanficdownloader/adapters/adapter_sinfuldesireorg.py @@ -0,0 +1,252 @@ +# -*- coding: utf-8 -*- + +# Copyright 2012 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return SinfulDesireOrgAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class SinfulDesireOrgAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/archive/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','snds') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%m/%d/%Y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'www.sinful-desire.org' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/archive/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/archive/viewstory.php?sid=")+r"\d+$" + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + # Weirdly, different sites use different warning numbers. + # If the title search below fails, there's a good chance + # you need a different number. print data at that point + # and see what the 'click here to continue' url says. + addurl = "&warning=5" + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/archive/'+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+'/archive/'+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 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) + for genre in genres: + self.story.addToList('genre',genre.string) + 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+'/archive/'+a['href'] + + # use BeautifulSoup HTML parser to make everything easier to find. + seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url)) + # can't use ^viewstory...$ in case of higher rated stories with javascript href. + storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+')) + i=1 + for a in storyas: + # skip 'report this' and 'TOC' links + if 'contact.php' not in a['href'] and 'index' not in a['href']: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_siyecouk.py b/fanficdownloader/adapters/adapter_siyecouk.py new file mode 100644 index 0000000..9129bfc --- /dev/null +++ b/fanficdownloader/adapters/adapter_siyecouk.py @@ -0,0 +1,247 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +# This function is called by the downloader in all adapter_*.py files +# in this dir to register the adapter class. So it needs to be +# updated to reflect the class below it. That, plus getSiteDomain() +# take care of 'Registering'. +def getClass(): + return SiyeCoUkAdapter # XXX + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class SiyeCoUkAdapter(BaseSiteAdapter): # XXX + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8",]# 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/siye/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','siye') # XXX + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%Y.%m.%d" # XXX + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'www.siye.co.uk' # XXX + + @classmethod + def getAcceptDomains(cls): + return ['www.siye.co.uk','siye.co.uk'] + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/siye/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://")+r"(www\.)?siye\.co\.uk/(siye/)?"+re.escape("viewstory.php?sid=")+r"\d+$" + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + # Except it doesn't this time. :-/ + url = self.url #+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/siye/'+a['href']) + self.story.setMetadata('author',a.string) + + # need(or easier) to pull other metadata from the author's list page. + authsoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl'))) + + ## Title + titlea = authsoup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(titlea)) + + # Find the chapters (from soup, not authsoup): + 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+'/siye/'+chapter['href'])) + + if self.chapterUrls: + self.story.setMetadata('numChapters',len(self.chapterUrls)) + else: + self.chapterUrls.append((self.story.getMetadata('title'),url)) + self.story.setMetadata('numChapters',1) + + # The stuff we can get from the chapter list/one-shot page are + # in the first table with 95% width. + metatable = soup.find('table',{'width':'95%'}) + + # Categories + cat_as = metatable.findAll('a', href=re.compile(r'categories.php')) + for cat_a in cat_as: + self.story.addToList('category',stripHTML(cat_a)) + + moremetaparts = stripHTML(metatable).split('\n') + for part in moremetaparts: + part = part.strip() + if part.startswith("Characters:"): + part = part[part.find(':')+1:] + for item in part.split(','): + if item.strip() == "Harry/Ginny": + self.story.addToList('characters',"Harry") + self.story.addToList('characters',"Ginny") + elif item.strip() not in ("None","All"): + self.story.addToList('characters',item) + + if part.startswith("Genres:"): + part = part[part.find(':')+1:] + for item in part.split(','): + if item.strip() != "None": + self.story.addToList('genre',item) + + if part.startswith("Warnings:"): + part = part[part.find(':')+1:] + for item in part.split(','): + if item.strip() != "None": + self.story.addToList('warnings',item) + + if part.startswith("Rating:"): + part = part[part.find(':')+1:] + self.story.setMetadata('rating',part) + + if part.startswith("Summary:"): + part = part[part.find(':')+1:] + self.setDescription(url,part) + #self.story.setMetadata('description',part) + + # want to get the next tr of the table. + #print("%s"%titlea.parent.parent.findNextSibling('tr')) + + # eFiction sites don't help us out a lot with their meta data + # formating, so it's a little ugly. + + # SIYE formats stories in the author list differently when their part of a series. + # Look for non-series... + divdesc = titlea.parent.parent.find('div',{'class':'desc'}) + if not divdesc: + # ... now look for series. + divdesc = titlea.parent.parent.findNextSibling('tr').find('div',{'class':'desc'}) + + moremeta = stripHTML(divdesc) + #print("moremeta:%s"%moremeta) + for part in moremeta.replace(' - ','\n').split('\n'): + #print("part:%s"%part) + try: + (name,value) = part.split(': ') + except: + # not going to worry about fancier processing for the bits + # that don't match. + continue + name=name.strip() + value=value.strip() + if name == 'Published': + self.story.setMetadata('datePublished', makeDate(value, self.dateformat)) + if name == 'Updated': + self.story.setMetadata('dateUpdated', makeDate(value, self.dateformat)) + if name == 'Completed': + if value == 'Yes': + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + if name == 'Words': + self.story.setMetadata('numWords', value) + + try: + # Find Series name from series URL. + a = titlea.findPrevious('a', href=re.compile(r"series.php\?seriesid=\d+")) + series_name = a.string + series_url = 'http://'+self.host+'/'+a['href'] + + # use BeautifulSoup HTML parser to make everything easier to find. + seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url)) + storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + # soup = bs.BeautifulSoup(self._fetchUrl(url)) + # BeautifulSoup objects to <p> inside <span>, which + # technically isn't allowed. + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + # not the most unique thing in the world, but it appears to be + # the best we can do here. + story = soup.find('span', {'style' : 'font-size: 100%;'}) + + if None == story: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + story.name='div' + + return self.utf8FromSoup(url,story) diff --git a/fanficdownloader/adapters/adapter_spikeluvercom.py b/fanficdownloader/adapters/adapter_spikeluvercom.py new file mode 100644 index 0000000..511463d --- /dev/null +++ b/fanficdownloader/adapters/adapter_spikeluvercom.py @@ -0,0 +1,208 @@ +# Software: eFiction +import re +import urllib2 +import urlparse + +from .. import BeautifulSoup +from ..htmlcleanup import stripHTML + +from base_adapter import BaseSiteAdapter, makeDate +from .. import exceptions + + +def getClass(): + return SpikeluverComAdapter + + +# yields Tag _and_ NavigableString siblings from the given tag. The +# BeautifulSoup findNextSiblings() method for some reasons only returns either +# NavigableStrings _or_ Tag objects, not both. +def _yield_next_siblings(tag): + sibling = tag.nextSibling + while sibling: + yield sibling + sibling = sibling.nextSibling + + +class SpikeluverComAdapter(BaseSiteAdapter): + SITE_ABBREVIATION = 'slc' + SITE_DOMAIN = 'spikeluver.com' + + BASE_URL = 'http://' + SITE_DOMAIN + '/SpuffyRealm/' + LOGIN_URL = BASE_URL + 'user.php?action=login' + VIEW_STORY_URL_TEMPLATE = BASE_URL + 'viewstory.php?sid=%d' + METADATA_URL_SUFFIX = '&index=1' + AGE_CONSENT_URL_SUFFIX = '&ageconsent=ok&warning=5' + + DATETIME_FORMAT = '%m/%d/%Y' + STORY_DOES_NOT_EXIST_ERROR_TEXT = 'That story does not exist on this archive. You may search for it or return to the home page.' + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + query_data = urlparse.parse_qs(self.parsedUrl.query) + story_id = query_data['sid'][0] + + self.story.setMetadata('storyId', story_id) + self._setURL(self.VIEW_STORY_URL_TEMPLATE % int(story_id)) + self.story.setMetadata('siteabbrev', self.SITE_ABBREVIATION) + + def _customized_fetch_url(self, url, exception=None, parameters=None): + if exception: + try: + data = self._fetchUrl(url, parameters) + except urllib2.HTTPError: + raise exception(self.url) + # Just let self._fetchUrl throw the exception, don't catch and + # customize it. + else: + data = self._fetchUrl(url, parameters) + + return BeautifulSoup.BeautifulSoup(data) + + @staticmethod + def getSiteDomain(): + return SpikeluverComAdapter.SITE_DOMAIN + + @classmethod + def getSiteExampleURLs(cls): + return cls.VIEW_STORY_URL_TEMPLATE % 1234 + + def getSiteURLPattern(self): + return re.escape(self.VIEW_STORY_URL_TEMPLATE[:-2]) + r'\d+$' + + def extractChapterUrlsAndMetadata(self): + soup = self._customized_fetch_url(self.url + self.METADATA_URL_SUFFIX) + + errortext_div = soup.find('div', {'class': 'errortext'}) + if errortext_div: + error_text = ''.join(errortext_div(text=True)).strip() + if error_text == self.STORY_DOES_NOT_EXIST_ERROR_TEXT: + raise exceptions.StoryDoesNotExist(self.url) + + # No additional login is required, just check for adult + pagetitle_div = soup.find('div', id='pagetitle') + if pagetitle_div.a['href'].startswith('javascript:'): + if not(self.is_adult or self.getConfig('is_adult')): + raise exceptions.AdultCheckRequired(self.url) + + url = ''.join([self.url, self.METADATA_URL_SUFFIX, self.AGE_CONSENT_URL_SUFFIX]) + soup = self._customized_fetch_url(url) + + pagetitle_div = soup.find('div', id='pagetitle') + self.story.setMetadata('title', stripHTML(pagetitle_div.a)) + + author_anchor = pagetitle_div.a.findNextSibling('a') + url = urlparse.urljoin(self.BASE_URL, author_anchor['href']) + components = urlparse.urlparse(url) + query_data = urlparse.parse_qs(components.query) + + self.story.setMetadata('author', stripHTML(author_anchor)) + self.story.setMetadata('authorId', query_data['uid']) + self.story.setMetadata('authorUrl', url) + + sort_div = soup.find('div', id='sort') + self.story.setMetadata('reviews', stripHTML(sort_div('a')[1])) + + listbox_tag = soup.find('div', {'class': 'listbox'}) + for span_tag in listbox_tag('span'): + key = span_tag.string.strip(' :') + try: + value = stripHTML(span_tag.nextSibling) + # This can happen with some fancy markup in the summary. Just + # ignore this error and set value to None, the summary parsing + # takes care of this + except AttributeError: + value = None + + if key == 'Summary': + contents = [] + keep_summary_html = self.getConfig('keep_summary_html') + + for sibling in _yield_next_siblings(span_tag): + if isinstance(sibling, BeautifulSoup.Tag): + # Encountered next label, break. Not as bad as other + # e-fiction sites, let's hope this is enough for proper + # parsing. + if sibling.name == 'span' and sibling.get('class', None) == 'label': + break + + if keep_summary_html: + contents.append(self.utf8FromSoup(self.url, sibling)) + else: + contents.append(''.join(sibling(text=True))) + else: + contents.append(sibling) + + # Remove the preceding break line tag and other crud + contents.pop() + contents.pop() + self.story.setMetadata('description', ''.join(contents)) + + elif key == 'Rated': + self.story.setMetadata('rating', value) + + elif key == 'Categories': + for sibling in span_tag.findNextSiblings(['a', 'br']): + if sibling.name == 'br': + break + + self.story.addToList('category', stripHTML(sibling)) + + # Seems to be always "None" for some reason + elif key == 'Characters': + for sibling in span_tag.findNextSiblings(['a', 'br']): + if sibling.name == 'br': + break + self.story.addToList('characters', stripHTML(sibling)) + + elif key == 'Genres': + for sibling in span_tag.findNextSiblings(['a', 'br']): + if sibling.name == 'br': + break + + self.story.addToList('genre', stripHTML(sibling)) + + elif key == 'Warnings': + for sibling in span_tag.findNextSiblings(['a', 'br']): + if sibling.name == 'br': + break + self.story.addToList('warnings', stripHTML(sibling)) + + # Challenges + + elif key == 'Series': + a = span_tag.findNextSibling('a') + if not a: + continue + self.story.setMetadata('series', stripHTML(a)) + self.story.setMetadata('seriesUrl', urlparse.urljoin(self.BASE_URL, a['href'])) + + elif key == 'Chapters': + self.story.setMetadata('numChapters', int(value)) + + elif key == 'Completed': + self.story.setMetadata('status', 'Completed' if value == 'Yes' else 'In-Progress') + + elif key == 'Word count': + self.story.setMetadata('numWords', value) + + elif key == 'Published': + self.story.setMetadata('datePublished', makeDate(value, self.DATETIME_FORMAT)) + + elif key == 'Updated': + self.story.setMetadata('dateUpdated', makeDate(value, self.DATETIME_FORMAT)) + + for p_tag in listbox_tag.findNextSiblings('p'): + chapter_anchor = p_tag.find('a', href=lambda href: href and href.startswith('viewstory.php?sid=')) + if not chapter_anchor: + continue + + title = stripHTML(chapter_anchor) + url = urlparse.urljoin(self.BASE_URL, chapter_anchor['href']) + self.chapterUrls.append((title, url)) + + def getChapterText(self, url): + url += self.AGE_CONSENT_URL_SUFFIX + soup = self._customized_fetch_url(url) + return self.utf8FromSoup(url, soup.find('div', id='story')) diff --git a/fanficdownloader/adapters/adapter_squidgeorgpeja.py b/fanficdownloader/adapters/adapter_squidgeorgpeja.py new file mode 100644 index 0000000..98927ca --- /dev/null +++ b/fanficdownloader/adapters/adapter_squidgeorgpeja.py @@ -0,0 +1,256 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + + +def getClass(): + return SquidgeOrgPejaAdapter + +## XXX IMPORTANT NOTE!! This adapter is for squidge.org/peja ONLY! +## There are lots of other sites and stuff under squidge.org that +## we're not supporting. If/When we ever want to support more +## sections of squidge.org, FFDL will need to be changed more +## fundamentally to find different adapters under the same domain. +## +## For now, I've only implemented the part for ini section names so +## if/when more adapters under squidge.org come along, existing ini +## files will still work correctly. + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class SquidgeOrgPejaAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["utf8", + "Windows-1252"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('https://' + self.getSiteDomain() + '/peja/cgi-bin/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','wwomb') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%m/%d/%y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'www.squidge.org' + + @classmethod # must be @classmethod, don't remove it. + def getConfigSection(cls): + # The config section name. Only override if != site domain. + return cls.getSiteDomain()+'/peja' + + @classmethod + def getSiteExampleURLs(cls): + return "https://"+cls.getSiteDomain()+"/peja/cgi-bin/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + # but not https://www.squidge.org/peja/cgi-bin/viewstory.php?sid=47746 -- that's the 'Site Map' negative look aead + return r"https?"+re.escape("://"+self.getSiteDomain()+"/")+r"~?"+re.escape("peja/cgi-bin/viewstory.php?sid=")+r"(?!47746)\d+$" + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + url = self.url + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + data = self._fetchUrl(url) + + if "fatal MySQL error was encountered" in data: + raise exceptions.FailedToDownload("Site SQL Error--bad story") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + # Find authorid and URL from... author url. + author = soup.find('div', {'id':"pagetitle"}).find('a') + self.story.setMetadata('authorId',author['href'].split('=')[1]) + self.story.setMetadata('authorUrl','https://'+self.host+'/peja/cgi-bin/'+author['href']) + self.story.setMetadata('author',author.string) + + authorSoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl'))) + + # eFiction sites don't help us out a lot with their meta data + # formating, so it's a little ugly. + + title = authorSoup.find('a',{'href':'viewstory.php?sid='+self.story.getMetadata('storyId')}) + self.story.setMetadata('title',stripHTML(title)) + titleblock=title.parent.parent + + chapterselect=soup.find('select',{'name':'chapter'}) + if chapterselect: + for ch in chapterselect.findAll('option'): + self.chapterUrls.append((stripHTML(ch),'https://'+self.host+'/peja/cgi-bin/viewstory.php?sid='+self.story.getMetadata('storyId')+'&chapter='+ch['value'])) + else: + self.chapterUrls.append((title,url)) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + + # utility method + def defaultGetattr(d,k): + try: + return d[k] + except: + return "" + + # <span class="classification">Rated:</span> NC-17<br /> etc + labels = titleblock.findAll('span',{'class':'classification'}) + for labelspan in labels: + value = labelspan.nextSibling + label = labelspan.string + #print("label:%s value:%s"%(label,value)) + + if 'Summary' in label: + ## Everything until the next span class='label' + svalue = "" + while not defaultGetattr(value,'class') == 'classification': + svalue += str(value) + value = value.nextSibling + self.setDescription(url,svalue) + #self.story.setMetadata('description',stripHTML(svalue)) + + if 'Rated' in label: + if value.endswith("["): + value = value[:-1] + self.story.setMetadata('rating', value) + + if 'Characters' in label: + for char in value.split(','): + self.story.addToList('characters',char.strip()) + + if 'Genre' in label: + for genre in value.split(','): + if genre.strip() != "None": + self.story.addToList('genre',genre.strip()) + + if 'Warnings' in label: + for warning in value.split(','): + if warning.strip() != 'None': + self.story.addToList('warnings',warning.strip()) + + if 'Word count' in label: + self.story.setMetadata('numWords', value) + + if 'Fandoms' in label: + cats = labelspan.parent.findAll('a',href=re.compile(r'categories.php')) + catstext = [cat.string for cat in cats] + for cat in catstext: + self.story.addToList('category',cat.string) + + if 'Completed' in label: + if 'Yes' in value: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label: + # there's a stray [ at the end. + #value = value[0:-1] + + # site has started including a tracking script between Updated label and date--sometimes... + svalue = "" + while value is not None and not defaultGetattr(value,'class') == 'classification': + if '<script' not in unicode(value): + svalue += unicode(value) + value = value.nextSibling + #print("svalue:%s"%svalue) + + self.story.setMetadata('dateUpdated', makeDate(stripHTML(svalue), self.dateformat)) + + try: + # Find Series name from series URL. + # http://www.squidge.org/peja/cgi-bin/series.php?seriesid=254 + a = titleblock.find('a', href=re.compile(r"series.php\?seriesid=\d+")) + series_name = a.string + series_url = 'https://'+self.host+'/peja/cgi-bin/'+a['href'] + + # use BeautifulSoup HTML parser to make everything easier to find. + seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url)) + storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + # don't count the 'site map' story. See the url pattern method. + if '47746' not in a['href']: + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + chaptext = soup.find('div',{'id':"story"}).find('span') + + if None == chaptext: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,chaptext) diff --git a/fanficdownloader/adapters/adapter_stargateatlantisorg.py b/fanficdownloader/adapters/adapter_stargateatlantisorg.py new file mode 100644 index 0000000..f842853 --- /dev/null +++ b/fanficdownloader/adapters/adapter_stargateatlantisorg.py @@ -0,0 +1,230 @@ +# -*- coding: utf-8 -*- + +# Copyright 2012 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return StargateAtlantisOrgAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class StargateAtlantisOrgAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/fanfics/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','stat') + + # 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 'stargate-atlantis.org' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/fanfics/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/fanfics/viewstory.php?sid=")+r"\d+$" + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1' + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title and author + a = soup.find('div', {'id' : 'pagetitle'}) + + aut = a.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',aut['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/fanfics/'+aut['href']) + self.story.setMetadata('author',aut.string) + aut.extract() + + self.story.setMetadata('title',stripHTML(a)[:(len(stripHTML(a))-3)]) + + # Find the chapters: + chapters=soup.findAll('div', {'class' : 'content'}) + if len(chapters) > 1: + for chapter in chapters: + # just in case there's tags, like <i> in chapter titles. + link = chapter.find('a') + self.chapterUrls.append((stripHTML(link),'http://'+self.host+'/fanfics/'+link['href'])) + else: + self.chapterUrls.append((self.story.getMetadata('title'),url)) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl'))) + + for list in asoup.findAll('div', {'class' : re.compile('listbox\s+')}): + a = list.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + if a != None: + break + + # eFiction sites don't help us out a lot with their meta data + # formating, so it's a little ugly. + + # utility method + def defaultGetattr(d,k): + try: + return d[k] + except: + return "" + + # <span class="label">Rated:</span> NC-17<br /> etc + labels = list.findAll('span', {'class' : 'classification'}) + for labelspan in labels: + label = labelspan.string + value = labelspan.nextSibling + + if 'Summary' in label: + ## Everything until the next span class='label' + svalue = "" + while not defaultGetattr(value,'class') == 'tail' and value != None: + svalue += str(value) + value = value.nextSibling + self.setDescription(url,svalue) + #self.story.setMetadata('description',stripHTML(svalue)) + + if 'Rated' in label: + self.story.setMetadata('rating', value[:len(value)-2]) + + if 'Word count' in label: + self.story.setMetadata('numWords', value) + + if 'Categories' in label: + cats = labelspan.parent.findAll('a',href=re.compile(r'categories.php\?catid=\d+')) + for cat in cats: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + for char in value.string.split(', '): + if not 'None' in char: + self.story.addToList('characters',char) + + if 'Genre' in label: + for genre in value.string.split(', '): + if not 'None' in genre: + self.story.addToList('genre',genre) + + if 'Warnings' in label: + for warning in value.string.split(', '): + if not 'None' in warning: + self.story.addToList('warnings',warning) + + if 'Completed' in label: + if 'Yes' in value: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value.split(' ::')[0]), self.dateformat)) + + if 'Updated' in label: + # there's a stray [ at the end. + #value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + try: + # Find Series name from series URL. + a = list.find('a', href=re.compile(r"series.php\?seriesid=\d+")) + series_name = a.string + series_url = 'http://'+self.host+'/fanfics/'+a['href'] + + # use BeautifulSoup HTML parser to make everything easier to find. + seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url)) + storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_storiesofardacom.py b/fanficdownloader/adapters/adapter_storiesofardacom.py new file mode 100644 index 0000000..5ad3ef8 --- /dev/null +++ b/fanficdownloader/adapters/adapter_storiesofardacom.py @@ -0,0 +1,161 @@ +# -*- 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 +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return StoriesOfArdaComAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class StoriesOfArdaComAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/chapterlistview.asp?SID='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','soa') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%m/%d/%Y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'www.storiesofarda.com' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/chapterlistview.asp?SID=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/chapterlistview.asp?SID=")+r"\d+$" + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title and author + a = soup.find('th', {'colspan' : '3'}) + + aut = a.find('a') + self.story.setMetadata('authorId',aut['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+aut['href']) + self.story.setMetadata('author',aut.string) + asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl'))) + + a.find('em').extract() + self.story.setMetadata('title',stripHTML(a)) + + # Find the chapters: chapterview.asp?sid=7000&cid=30919 + chapters=soup.findAll('a', href=re.compile(r'chapterview.asp\?sid='+self.story.getMetadata('storyId')+"&cid=\d+$")) + if len(chapters)==1: + self.chapterUrls.append((self.story.getMetadata('title'),'http://'+self.host+'/'+chapters[0]['href'])) + else: + for chapter in chapters: + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href'])) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + summary = soup.find('td', {'colspan' : '3'}) + self.setDescription(url,summary) + + # no convenient way to get word count + + for td in asoup.findAll('td', {'colspan' : '3'}): + if td.find('a', href=re.compile('chapterlistview.asp\?SID='+self.story.getMetadata('storyId'))) != None: + break + td=td.nextSibling.nextSibling + self.story.setMetadata('dateUpdated', makeDate(stripHTML(td).split(': ')[1], self.dateformat)) + tr=td.parent.nextSibling.nextSibling.nextSibling.nextSibling + td=tr.findAll('td') + self.story.setMetadata('rating', td[0].string.split(': ')[1]) + self.story.setMetadata('status', td[2].string.split(': ')[1]) + self.story.setMetadata('datePublished', makeDate(stripHTML(td[4]).split(': ')[1], self.dateformat)) + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + if self.getConfig('is_adult'): + params = {'confirmAge':'1'} + data = self._postUrl(url,params) + else: + data = self._fetchUrl(url) + + data = data[data.index('<table width="90%" align="center">'):] + data.replace("<body","<notbody").replace("<BODY","<NOTBODY") + + soup = bs.BeautifulStoneSoup(data, + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + if "Please indicate that you are an adult by selecting the appropriate choice below" in data: + raise exceptions.FailedToDownload("Chapter requires you be an adult. Set is_adult in personal.ini (chapter url:%s)" % url) + + div = soup.find('table', {'width' : '90%'}).find('td') + div.name='div' + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_storiesonlinenet.py b/fanficdownloader/adapters/adapter_storiesonlinenet.py new file mode 100644 index 0000000..2858aa1 --- /dev/null +++ b/fanficdownloader/adapters/adapter_storiesonlinenet.py @@ -0,0 +1,422 @@ +# -*- coding: utf-8 -*- + +# Copyright 2013 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return StoriesOnlineNetAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class StoriesOnlineNetAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url + self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2].split(':')[0]) + if 'storyInfo' in self.story.getMetadata('storyId'): + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/s/'+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','strol') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%Y-%m-%d" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'storiesonline.net' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/s/1234 http://"+cls.getSiteDomain()+"/s/1234:4010" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain())+r"/s/\d+((:\d+)?(;\d+)?$|(:i)?$)?" + + ## Login seems to be reasonably standard across eFiction sites. + def needToLoginCheck(self, data): + if self.needToLogin \ + or 'Free Registration' in data \ + or "Invalid Password!" in data \ + or "Invalid User Name!" in data \ + or "Log In" in data \ + or "Access to unlinked chapters requires" in data: + self.needToLogin = True + return self.needToLogin + + def performLogin(self, url): + params = {} + + if self.password: + params['theusername'] = self.username + params['thepassword'] = self.password + else: + params['theusername'] = self.getConfig("username") + params['thepassword'] = self.getConfig("password") + params['rememberMe'] = '1' + params['page'] = 'http://'+self.getSiteDomain()+'/' + params['submit'] = 'Login' + + loginUrl = 'http://' + self.getSiteDomain() + '/login.php' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['theusername'])) + + d = self._fetchUrl(loginUrl, params,usecache=False) + + if "My Account" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['theusername'])) + raise exceptions.FailedToLogin(url,params['theusername']) + return False + else: + return True + + def use_pagecache(self): + ''' + adapters that will work with the page cache need to implement + this and change it to True. + ''' + return True + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url + logger.debug("URL: "+url) + + self.needToLogin = False + try: + data = self._fetchUrl(url+":i") + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + elif e.code == 401: + self.needToLogin = True + data = '' + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url+":i",usecache=False) + + 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.") + elif "Error! The story you're trying to access is being filtered by your choice of contents filtering." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Error! The story you're trying to access is being filtered by your choice of contents filtering.") + + # 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('h1') + self.story.setMetadata('title',stripHTML(a)) + + notice = soup.find('div', {'class' : 'notice'}) + if notice: + self.story.setMetadata('notice',unicode(notice)) + + # Find authorid and URL from... author url. + for a in soup.findAll('a', href=re.compile(r"/a/\w+")): + self.story.addToList('authorId',a['href'].split('/')[2]) + self.story.addToList('authorUrl','http://'+self.host+a['href']) + self.story.addToList('author',stripHTML(a).replace("'s Page","")) + + # Find the chapters: + chapters = soup.findAll('a', href=re.compile(r'^/s/'+self.story.getMetadata('storyId')+":\d+$")) + if len(chapters) != 0: + for chapter in chapters: + # just in case there's tags, like <i> in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+chapter['href'])) + else: + self.chapterUrls.append((self.story.getMetadata('title'),'http://'+self.host+'/s/'+self.story.getMetadata('storyId'))) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + # surprisingly, the detailed page does not give enough details, so go to author's page + page=0 + i=0 + while i == 0: + asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getList('authorUrl')[0]+"/"+str(page))) + + a = asoup.findAll('td', {'class' : 'lc2'}) + for lc2 in a: + if lc2.find('a', href=re.compile(r'^/s/'+self.story.getMetadata('storyId'))): + i=1 + break + if a[len(a)-1] == lc2: + page=page+1 + + for cat in lc2.findAll('div', {'class' : 'typediv'}): + self.story.addToList('genre',cat.text) + + # in lieu of word count. + self.story.setMetadata('size', lc2.findNext('td', {'class' : 'num'}).text) + + score = lc2.findNext('th', {'class' : 'num'}).text + if score != '-': + self.story.setMetadata('score', score) + + lc4 = lc2.findNext('td', {'class' : 'lc4'}) + desc = lc4.contents[0] + + try: + a = lc4.find('a', href=re.compile(r"/series/\d+/.*")) + if a: + # if there's a number after the series name, series_contents is a two element list: + # [<a href="...">Title</a>, u' (2)'] + series_contents = a.parent.contents + i = 0 if len(series_contents) == 1 else series_contents[1].strip(' ()') + seriesUrl = 'http://'+self.host+a['href'] + self.story.setMetadata('seriesUrl',seriesUrl) + series_name = stripHTML(a) + logger.debug("Series name= %s" % series_name) + series_soup = bs.BeautifulSoup(self._fetchUrl(seriesUrl)) + if series_soup: + logger.debug("Retrieving Series - looking for name") + series_name = series_soup.find('span', {'id' : 'ptitle'}).text.partition(' — ')[0] + logger.debug("Series name: '{0}'".format(series_name)) + self.setSeries(series_name, i) + desc = lc4.contents[2] + # Check if series is in a universe + universe_url = self.story.getList('authorUrl')[0] + "&type=uni" + universes_soup = bs.BeautifulSoup(self._fetchUrl(universe_url) ) + logger.debug("Universe url='{0}'".format(universe_url)) + if universes_soup: + universes = universes_soup.findAll('div', {'class' : 'ser-box'}) + logger.debug("Number of Universes: %d" % len(universes)) + for universe in universes: + logger.debug("universe.find('a')={0}".format(universe.find('a'))) + # The universe id is in an "a" tag that has an id but nothing else. It is the first tag. + # The id is prefixed with the letter "u". + universe_id = universe.find('a')['id'][1:] + logger.debug("universe_id='%s'" % universe_id) + universe_name = universe.find('div', {'class' : 'ser-name'}).text.partition(' ')[2] + logger.debug("universe_name='%s'" % universe_name) + # If there is link to the story, we have the right universe + story_a = universe.find('a', href=re.compile('/s/'+self.story.getMetadata('storyId'))) + if story_a: + logger.debug("Story is in a series that is in a universe! The universe is '%s'" % universe_name) + self.story.setMetadata("universe", universe_name) + self.story.setMetadata('universeUrl','http://'+self.host+ '/library/universe.php?id=' + universe_id) + break + else: + logger.debug("No universe page") + except: + pass + try: + a = lc4.find('a', href=re.compile(r"/universe/\d+/.*")) + logger.debug("Looking for universe - a='{0}'".format(a)) + if a: + self.story.setMetadata("universe",stripHTML(a)) + desc = lc4.contents[2] + # Assumed only one universe, but it does have a URL--use universeHTML + universe_name = stripHTML(a) + universeUrl = 'http://'+self.host+a['href'] + logger.debug("Retrieving Universe - about to get page - universeUrl='{0}".format(universeUrl)) + universe_soup = bs.BeautifulSoup(self._fetchUrl(universeUrl)) + logger.debug("Retrieving Universe - have page") + if universe_soup: + logger.debug("Retrieving Universe - looking for name") + universe_name = universe_soup.find('h1', {'id' : 'ptitle'}).text.partition(' —')[0] + logger.debug("Universes name: '{0}'".format(universe_name)) + + self.story.setMetadata('universeUrl',universeUrl) + logger.debug("Setting universe name: '{0}'".format(universe_name)) + self.story.setMetadata('universe',universe_name) + if self.getConfig("universe_as_series"): + self.setSeries(universe_name, 0) + self.story.setMetadata('seriesUrl',universeUrl) + else: + logger.debug("Do not have a universe") + except: + pass + + self.setDescription('http://'+self.host+'/s/'+self.story.getMetadata('storyId'),desc) + + for b in lc4.findAll('b'): + #logger.debug('Getting metadata: "%s"' % b) + label = b.text + if label in ['Posted:', 'Concluded:', 'Updated:']: + value = b.findNext('noscript').text + #logger.debug('Have a date field label: "%s", value: "%s"' % (label, value)) + else: + value = b.nextSibling + #logger.debug('label: "%s", value: "%s"' % (label, value)) + + if 'Sex' in label: + self.story.setMetadata('rating', value) + + if 'Tags' in label or 'Codes' in label: + for code in re.split(r'\s*,\s*', value.strip()): + self.story.addToList('sitetags',code) + + if 'Posted' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + if 'Concluded' in label: + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label: + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) +# + status = lc4.find('span', {'class' : 'ab'}) + if status != None: + self.story.setMetadata('status', 'In-Progress') + if "Last Activity" in status.text: + # date is passed as a timestamp and converted in JS. + value = status.findNext('noscript').text + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + else: + self.story.setMetadata('status', 'Completed') + + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + # some big chapters are split over several pages + pager = div.find('span', {'class' : 'pager'}) + if pager != None: + a = pager.previousSibling + while a != None: + logger.debug("before pager: {0}".format(a)) + b = a.previousSibling + a.extract() + a = b + + urls=pager.findAll('a') + urls=urls[:len(urls)-1] + pager.extract() + div.contents = div.contents[2:] +# logger.debug(div) + + for ur in urls: + soup = bs.BeautifulSoup(self._fetchUrl("http://"+self.getSiteDomain()+ur['href']), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div1 = soup.find('div', {'id' : 'story'}) + + # Find the "Continues" marker on the current page and remove everything after that. + continues = div.find('span', {'class' : 'conTag'}) + if continues != None: + while continues != None: +# logger.debug("removing end: {0}".format(continues)) + b = continues.nextSibling + continues.extract() + continues = b + + # Find the "Continued" marker and delete everything before that + continued = div1.find('span', {'class' : 'conTag'}) + if continued != None: + a = continued.previousSibling + while a != None: +# logger.debug("before conTag: {0}".format(a)) + b = a.previousSibling + a.extract() + a = b + # Remove the pager from the end if this is the last page + endPager = div1.find('span', {'class' : 'pager'}) + if endPager != None: + b = endPager.nextSibling + while endPager != None: + logger.debug("removing end: {0}".format(endPager)) + b = endPager.nextSibling + endPager.extract() + endPager = b + div1.contents = div1.contents[:len(div1) - 2] +# logger.debug("after removing pager: {0}".format(div1)) + for tag in div1.contents[2:]: + div.append(tag) + + # If it is a chapter, there are dates at the start for when it was posted or modified. These plus + # everything before them can be discarded. + postedDates = div.findAll('div', {'class' : 'date'}) + if postedDates: + a = postedDates[0].previousSibling + while a != None: +# logger.debug("before dates: {0}".format(a)) + b = a.previousSibling + a.extract() + a = b + for a in div.findAll('div', {'class' : 'date'}): + a.extract() + + # For single chapter stories, there is a copyright statement. Remove this and everything + # before it. + copy = div.find('h4', {'class': 'copy'}) + while copy != None: +# logger.debug("before copyright: {0}".format(copy)) + b = copy.previousSibling + copy.extract() + copy = b + + # For a story or the last chapter, remove voting form and the in library box + a = div.find('div', {'id' : 'vote-form'}) + if a != None: + a.extract() + a = div.find('div', {'id' : 'b-man-div'}) + if a != None: + a.extract() + + # Kill the "The End" header and everything after it. + a = div.find(['h2', 'h3'], {'class' : 'end'}) + logger.debug("Chapter end= '{0}'".format(a)) + while a != None: + b = a.nextSibling + a.extract() + a=b + + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_tenhawkpresentscom.py b/fanficdownloader/adapters/adapter_tenhawkpresentscom.py new file mode 100644 index 0000000..634adfe --- /dev/null +++ b/fanficdownloader/adapters/adapter_tenhawkpresentscom.py @@ -0,0 +1,257 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +class TenhawkPresentsComSiteAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + self.story.setMetadata('siteabbrev','thpc') + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId')) + self.dateformat = "%b %d %Y" + + + @staticmethod + def getSiteDomain(): + return 'fanfiction.tenhawkpresents.com' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$" + + def use_pagecache(self): + ''' + adapters that will work with the page cache need to implement + this and change it to True. + ''' + return True + + 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' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['penname'])) + + d = self._fetchUrl(loginUrl, params) + + if "Member Account" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['penname'])) + raise exceptions.FailedToLogin(url,params['penname']) + return False + else: + return True + + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + addurl = "&ageconsent=ok&warning=3" + else: + addurl="" + + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + addurl = "&ageconsent=ok&warning=4" + url = self.url+'&index=1'+addurl + logger.debug("Changing URL: "+url) + self.performLogin(url) + data = self._fetchUrl(url,usecache=False) + + if "This story contains mature content which may include violence, sexual situations, and coarse language" in data: + 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) + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId'))) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href']) + self.story.setMetadata('author',a.string) + + # Find the chapters: + for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")): + # just in case there's tags, like <i> in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href']+addurl)) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + 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')) + catstext = [cat.string for cat in cats] + for cat in catstext: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + charstext = [char.string for char in chars] + for char in charstext: + self.story.addToList('characters',char.string) + + if 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class')) + genrestext = [genre.string for genre in genres] + self.genre = ', '.join(genrestext) + for genre in genrestext: + self.story.addToList('genre',genre.string) + + if 'Completed' in label: + if 'Yes' in value: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label: + # there's a stray [ at the end. + #value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + try: + # Find Series name from series URL. + a = soup.find('a', href=re.compile(r"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)) + storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + span = soup.find('div', {'id' : 'story'}) + + if None == span: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,span) + +def getClass(): + return TenhawkPresentsComSiteAdapter + diff --git a/fanficdownloader/adapters/adapter_test1.py b/fanficdownloader/adapters/adapter_test1.py new file mode 100644 index 0000000..4bf8525 --- /dev/null +++ b/fanficdownloader/adapters/adapter_test1.py @@ -0,0 +1,367 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import datetime +import time +import logging +logger = logging.getLogger(__name__) + +from .. import exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +class TestSiteAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + self.story.setMetadata('siteabbrev','tst1') + self.crazystring = u" crazy tests:[bare amp(&) quote(') amp(&) gt(>) lt(<) ATnT(AT&T) pound(£)]" + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + self.username='' + self.is_adult=False + + @staticmethod + def getSiteDomain(): + return 'test1.com' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"?sid=1234" + + def getSiteURLPattern(self): + return BaseSiteAdapter.getSiteURLPattern(self)+r'/?\?sid=\d+$' + + def extractChapterUrlsAndMetadata(self): + idstr = self.story.getMetadata('storyId') + idnum = int(idstr) + self.do_sleep() + + if idnum >= 1000: + logger.warn("storyId:%s - Custom INI data will be used."%idstr) + + sections = ['teststory:%s'%idstr,'teststory:defaults'] + #print("self.get_config_list(sections,'valid_entries'):%s"%self.get_config_list(sections,'valid_entries')) + for key in self.get_config_list(sections,'valid_entries'): + if key.endswith("_list"): + nkey = key[:-len("_list")] + #print("addList:%s"%(nkey)) + for val in self.get_config_list(sections,key): + #print("addList:%s->%s"%(nkey,val)) + self.story.addToList(nkey,val.decode('utf-8').replace('{{storyId}}',idstr)) + else: + # Special cases: + if key in ['datePublished','dateUpdated']: + self.story.setMetadata(key,makeDate(self.get_config(sections,key),"%Y-%m-%d")) + else: + self.story.setMetadata(key,self.get_config(sections,key).decode('utf-8').replace('{{storyId}}',idstr)) + #print("set:%s->%s"%(key,self.story.getMetadata(key))) + + self.chapterUrls = [] + for (j,chap) in enumerate(self.get_config_list(sections,'chaptertitles'),start=1): + self.chapterUrls.append( (chap,self.url+"&chapter=%d"%j) ) + # self.chapterUrls = [(u'Prologue '+self.crazystring,self.url+"&chapter=1"), + # ('Chapter 1, Xenos on Cinnabar',self.url+"&chapter=2"), + # ] + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + return + + if idstr == '665' and not (self.is_adult or self.getConfig("is_adult")): + logger.warn("self.is_adult:%s"%self.is_adult) + raise exceptions.AdultCheckRequired(self.url) + + if idstr == '666': + raise exceptions.StoryDoesNotExist(self.url) + + if idstr.startswith('670'): + time.sleep(1.0) + + if idstr.startswith('671'): + time.sleep(1.0) + + if self.getConfig("username"): + self.username = self.getConfig("username") + + if idstr == '668' and self.username != "Me" : + raise exceptions.FailedToLogin(self.url,self.username) + + if idstr == '664': + self.story.setMetadata(u'title',"Test Story Title "+idstr+self.crazystring) + self.story.setMetadata('author','Test Author aa bare amp(&) quote(') amp(&)') + else: + self.story.setMetadata(u'title',"Test Story Title "+idstr) + self.story.setMetadata('author','Test Author aa') + self.story.setMetadata('storyUrl',self.url) + 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")) + if idstr == '669': + self.story.setMetadata('dateUpdated',datetime.datetime.now()) + else: + self.story.setMetadata('dateUpdated',makeDate("1975-04-15","%Y-%m-%d")) + self.story.setMetadata('numWords','123456') + + if idnum % 2 == 1: + self.story.setMetadata('status','In-Progress') + else: + self.story.setMetadata('status','Completed') + + # greater than 10, no language or series. + if idnum < 10: + langs = { + 0:"English", + 1:"Russian", + 2:"French", + 3:"German", + } + self.story.setMetadata('language',langs[idnum%len(langs)]) + self.setSeries('The Great Test',idnum) + self.story.setMetadata('seriesUrl','http://test1.com?seriesid=1') + if idnum == 0: + self.setSeries("A Nook Hyphen Test "+self.story.getMetadata('dateCreated'),idnum) + self.story.setMetadata('seriesUrl','http://test1.com?seriesid=0') + + self.story.setMetadata('rating','Tweenie') + + if idstr == '673': + self.story.addToList('author','Author From List 1') + self.story.addToList('author','Author From List 2') + self.story.addToList('author','Author From List 3') + self.story.addToList('author','Author From List 4') + self.story.addToList('author','Author From List 5') + self.story.addToList('author','Author From List 6') + self.story.addToList('author','Author From List 7') + self.story.addToList('author','Author From List 8') + self.story.addToList('author','Author From List 9') + self.story.addToList('author','Author From List 0') + self.story.addToList('author','Author From List q') + self.story.addToList('author','Author From List w') + self.story.addToList('author','Author From List e') + self.story.addToList('author','Author From List r') + self.story.addToList('author','Author From List t') + self.story.addToList('author','Author From List y') + self.story.addToList('author','Author From List u') + self.story.addToList('author','Author From List i') + self.story.addToList('author','Author From List o') + + self.story.addToList('authorId','98765-1') + self.story.addToList('authorId','98765-2') + self.story.addToList('authorId','98765-3') + self.story.addToList('authorId','98765-4') + self.story.addToList('authorId','98765-5') + self.story.addToList('authorId','98765-6') + self.story.addToList('authorId','98765-7') + self.story.addToList('authorId','98765-8') + self.story.addToList('authorId','98765-9') + self.story.addToList('authorId','98765-0') + self.story.addToList('authorId','98765-q') + self.story.addToList('authorId','98765-w') + self.story.addToList('authorId','98765-e') + self.story.addToList('authorId','98765-r') + self.story.addToList('authorId','98765-t') + self.story.addToList('authorId','98765-y') + self.story.addToList('authorId','98765-u') + self.story.addToList('authorId','98765-i') + self.story.addToList('authorId','98765-o') + + self.story.addToList('authorUrl','http://author/url-1') + self.story.addToList('authorUrl','http://author/url-2') + self.story.addToList('authorUrl','http://author/url-3') + self.story.addToList('authorUrl','http://author/url-4') + self.story.addToList('authorUrl','http://author/url-5') + self.story.addToList('authorUrl','http://author/url-6') + self.story.addToList('authorUrl','http://author/url-7') + self.story.addToList('authorUrl','http://author/url-8') + self.story.addToList('authorUrl','http://author/url-9') + self.story.addToList('authorUrl','http://author/url-0') + self.story.addToList('authorUrl','http://author/url-q') + self.story.addToList('authorUrl','http://author/url-w') + self.story.addToList('authorUrl','http://author/url-e') + self.story.addToList('authorUrl','http://author/url-r') + self.story.addToList('authorUrl','http://author/url-t') + self.story.addToList('authorUrl','http://author/url-y') + self.story.addToList('authorUrl','http://author/url-u') + self.story.addToList('authorUrl','http://author/url-i') + self.story.addToList('authorUrl','http://author/url-o') + + self.story.addToList('category','Power Rangers') + self.story.addToList('category','SG-1') + self.story.addToList('genre','Porn') + self.story.addToList('genre','Drama') + else: + self.story.setMetadata('authorId','98765') + self.story.setMetadata('authorUrl','http://author/url') + + self.story.addToList('warnings','Swearing') + self.story.addToList('warnings','Violence') + + if idstr == '80': + self.story.addToList('category',u'Rizzoli & Isles') + self.story.addToList('characters','J. Rizzoli') + elif idstr == '81': + self.story.addToList('category',u'Pitch Perfect') + self.story.addToList('characters','Chloe B.') + elif idstr == '82': + self.story.addToList('characters','Henry (Once Upon a Time)') + self.story.addToList('category',u'Once Upon a Time (TV)') + elif idstr == '83': + self.story.addToList('category',u'Rizzoli & Isles') + self.story.addToList('characters','J. Rizzoli') + self.story.addToList('category',u'Pitch Perfect') + self.story.addToList('characters','Chloe B.') + self.story.addToList('ships','Chloe B. & J. Rizzoli') + elif idstr == '90': + self.story.setMetadata('characters','Henry (Once Upon a Time)') + self.story.setMetadata('category',u'Once Upon a Time (TV)') + else: + self.story.addToList('category','Harry Potter') + self.story.addToList('category','Furbie') + self.story.addToList('category','Crossover') + self.story.addToList('category',u'Puella Magi Madoka Magica/魔法少女まどか★マギカ') + self.story.addToList('category',u'Magical Girl Lyrical Nanoha') + self.story.addToList('category',u'Once Upon a Time (TV)') + self.story.addToList('characters','Bob Smith') + self.story.addToList('characters','George Johnson') + self.story.addToList('characters','Fred Smythe') + self.story.addToList('ships','Harry Potter/Ginny Weasley') + self.story.addToList('ships','Harry Potter/Ginny Weasley/Albus Dumbledore') + self.story.addToList('ships','Harry Potter & Hermione Granger') + + self.story.addToList('genre','Fantasy') + self.story.addToList('genre','Comedy') + self.story.addToList('genre','Sci-Fi') + self.story.addToList('genre','Noir') + + self.story.addToList('listX','xVal1') + self.story.addToList('listX','xVal2') + self.story.addToList('listX','xVal3') + self.story.addToList('listX','xVal4') + + self.story.addToList('listY','yVal1') + self.story.addToList('listY','yVal2') + self.story.addToList('listY','yVal3') + self.story.addToList('listY','yVal4') + + self.story.addToList('listZ','zVal1') + self.story.addToList('listZ','zVal2') + self.story.addToList('listZ','zVal3') + self.story.addToList('listZ','zVal4') + + self.story.setMetadata('metaA','98765') + self.story.setMetadata('metaB','01245') + self.story.setMetadata('metaC','The mighty metaC!') + + self.chapterUrls = [(u'Prologue '+self.crazystring,self.url+"&chapter=1"), + ('Chapter 1, Xenos on Cinnabar',self.url+"&chapter=2"), + ('Chapter 2, Sinmay on Kintikin',self.url+"&chapter=3"), + ('Chapter 3, Over Cinnabar',self.url+"&chapter=4"), + ('Chapter 4',self.url+"&chapter=5"), + ('Chapter 5',self.url+"&chapter=6"), + ('Chapter 6',self.url+"&chapter=7"), + ('Chapter 7',self.url+"&chapter=8"), + ('Chapter 8',self.url+"&chapter=9"), + #('Chapter 9',self.url+"&chapter=0"), + #('Chapter 0',self.url+"&chapter=a"), + #('Chapter a',self.url+"&chapter=b"), + #('Chapter b',self.url+"&chapter=c"), + #('Chapter c',self.url+"&chapter=d"), + #('Chapter d',self.url+"&chapter=e"), + #('Chapter e',self.url+"&chapter=f"), + #('Chapter f',self.url+"&chapter=g"), + #('Chapter g',self.url+"&chapter=h"), + #('Chapter h',self.url+"&chapter=i"), + #('Chapter i',self.url+"&chapter=j"), + #('Chapter j',self.url+"&chapter=k"), + #('Chapter k',self.url+"&chapter=l"), + #('Chapter l',self.url+"&chapter=m"), + #('Chapter m',self.url+"&chapter=n"), + #('Chapter n',self.url+"&chapter=o"), + ] + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + + def getChapterText(self, url): + logger.debug('Getting chapter text from: %s' % url) + self.do_sleep() + if self.story.getMetadata('storyId').startswith('670') or \ + self.story.getMetadata('storyId').startswith('672'): + time.sleep(1.0) + + if "chapter=1" in url : + text=u''' +<div> +<h3>Prologue</h3> +<p>This is a fake adapter for testing purposes. Different sid's will give different errors:</p> +<h4>Config(personal.ini)</h4> +<p>sid>=1000 will use custom test story data from your configuration(personal.ini)</p> +<p>Hard coded ids:</p> +<p>http://test1.com?sid=664 - Crazy string title</p> +<p>http://test1.com?sid=665 - raises AdultCheckRequired</p> +<p>http://test1.com?sid=666 - raises StoryDoesNotExist</p> +<p>http://test1.com?sid=667 - raises FailedToDownload on chapters 2+</p> +<p>http://test1.com?sid=668 - raises FailedToLogin unless username='Me'</p> +<p>http://test1.com?sid=669 - Succeeds with Updated Date=now</p> +<p>http://test1.com?sid=670 - Succeeds, but sleeps 2sec on each chapter</p> +<p>http://test1.com?sid=671 - Succeeds, but sleeps 2sec metadata only</p> +<p>http://test1.com?sid=672 - Succeeds, quick meta, sleeps 2sec chapters only</p> +<p>http://test1.com?sid=673 - Succeeds, multiple authors, extra categories, genres</p> +<p>http://test1.com?sid=0 - Succeeds, generates some text specifically for testing hyphenation problems with Nook STR/STRwG</p> +<p>Odd sid's will be In-Progress, evens complete. sid<10 will be assigned one of four languages and included in a series.</p> +</div> +''' + elif self.story.getMetadata('storyId') == '0': + text=u'''<div> +<h3>45. Pronglet Returns to Hogwarts: Chapter 7</h3> +<br /> + eyes… but I’m not convinced we should automatically<br /> +<br /><br /> +<b>Thanks to the latest to recommend me: Alastor</b><br /> +<br /><br /> + “Sure, invite her along. Does she have children?”<br /> +<br /> +</div> +''' + else: + if self.story.getMetadata('storyId') == '667': + raise exceptions.FailedToDownload("Error downloading Chapter: %s!" % url) + + text=u''' +<div> +<h3 extra="value">Chapter title from site</h3> +<p>Timestamp:'''+datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")+'''</p> +<p>Lorem '''+self.crazystring+u''' <i>italics</i>, <b>bold</b>, <u>underline</u> consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p> +br breaks<br><br> +Puella Magi Madoka Magica/魔法少女まどか★マギカ +<!-- a href="http://code.google.com/p/fanficdownloader/wiki/FanFictionDownLoaderPluginWithReadingList" title="Tilt-a-Whirl by Jim & Sarah, on Flickr"><img src="http://i.imgur.com/bo8eD.png"></a --><br/> +br breaks<br><br> +Don't—e;ver—d;o—that—a;gain, 法 é +<hr> +horizontal rules +<hr size=1 noshade> +<p>"Lorem ipsum dolor sit amet", consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore--et dolore magna aliqua. 'Ut enim ad minim veniam', quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p> +<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p> +</div> +''' + soup = self.make_soup(text) + return self.utf8FromSoup(url,soup) + +def getClass(): + return TestSiteAdapter + diff --git a/fanficdownloader/adapters/adapter_thealphagatecom.py b/fanficdownloader/adapters/adapter_thealphagatecom.py new file mode 100644 index 0000000..4420616 --- /dev/null +++ b/fanficdownloader/adapters/adapter_thealphagatecom.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- + +# Copyright 2012 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return TheAlphaGateComAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class TheAlphaGateComAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','tag') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%d %b %Y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'www.thealphagate.com' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$" + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1' + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href']) + self.story.setMetadata('author',a.string) + + # Find the chapters: + for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")): + # just in case there's tags, like <i> in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href'])) + + 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 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) + for genre in genres: + self.story.addToList('genre',genre.string) + + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX + 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)) + storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_thehexfilesnet.py b/fanficdownloader/adapters/adapter_thehexfilesnet.py new file mode 100644 index 0000000..1316b08 --- /dev/null +++ b/fanficdownloader/adapters/adapter_thehexfilesnet.py @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return TheHexFilesNetAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class TheHexFilesNetAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','thf') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%Y.%m.%d" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'thehexfiles.net' + + @classmethod + def getAcceptDomains(cls): + return ['www.thehexfiles.net','thehexfiles.net'] + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://")+"(www\.)?"+re.escape(self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$" + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href']) + self.story.setMetadata('author',stripHTML(a)) + asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl'))) + + try: + # in case link points somewhere other than the first chapter + a = soup.findAll('option')[1]['value'] + self.story.setMetadata('storyId',a.split('=',)[1]) + url = 'http://'+self.host+'/'+a + soup = bs.BeautifulSoup(self._fetchUrl(url)) + except: + pass + + for info in asoup.findAll('table', {'cellspacing' : '4'}): + a = info.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + if a != None: + self.story.setMetadata('title',stripHTML(a)) + break + + + # Find the chapters: + chapters=soup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+&i=1$')) + if len(chapters) == 0: + self.chapterUrls.append((self.story.getMetadata('title'),url)) + else: + for chapter in chapters: + # just in case there's tags, like <i> in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href'])) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + + cats = info.findAll('a',href=re.compile('categories.php')) + for cat in cats: + self.story.addToList('category',cat.string) + + words = info.find(text=re.compile('Words:')).split('|')[1].split(': ')[1] + self.story.setMetadata('numWords', words) + + comp = info.find('span', {'class' : 'completed'}).string.split(': ')[1] + if 'Yes' in comp: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + summary = info.find('td', {'class' : 'summary'}) + summary.name='div' # change td to div so it doesn't mess up the display when using table titlepage. + self.setDescription(url,summary) + + rating=stripHTML(info.find('td', {'align' : 'left'})).split('(')[1].split(')')[0] + self.story.setMetadata('rating', rating) + + labels = info.findAll('td', {'width' : '10%'}) + values = info.findAll('td', {'width' : '40%'}) + for i in range(0,len(labels)): + value = stripHTML(values[i]) + label = stripHTML(labels[i]) + + if 'Genres' in label: + genres = value.split(', ') + for genre in genres: + if genre != 'none': + self.story.addToList('genre',genre) + + + if 'Warnings' in label: + warnings = value.split(', ') + for warning in warnings: + if warning != 'none': + self.story.addToList('warnings',warning) + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label: + # there's a stray [ at the end. + #value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr','img')) # otherwise soup eats the br/hr tags. + + if None == soup: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + # Ugh. chapter html doesn't haven't anything useful around it to demarcate. + for a in soup.findAll('table'): + a.extract() + + for a in soup.findAll('head'): + a.extract() + + html = soup.find('html') + html.name='div' + + return self.utf8FromSoup(url,soup) diff --git a/fanficdownloader/adapters/adapter_thehookupzonenet.py b/fanficdownloader/adapters/adapter_thehookupzonenet.py new file mode 100644 index 0000000..2b6c52e --- /dev/null +++ b/fanficdownloader/adapters/adapter_thehookupzonenet.py @@ -0,0 +1,309 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +# By virtue of being recent and requiring both is_adult and user/pass, +# adapter_fanficcastletvnet.py is the best choice for learning to +# write adapters--especially for sites that use the eFiction system. +# Most sites that have ".../viewstory.php?sid=123" in the story URL +# are eFiction. + +# For non-eFiction sites, it can be considerably more complex, but +# this is still a good starting point. + +# In general an 'adapter' needs to do these five things: + +# - 'Register' correctly with the downloader +# - Site Login (if needed) +# - 'Are you adult?' check (if needed--some do one, some the other, some both) +# - Grab the chapter list +# - Grab the story meta-data (some (non-eFiction) adapters have to get it from the author page) +# - Grab the chapter texts + +# Search for XXX comments--that's where things are most likely to need changing. + +# This function is called by the downloader in all adapter_*.py files +# in this dir to register the adapter class. So it needs to be +# updated to reflect the class below it. That, plus getSiteDomain() +# take care of 'Registering'. +def getClass(): + return TheHookupZoneNetAdapter # XXX + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class TheHookupZoneNetAdapter(BaseSiteAdapter): # XXX + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + # XXX Most sites don't have the /fanfic part. Replace all to remove it usually. + self._setURL('http://' + self.getSiteDomain() + '/CriminalMinds/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','thupz') # 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 + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'thehookupzone.net' # XXX + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/CriminalMinds/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/CriminalMinds/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() + '/CriminalMinds/user.php?action=login' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['penname'])) + + d = self._fetchUrl(loginUrl, params) + + if "Member Account" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['penname'])) + raise exceptions.FailedToLogin(url,params['penname']) + return False + else: + return True + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + # Weirdly, different sites use different warning numbers. + # If the title search below fails, there's a good chance + # you need a different number. print data at that point + # and see what the 'click here to continue' url says. + addurl = "&ageconsent=ok&warning=4" # XXX + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url) + + # 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 "Age Consent Required" in data: # XXX + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/CriminalMinds/'+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+'/CriminalMinds/'+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')) + catstext = [cat.string for cat in cats] + for cat in catstext: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + charstext = [char.string for char in chars] + for char in charstext: + self.story.addToList('characters',char.string) + + ## Not all sites use Genre, but there's no harm to + ## leaving it in. Check to make sure the type_id number + ## is correct, though--it's site specific. + if 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX + genrestext = [genre.string for genre in genres] + self.genre = ', '.join(genrestext) + for genre in genrestext: + self.story.addToList('genre',genre.string) + + ## Not all sites use Warnings, but there's no harm to + ## leaving it in. Check to make sure the type_id number + ## is correct, though--it's site specific. + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX + warningstext = [warning.string for warning in warnings] + self.warning = ', '.join(warningstext) + for warning in warningstext: + self.story.addToList('warnings',warning.string) + + if 'Completed' in label: + if 'Yes' in value: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label: + # there's a stray [ at the end. + #value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + try: + # Find Series name from series URL. + a = soup.find('a', href=re.compile(r"viewseries.php\?seriesid=\d+")) + series_name = a.string + series_url = 'http://'+self.host+'/CriminalMinds/'+a['href'] + + # use BeautifulSoup HTML parser to make everything easier to find. + seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url)) + storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_themaplebookshelf.py b/fanficdownloader/adapters/adapter_themaplebookshelf.py new file mode 100644 index 0000000..1d55deb --- /dev/null +++ b/fanficdownloader/adapters/adapter_themaplebookshelf.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- + +# Copyright 2014 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +from base_efiction_adapter import BaseEfictionAdapter + +class TheMapleBookshelfComSiteAdapter(BaseEfictionAdapter): + + @staticmethod + def getSiteDomain(): + return 'themaplebookshelf.com' + + @classmethod + def getPathToArchive(self): + return '/Literati' + + @classmethod + def getSiteAbbrev(seluuf): + return 'maplebook' + + @classmethod + def getDateFormat(self): + return "%b %d, %Y" + +def getClass(): + return TheMapleBookshelfComSiteAdapter diff --git a/fanficdownloader/adapters/adapter_themasquenet.py b/fanficdownloader/adapters/adapter_themasquenet.py new file mode 100644 index 0000000..db65ee6 --- /dev/null +++ b/fanficdownloader/adapters/adapter_themasquenet.py @@ -0,0 +1,274 @@ +# -*- coding: utf-8 -*- + +# Copyright 2012 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return TheMasqueNetAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class TheMasqueNetAdapter(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]) + + + if self.parsedUrl.path.split('/',)[1] == 'wiktt': + self.story.addToList("category","Harry Potter") + self.section='/wiktt/efiction/' + self.dateformat = "%m/%d/%Y" + else: + self.story.addToList("category","Originals") + self.section='/efiction/' + self.dateformat = "%b %d, %Y" + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + self.section + 'viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','msq') + + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'themasque.net' + + @classmethod + def getSiteExampleURLs(cls): + return "http://themasque.net/wiktt/efiction/viewstory.php?sid=1234 http://themasque.net/efiction/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain())+"(/wiktt)?/efiction"+re.escape("/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() + self.section + 'user.php?action=login' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['penname'])) + + d = self._fetchUrl(loginUrl, params) + + if "Member Account" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['penname'])) + raise exceptions.FailedToLogin(url,params['penname']) + return False + else: + return True + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + # Weirdly, different sites use different warning numbers. + # If the title search below fails, there's a good chance + # you need a different number. print data at that point + # and see what the 'click here to continue' url says. + addurl = "&ageconsent=ok&warning=4" + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url) + + m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href']) + self.story.setMetadata('author',a.string) + + # Find the chapters: + for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")): + # just in case there's tags, like <i> in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host + self.section + chapter['href']+addurl)) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + # eFiction sites don't help us out a lot with their meta data + # formating, so it's a little ugly. + + # utility method + def defaultGetattr(d,k): + try: + return d[k] + except: + return "" + +# summary, rated, word count, categories, characters, genre, warnings, completed, published, updated, seires + + # <span class="label">Rated:</span> NC-17<br /> etc + labels = soup.findAll('span',{'class':'label'}) + for labelspan in labels: + value = labelspan.nextSibling + label = labelspan.text + + 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 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) + for genre in genres: + self.story.addToList('genre',genre.string) + + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=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)) + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_thepetulantpoetesscom.py b/fanficdownloader/adapters/adapter_thepetulantpoetesscom.py new file mode 100644 index 0000000..b35eaa5 --- /dev/null +++ b/fanficdownloader/adapters/adapter_thepetulantpoetesscom.py @@ -0,0 +1,242 @@ +# -*- coding: utf-8 -*- + +# Copyright 2012 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return ThePetulantPoetessComAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class ThePetulantPoetessComAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId') +'&i=1') + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','tpp') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%Y/%m/%d" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'www.thepetulantpoetess.com' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$" + + ## Login seems to be reasonably standard across eFiction sites. + def needToLoginCheck(self, data): + if 'You must be a member to read this story.' in data \ + or "The Ministry of Magic does not have a record of that password. " 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' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['penname'])) + + d = self._fetchUrl(loginUrl, params) + + if "My Account Page" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['penname'])) + raise exceptions.FailedToLogin(url,params['penname']) + return False + else: + return True + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + # 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: + chapters=soup.find('select', {'name' : 'sid'}) + if chapters == None: + self.chapterUrls.append((self.story.getMetadata('title'),url)) + else: + for chapter in chapters.findAll('option', value=re.compile(r"viewstory.php\?sid=\d+&i=1")): + # just in case there's tags, like <i> in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['value'])) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + # make sure that the story id is from the first chapter + self.story.setMetadata('storyId',self.chapterUrls[0][1].split('=')[1].split('&')[0]) + + #locate the story on author's page + index = 1 + found = 0 + while found == 0: + asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')+"&page="+str(index))) + + for info in asoup.findAll('td', {'class' : 'highlightcolor1'}): + a = info.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + if a != None: + self.story.setMetadata('title',stripHTML(a)) + found = 1 + break + index=index+1 + + # extract metadata + b=info.find('b') + b.find('a').extract() + self.story.setMetadata('rating', b.text.split('[')[1].split(']')[0]) + + info = info.findNext('td', {'colspan' : '2'}) + for label in info.findAll('b'): + value = label.nextSibling + + if "Category" in label.text: + for cat in info.findAll('a'): + self.story.addToList('category',cat.string) + + if "Characters" in label.text: + for char in value.split(', '): + self.story.addToList('characters',char) + + if "Genres" in label.text: + for genre in value.split(', '): + if "General" not in genre: + self.story.addToList('genre',genre) + + if "Warnings" in label.text: + for warning in value.split(', '): + if "none" not in warning.lower(): + self.story.addToList('warnings',warning) + + + info = info.findNext('td', {'class' : 'tblborder'}) + info.find('b').extract() + self.setDescription(url,info) + + info = info.findNext('td', {'class' : 'highlightcolor2'}) + for label in info.findAll('b'): + value = label.nextSibling + + if "Completed" in label.text: + if 'Yes' in value: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Published' in label.text: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label.text: + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + if 'Word Count' in label.text: + self.story.setMetadata('numWords', value) + + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.findAll('table')[2].findAll('td')[1] + for a in div.findAll('div'): + a.extract() + div.name='div' + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_thequidditchpitchorg.py b/fanficdownloader/adapters/adapter_thequidditchpitchorg.py new file mode 100644 index 0000000..68401bf --- /dev/null +++ b/fanficdownloader/adapters/adapter_thequidditchpitchorg.py @@ -0,0 +1,292 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +# This function is called by the downloader in all adapter_*.py files +# in this dir to register the adapter class. So it needs to be +# updated to reflect the class below it. That, plus getSiteDomain() +# take care of 'Registering'. +def getClass(): + return TheQuidditchPitchOrgAdapter # XXX + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class TheQuidditchPitchOrgAdapter(BaseSiteAdapter): # XXX + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + # XXX Most sites don't have the part. Replace all to remove it usually. + 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','tqdpch') # 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 + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'thequidditchpitch.org' # XXX + + @classmethod + def getAcceptDomains(cls): + return ['www.thequidditchpitch.org','thequidditchpitch.org'] + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://")+"(www\.)?"+re.escape(self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$" + + ## Login seems to be reasonably standard across eFiction sites. + def needToLoginCheck(self, data): + if 'Registered Users Only - Not suitable for readers under the age of legal consent in their country.' 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' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['penname'])) + + d = self._fetchUrl(loginUrl, params) + + if "Member Account" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['penname'])) + raise exceptions.FailedToLogin(url,params['penname']) + return False + else: + return True + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + # Weirdly, different sites use different warning numbers. + # If the title search below fails, there's a good chance + # you need a different number. print data at that point + # and see what the 'click here to continue' url says. + addurl = "&ageconsent=ok&warning=4" # XXX + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url) + + # 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 ("Not suitable for readers under the age of legal consent in their country." in data \ + or "Not suitable for readers under 16 yrs. \r\nStories may contain violence, slight nudity, and/or sexual situations." in data ) \ + and not (self.is_adult or self.getConfig("is_adult")): # XXX + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + #print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId'))) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href']) + self.story.setMetadata('author',a.string) + + # Find the chapters: + for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")): + # just in case there's tags, like <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')) + catstext = [cat.string for cat in cats] + for cat in catstext: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + charstext = [char.string for char in chars] + for char in charstext: + self.story.addToList('characters',char.string) + + if 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) # XXX + genrestext = [genre.string for genre in genres] + self.genre = ', '.join(genrestext) + for genre in genrestext: + 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')) # XXX + warningstext = [warning.string for warning in warnings] + self.warning = ', '.join(warningstext) + for warning in warningstext: + self.story.addToList('warnings',warning.string) + + if 'Completed' in label: + if 'Yes' in value: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label: + # there's a stray [ at the end. + #value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + try: + # Find Series name from series URL. + a = soup.find('a', href=re.compile(r"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)) + storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + # span? Really? span? Yeah... I don't think so. + div = soup.find('span', {'style' : 'font-size: 100%;'}) + div.name='div' + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_tokrafandomnetcom.py b/fanficdownloader/adapters/adapter_tokrafandomnetcom.py new file mode 100644 index 0000000..1d3a927 --- /dev/null +++ b/fanficdownloader/adapters/adapter_tokrafandomnetcom.py @@ -0,0 +1,238 @@ +# -*- coding: utf-8 -*- + +# Copyright 2013 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return TokraFandomnetComAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class TokraFandomnetComAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','tokra') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%m/%d/%Y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. But it + # doesn't matter too much anymore. + return 'tokra.fandomnet.com' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$" + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + 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=3" + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + #print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href']) + self.story.setMetadata('author',a.string) + + # Rating + rate = stripHTML(soup.find('div',{'id':'pagetitle'})) + rate = rate[rate.rindex('[')+1:rate.rindex(']')] + self.story.setMetadata('rating', rate) + + # 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. + + metadiv = soup.find('div',{'class':'content'}) + smalldiv = metadiv.find('div',{'class':'small'}) + + # tokra categories -> genre + # categories will be filled from ini. + genres = smalldiv.parent.findAll('a',href=re.compile(r'browse.php\?type=categories')) + for genre in genres: + self.story.addToList('genre',genre.string) + + chars = smalldiv.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + for char in chars: + self.story.addToList('characters',char.string) + + metatext = stripHTML(smalldiv) + + if 'Completed: Yes' in metatext: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + wordstart=metatext.rindex('Word count:')+12 + words = metatext[wordstart:metatext.index(' ',wordstart)] + self.story.setMetadata('numWords', words) + + datesdiv = soup.find('div',{'class':'bottom'}) + dates = stripHTML(datesdiv).split() + # Published: 04/26/2011 Updated: 03/06/2013 + self.story.setMetadata('datePublished', makeDate(dates[1], self.dateformat)) + self.story.setMetadata('dateUpdated', makeDate(dates[3], 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) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # remove 'small' leaving only summary. + smalldiv.extract() + self.setDescription(url,metadiv) + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulSoup(self._fetchUrl(url)) + + div = soup.find('div', {'class' : 'content'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + # remove some decorations while keeping notes. + remove = div.find('div', {'id' : 'pagetitle'}) + remove.extract() + + for remove in div.findAll('div', {'class' : 'right'}): + remove.extract() + + for remove in div.findAll('div', {'class' : 'left'}): + remove.extract() + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_tolkienfanfiction.py b/fanficdownloader/adapters/adapter_tolkienfanfiction.py new file mode 100644 index 0000000..b964baf --- /dev/null +++ b/fanficdownloader/adapters/adapter_tolkienfanfiction.py @@ -0,0 +1,238 @@ +# -*- coding: utf-8 -*- + +""" +FFDL Adapter for TolkienFanFiction.com. + +Chapter URL: http://www.tolkienfanfiction.com/Story_Read_Chapter.php?CHid=1234 + Metadata + Link to Story URL [Index] + chapterTitle + storyTitle +Story URL: http://www.tolkienfanfiction.com/Story_Read_Head.php?STid=1034 + Metadata + Links to Chapter URLs + storyTitle + chapterTitle[s] + author + authorId + authorUrl + numChapters + wordCount + description/summary + rating TODO + genre TODO + Characters + Ages (specific) TODO +Search: http://www.tolkienfanfiction.com/Story_Chapter_Search.php?text=From+Wilderness+to+Cities+White&field=1&type=3&search=Search + Strategy + Search by exact phrase for styo + Metadata + dateUpdated + Parameters + field (field to search) + 1: title + 2: description + 3: chapter text + type (any, all or exact phrase) + 1: any + 2: all + 3: exact phrase + +""" +# Copyright 2014 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib +import urllib2 +import urlparse +import string + +from .. import BeautifulSoup as bs +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def _is_story_url(url): + return "Story_Read_Head.php" in url + +def _latinize(text): + """ + See http://stackoverflow.com/a/19114706/201318 + """ + src = u"áâäÉéêëíóôöúû" + tgt = u"aaaEeeeiooouu" + src_ord = [ord(char) for char in src] + translate_table = dict(zip(src_ord, tgt)) + return text.translate(translate_table) + +def _fix_broken_markup(html): + """Replaces invalid comment tags""" + if html.startswith("<CENTER>"): + logger.error("TolkienFanFiction.com couldn't handle this request: '%s'" % html) + html = re.sub("<!-.+?->", "", html) + return html + + +class TolkienFanfictionAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["ISO-8859-1", "Windows-1252"] + + self.story.setMetadata('siteabbrev','tolkien') + + self.dateformat = '%B %d, %Y' + + self._normalizeURL(url) + + def _normalizeURL(self, url): + if _is_story_url(url): + self.story.setMetadata('storyId', re.compile(self.getSiteURLPattern()).match(url).group('storyId')) + self._setURL('http://' + self.getSiteDomain() + '/Story_Read_Head.php?STid=' + self.story.getMetadata('storyId')) + + @staticmethod + def getSiteDomain(): + return 'tolkienfanfiction.com' + + @classmethod + def getAcceptDomains(cls): + return ['tolkienfanfiction.com', 'www.tolkienfanfiction.com'] + + @classmethod + def getSiteExampleURLs(cls): + return 'http://www.tolkienfanfiction.com/Story_Read_Head.php?STid=1034 http://www.tolkienfanfiction.com/Story_Read_Chapter.php?CHid=4945' + + def getSiteURLPattern(self): + return r"http://(?:www.)?tolkienfanfiction.com/(?:Story_Read_Chapter\.php\?CH|Story_Read_Head\.php\?ST)id=(?P<storyId>[0-9]+)" + + def extractChapterUrlsAndMetadata(self): + + if not _is_story_url(self.url): + # Get the link to the index page + try: + chapterHtml = _fix_broken_markup(self._fetchUrl(self.url)) + chapterSoup = bs.BeautifulSoup(chapterHtml) + indexLink = chapterSoup.find("a", text="[Index]").parent + self._normalizeURL('http://' + self.getSiteDomain() + '/' + indexLink.get('href')) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + logger.debug("Determined index page: <%s>" % self.url) + + try: + indexHtml = _fix_broken_markup(self._fetchUrl(self.url)) + soup = bs.BeautifulSoup(indexHtml) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + # chapterUrls + for pfLink in soup.findAll("a", text='[PF] '): + chapterLink = pfLink.parent.findNext("a") + chapterTitle = chapterLink.string + if self.getConfig('strip_chapter_numeral'): + chapterTitle = re.sub("^\d+:", "", chapterTitle) + chapterUrl = 'http://' + self.host + '/' + chapterLink['href'] + self.chapterUrls.append((chapterTitle, chapterUrl)) + numChapters = len(self.chapterUrls) + self.story.setMetadata('numChapters', numChapters) + logger.debug('Number of Chapters: %s' % numChapters) + + # title + title = soup.find("table", "headertitle").find("tr").contents[1].string + logger.debug("Title: '%s'" % title) + self.story.setMetadata('title', title) + + # author + authorLink = soup.find("a", {"href":lambda x: x.startswith("Author_Profile.php")}) + authorName = authorLink.find("b").string + authorHref = authorLink['href'] + authorUrl = 'http:' + self.host + '/' + authorHref + authorId = authorHref[authorHref.index('=')+1:] + self.story.setMetadata('author', authorName) + self.story.setMetadata('authorId', authorId) + self.story.setMetadata('authorUrl', authorUrl) + logger.debug("Author: %s [%s] @ <%s>" % (authorId, authorName, authorUrl)) + + # numWords + numWordsMatch = re.search("Word Count: (\d+)<BR>", indexHtml) + if numWordsMatch: + numWords = numWordsMatch.group(1) + logger.debug('Number of words: %s' % numWords) + self.story.setMetadata('numWords', numWords) + + # description + description = soup.find("b", text="Description:").parent.nextSibling.nextSibling + self.story.setDescription(description) + logger.debug("Summary: '%s'" % description) + + # characters + characters = soup.find("b", text="Characters").parent.nextSibling.nextSibling.nextSibling + for character in characters.split(", "): + self.story.addToList('characters', character) + logger.debug("Characters: %s" % self.story.getMetadata('characters')) + + logger.debug('Title as `str`: ' + str(title)) + # For publication date we need to search + try: + queryString = urllib.urlencode(( + ('type', 3), + ('field', 1), + # need translate here for the weird accented letters + ('text', _latinize(title)), + ('search', 'Search'), + )) + searchUrl = 'http://%s/Story_Chapter_Search.php?%s' % (self.host, queryString) + logger.debug("Search URL: <%s>" % searchUrl) + searchHtml = _fix_broken_markup(self._fetchUrl(searchUrl)) + searchSoup = bs.BeautifulSoup(searchHtml) + date = searchSoup.find(text="Updated:").nextSibling.string + logger.debug("Last Updated: '%s'" % date) + self.story.setMetadata('dateUpdated', makeDate(date, self.dateformat)) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + def getChapterText(self, url): + + logger.debug('Downloading chapter <%s>' % url) + + time.sleep(0.5) + htmldata = _fix_broken_markup(self._fetchUrl(url)) + soup = bs.BeautifulSoup(htmldata) + + #strip comments from soup + [comment.extract() for comment in soup.findAll(text=lambda text:isinstance(text, bs.Comment))] + + # Strip redundant headings + [font.parent.extract() for font in soup.findAll("font", {"size": "4"})] + + # get story text + textDiv = soup.find("div", "text") + return self.utf8FromSoup(url, textDiv) + +def getClass(): + return TolkienFanfictionAdapter diff --git a/fanficdownloader/adapters/adapter_trekiverseorg.py b/fanficdownloader/adapters/adapter_trekiverseorg.py new file mode 100644 index 0000000..7b44068 --- /dev/null +++ b/fanficdownloader/adapters/adapter_trekiverseorg.py @@ -0,0 +1,325 @@ +# -*- coding: utf-8 -*- + +# Copyright 2013 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return TrekiverseOrgAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class TrekiverseOrgAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["iso-8859-1", + "Windows-1252"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + # normalized story URL. + self._setURL("http://"+self.getSiteDomain()\ + +"/efiction/viewstory.php?sid="+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','trkvs') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%d/%m/%Y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. + return 'trekiverse.org' + + @classmethod + def getAcceptDomains(cls): + return ['trekiverse.org','efiction.trekiverse.org'] + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/efiction/viewstory.php?sid=1234 http://efiction."+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return r'(http://trekiverse\.org/efiction/viewstory\.php\?sid=\d+|http://efiction\.trekiverse\.org/viewstory\.php\?sid=\d+)' + + ## Login seems to be reasonably standard across eFiction sites. + def needToLoginCheck(self, data): + if 'Registered Users Only' in data \ + or 'There is no such account on our website' in data \ + or "That password doesn't match the one in our database" in data: + return True + else: + return False + + def performLogin(self, url): + params = {} + + if self.password: + params['penname'] = self.username + params['password'] = self.password + else: + params['penname'] = self.getConfig("username") + params['password'] = self.getConfig("password") + params['cookiecheck'] = '1' + params['submit'] = 'Submit' + + loginUrl = 'http://' + self.getSiteDomain() + '/efiction/user.php?action=login' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['penname'])) + + d = self._fetchUrl(loginUrl, params) + + if "Member Account" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['penname'])) + raise exceptions.FailedToLogin(url,params['penname']) + return False + else: + return True + + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + # Weirdly, different sites use different warning numbers. + # If the title search below fails, there's a good chance + # you need a different number. print data at that point + # and see what the 'click here to continue' url says. + addurl = "&index=1&ageconsent=ok&warning=5" + else: + addurl="&index=1" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url) + + m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + + # Now go hunting for all the meta data and the chapter list. + + ## Title and author + a = soup.find('div', {'id' : 'pagetitle'}) + aut = a.find('a', href=re.compile(r"^viewuser\.php\?uid=")) + self.story.setMetadata('authorId',aut['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/efiction/'+aut['href']) + self.story.setMetadata('author',aut.string) + + ttl = a.find('a', href=re.compile(r'^viewstory.php\?sid=%s$'%self.story.getMetadata('storyId'))) + self.story.setMetadata('title',ttl.string) + + # Find the chapters: + outputdiv = soup.find('div', {'id':'output'}) + # (amp;)? because it should be &, but is escaped to & in URL. + # viewstory.php?sid=35&chapter=3 + chapters=outputdiv.findAll('a', href=re.compile(r'^viewstory.php\?sid=%s&(amp;)?chapter=\d+$'%self.story.getMetadata('storyId'))) + if len(chapters)==0: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: No php/html chapters found.") + if len(chapters)==1: + self.chapterUrls.append((self.story.getMetadata('title'),'http://'+self.host+'/efiction/'+chapters[0]['href'])) + else: + for chapter in chapters: + # just in case there's tags, like <i> in chapter titles. + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/efiction/'+chapter['href'])) + + 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. + + 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 value and not defaultGetattr(value,'class') == 'label': + svalue += str(value) + value = value.nextSibling + # sometimes poorly formated desc (<p> w/o </p>) leads + # to all labels being included. + svalue=svalue[:svalue.find('<span class="label">')] + 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')) + catstext = [cat.string for cat in cats] + for cat in catstext: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + charstext = [char.string for char in chars] + for char in charstext: + self.story.addToList('characters',char.string) + + if 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=9')) + genrestext = [genre.string for genre in genres] + self.genre = ', '.join(genrestext) + for genre in genrestext: + self.story.addToList('genre',genre.string) + + if 'Awards' in label: + awards = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=12')) + awardstext = [award.string for award in awards] + self.award = ', '.join(awardstext) + for award in awardstext: + self.story.addToList('awards',award.string) + + if 'Pairing' in label: + ships = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=10')) + shipstext = [ship.string for ship in ships] + self.ship = ', '.join(shipstext) + for ship in shipstext: + self.story.addToList('ships',ship.string) + + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=11')) + warningstext = [warning.string for warning in warnings] + self.warning = ', '.join(warningstext) + for warning in warningstext: + 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(value.strip(), "%d %b %Y")) + + if 'Updated' in label: + self.story.setMetadata('dateUpdated', makeDate(value.strip(), "%d %b %Y")) + + 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+'/efiction/'+a['href'] + + # use BeautifulSoup HTML parser to make everything easier to find. + seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url)) + storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulSoup(self._fetchUrl(url)) # 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) + + notesdiv = soup.find('div', {'class':'noteinfo'}) + if notesdiv != None: + div.insert(0,"<hr>") + div.insert(0,notesdiv) + div.insert(0,"<hr>") + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_tthfanficorg.py b/fanficdownloader/adapters/adapter_tthfanficorg.py new file mode 100644 index 0000000..84c06b2 --- /dev/null +++ b/fanficdownloader/adapters/adapter_tthfanficorg.py @@ -0,0 +1,310 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 +import time + +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + self.story.setMetadata('siteabbrev','tth') + self.dateformat = u"%d\u00a0%b\u00a0%y" #   becomes \u00a0 with bs4/html5lib. + self.is_adult=False + self.username = None + self.password = None + # get storyId from url--url validation guarantees query correct + m = re.match(self.getSiteURLPattern(),url) + if m: + self.story.setMetadata('storyId',m.group('id')) + + # normalized story URL. + self._setURL("http://"+self.getSiteDomain()\ + +"/Story-"+self.story.getMetadata('storyId')) + else: + raise exceptions.InvalidStoryURL(url, + self.getSiteDomain(), + self.getSiteExampleURLs()) + + @staticmethod + def getSiteDomain(): + return 'www.tthfanfic.org' + + @classmethod + def getSiteExampleURLs(cls): + return "http://www.tthfanfic.org/Story-1234 http://www.tthfanfic.org/Story-1234/Author+Story+Title.htm http://www.tthfanfic.org/T-99999999/Story-1234-1/Author+Story+Title.htm http://www.tthfanfic.org/story.php?no=12345" + + # http://www.tthfanfic.org/T-999999999999/Story-12345-1/Author+Story+Title.htm + # http://www.tthfanfic.org/Story-12345 + # http://www.tthfanfic.org/Story-12345/Author+Story+Title.htm + # http://www.tthfanfic.org/story.php?no=12345 + def getSiteURLPattern(self): + return r"http://www.tthfanfic.org(/(T-\d+/)?Story-|/story.php\?no=)(?P<id>\d+)(-\d+)?(/.*)?$" + + def use_pagecache(self): + ''' + adapters that will work with the page cache need to implement + this and change it to True. + ''' + return True + + # tth won't send you future updates if you aren't 'caught up' + # on the story. Login isn't required for F21, but logging in will + # mark stories you've downloaded as 'read' on tth. + def performLogin(self): + params = {} + + if self.password: + params['urealname'] = self.username + params['password'] = self.password + else: + params['urealname'] = self.getConfig("username") + params['password'] = self.getConfig("password") + params['loginsubmit'] = 'Login' + + if not params['password']: + return + + loginUrl = 'http://' + self.getSiteDomain() + '/login.php' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['urealname'])) + + ## need to pull empty login page first to get ctkn and + ## password name, which are BUSs +# <form method='post' action='/login.php' accept-charset="utf-8"> +# <input type='hidden' name='ctkn' value='4bdf761f5bea06bf4477072afcbd0f8d721d1a4f989c09945a9e87afb7a66de1'/> +# <input type='text' id='urealname' name='urealname' value=''/> +# <input type='password' id='password' name='6bb3fcd148d148629223690bf19733b8'/> +# <input type='submit' value='Login' name='loginsubmit'/> + soup = self.make_soup(self._fetchUrl(loginUrl)) + params['ctkn']=soup.find('input', {'name':'ctkn'})['value'] + params[soup.find('input', {'id':'password'})['name']] = params['password'] + + d = self._fetchUrl(loginUrl, params) + + if "Stories Published" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['urealname'])) + raise exceptions.FailedToLogin(self.url,params['urealname']) + return False + else: + return True + + def extractChapterUrlsAndMetadata(self): + # fetch the chapter. From that we will get almost all the + # metadata and chapter list + + url=self.url + logger.debug("URL: "+url) + + # tth won't send you future updates if you aren't 'caught up' + # on the story. Login isn't required for F21, but logging in will + # mark stories you've downloaded as 'read' on tth. + self.performLogin() + + # use BeautifulSoup HTML parser to make everything easier to find. + try: + data = self._fetchUrl(url) + #print("data:%s"%data) + soup = self.make_soup(data) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(url) + else: + raise e + + descurl = url + + if "<h2>Story Not Found</h2>" in data: + raise exceptions.StoryDoesNotExist(url) + + if self.is_adult or self.getConfig("is_adult"): + form = soup.find('form', {'id':'sitemaxratingform'}) + # if is_adult and rating isn't already set to FR21, set it so. + if not form.find('option',{'value':'5'}).get('selected'): + params={'ctkn':form.find('input', {'name':'ctkn'})['value'], + 'sitemaxrating':'5'} + logger.info("Attempting to get rating cookie for %s" % url) + data = self._postUrl("http://"+self.getSiteDomain()+'/setmaxrating.php',params) + # refetch story page. + ## XXX - needs cache invalidate? Or at least check that it this needs doing... + data = self._fetchUrl(url,usecache=False) + soup = self.make_soup(data) + + if "NOTE: This story is rated FR21 which is above your chosen filter level." in data: + raise exceptions.AdultCheckRequired(self.url) + + # http://www.tthfanfic.org/AuthorStories-3449/Greywizard.htm + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"^/AuthorStories-\d+")) + self.story.setMetadata('authorId',a['href'].split('/')[1].split('-')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+a['href']) + self.story.setMetadata('author',stripHTML(a)) + authorurl = 'http://'+self.host+a['href'] + + try: + # going to pull part of the meta data from *primary* author list page. + logger.debug("**AUTHOR** URL: "+authorurl) + authordata = self._fetchUrl(authorurl) + descurl=authorurl + authorsoup = self.make_soup(authordata) + # author can have several pages, scan until we find it. + while( not authorsoup.find('a', href=re.compile(r"^/Story-"+self.story.getMetadata('storyId'))) ): + nextarrow = authorsoup.find('a', {'class':'arrowf'}) + if not nextarrow: + ## if rating is set lower than story, it won't be + ## visible on author lists unless. The *story* is + ## visible via the url, just not the entry on + ## author list. + raise exceptions.AdultCheckRequired(self.url) + nextpage = 'http://'+self.host+nextarrow['href'] + logger.debug("**AUTHOR** nextpage URL: "+nextpage) + authordata = self._fetchUrl(nextpage) + descurl=nextpage + authorsoup = self.make_soup(authordata) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(url) + else: + raise e + + storydiv = authorsoup.find('div', {'id':'st'+self.story.getMetadata('storyId'), 'class':re.compile(r"storylistitem")}) + self.setDescription(descurl,storydiv.find('div',{'class':'storydesc'})) + #self.story.setMetadata('description',stripHTML(storydiv.find('div',{'class':'storydesc'}))) + self.story.setMetadata('title',stripHTML(storydiv.find('a',{'class':'storylink'}))) + + ainfo = soup.find('a', href='/StoryInfo-%s-1'%self.story.getMetadata('storyId')) + if ainfo != None: # indicates multiple authors/contributors. + try: + # going to pull part of the meta data from author list page. + infourl = 'http://'+self.host+ainfo['href'] + logger.debug("**StoryInfo** URL: "+infourl) + infodata = self._fetchUrl(infourl) + infosoup = self.make_soup(infodata) + + # for a in infosoup.findAll('a',href=re.compile(r"^/Author-\d+")): + # self.story.addToList('authorId',a['href'].split('/')[1].split('-')[1]) + # self.story.addToList('authorUrl','http://'+self.host+a['href'].replace("/Author-","/AuthorStories-")) + # self.story.addToList('author',stripHTML(a)) + + # second verticaltable is the chapter list. + table = infosoup.findAll('table',{'class':'verticaltable'})[1] + for a in table.findAll('a',href=re.compile(r"^/Story-"+self.story.getMetadata('storyId'))): + autha = a.findNext('a',href=re.compile(r"^/Author-\d+")) + self.story.addToList('authorId',autha['href'].split('/')[1].split('-')[1]) + self.story.addToList('authorUrl','http://'+self.host+autha['href'].replace("/Author-","/AuthorStories-")) + self.story.addToList('author',stripHTML(autha)) + # include leading number to match 1. ... 2. ... + self.chapterUrls.append(("%d. %s by %s"%(len(self.chapterUrls)+1, + stripHTML(a), + stripHTML(autha)),'http://'+self.host+a['href'])) + + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(url) + else: + raise e + else: # single author: + # Find the chapter selector + select = soup.find('select', { 'name' : 'chapnav' } ) + + if select is None: + # no selector found, so it's a one-chapter story. + self.chapterUrls.append((self.story.getMetadata('title'),url)) + else: + allOptions = select.findAll('option') + for o in allOptions: + url = "http://"+self.host+o['value'] + # just in case there's tags, like <i> in chapter titles. + self.chapterUrls.append((stripHTML(o),url)) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + verticaltable = soup.find('table', {'class':'verticaltable'}) + + BtVS = True + BtVSNonX = False + for cat in verticaltable.findAll('a', href=re.compile(r"^/Category-")): + if cat.string not in ['General', 'Non-BtVS/AtS Stories', 'Non-BTVS/AtS Stories', 'BtVS/AtS Non-Crossover', 'Non-BtVS Crossovers']: + self.story.addToList('category',cat.string) + else: + if 'Non-BtVS' in cat.string or 'Non-BTVS' in cat.string: + BtVS = False + if 'BtVS/AtS Non-Crossover' == cat.string: + BtVSNonX = True + + verticaltabletds = verticaltable.findAll('td') + self.story.setMetadata('rating', verticaltabletds[2].string) + self.story.setMetadata('numWords', verticaltabletds[4].string) + + # Complete--if completed. + if 'Yes' in verticaltabletds[10].string: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + #print("date:%s"%verticaltabletds[8]) + self.story.setMetadata('datePublished',makeDate(stripHTML(verticaltabletds[8].string), self.dateformat)) + self.story.setMetadata('dateUpdated',makeDate(stripHTML(verticaltabletds[9].string), self.dateformat)) + + for icon in storydiv.find('span',{'class':'storyicons'}).findAll('img'): + if( icon['title'] not in ['Non-Crossover'] ) : + self.story.addToList('genre',icon['title']) + else: + if not BtVSNonX: + BtVS = False # Don't add BtVS if Non-Crossover, unless it's a BtVS/AtS Non-Crossover + + #print("BtVS: %s BtVSNonX: %s"%(BtVS,BtVSNonX)) + if BtVS: + self.story.addToList('category','Buffy: The Vampire Slayer') + + pseries = soup.find('p', {'style':'margin-top:0px'}) + #print("pseries:%s"%pseries.get_text()) + m = re.match('This story is No\. (?P<num>\d+) in the series "(?P<series>.+)"\.', + pseries.get_text()) + if m: + self.setSeries(m.group('series'),m.group('num')) + self.story.setMetadata('seriesUrl',"http://"+self.host+pseries.find('a')['href']) + + def getChapterText(self, url): + logger.debug('Getting chapter text from: %s' % url) + soup = self.make_soup(self._fetchUrl(url)) + + div = soup.find('div', {'id' : 'storyinnerbody'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + # strip out included chapter title, if present, to avoid doubling up. + try: + div.find('h3').extract() + except: + pass + return self.utf8FromSoup(url,div) + +def getClass(): + return TwistingTheHellmouthSiteAdapter + diff --git a/fanficdownloader/adapters/adapter_twcslibrarynet.py b/fanficdownloader/adapters/adapter_twcslibrarynet.py new file mode 100644 index 0000000..6bae06a --- /dev/null +++ b/fanficdownloader/adapters/adapter_twcslibrarynet.py @@ -0,0 +1,272 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +class TheWritersCoffeeShopComSiteAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + self.story.setMetadata('siteabbrev','twcs') + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId')) + self.dateformat = "%d %b %Y" + + + @classmethod + def getAcceptDomains(cls): + return ['thewriterscoffeeshop.com','twcslibrary.net'] + + @staticmethod + def getSiteDomain(): + return 'www.twcslibrary.net' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://")+"(www.)?("+'|'.join(self.getAcceptDomains())+")/(library/)?"+re.escape("viewstory.php?sid=")+r"\d+$" + + def needToLoginCheck(self, data): + if 'Registered Users Only' in data \ + or 'There is no such account on our website' in data \ + or "That password doesn't match the one in our database" in data: + return True + else: + return False + + def 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' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['penname'])) + + d = self._fetchUrl(loginUrl, params) + + if "Member Account" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['penname'])) + raise exceptions.FailedToLogin(url,params['penname']) + return False + else: + return True + + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + addurl = "&ageconsent=ok&warning=3" + else: + addurl="" + + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url) + + if "Age Consent Required" in data: + 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.") + + # problems with some stories, but only in calibre. I suspect + # issues with different SGML parsers in python. This is a + # nasty hack, but it works. + data = data[data.index("<body"):] + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('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)) + + 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) + # poor HTML(unclosed <p> for one) can cause run on + # over the next label. + if '<span class="label">' in svalue: + svalue = svalue[0:svalue.find('<span class="label">')] + break + else: + value = value.nextSibling + self.setDescription(url,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')) + catstext = [cat.string for cat in cats] + for cat in catstext: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + charstext = [char.string for char in chars] + for char in charstext: + self.story.addToList('characters',char.string) + + if 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class')) + genrestext = [genre.string for genre in genres] + self.genre = ', '.join(genrestext) + for genre in genrestext: + self.story.addToList('genre',genre.string) + + if 'Completed' in label: + if 'Yes' in value: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Published' in label: + self.story.setMetadata('datePublished', makeDate(stripHTML(value), self.dateformat)) + + if 'Updated' in label: + # there's a stray [ at the end. + #value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat)) + + try: + # Find Series name from series URL. + a = soup.find('a', href=re.compile(r"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)) + storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + data = self._fetchUrl(url) + # problems with some stories, but only in calibre. I suspect + # issues with different SGML parsers in python. This is a + # nasty hack, but it works. + data = data[data.index("<body"):] + + chapter=bs.BeautifulSoup('<div class="story"></div>') + + soup = bs.BeautifulSoup(data) + + found=False + for div in soup.findAll('div'): + if div.has_key('class') and div['class'] == 'notes': + chapter.append(div) + if div.has_key('id') and div['id'] == 'story': + chapter.append(div) + found=True + + if not found: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,chapter) + +def getClass(): + return TheWritersCoffeeShopComSiteAdapter + diff --git a/fanficdownloader/adapters/adapter_twilightarchivescom.py b/fanficdownloader/adapters/adapter_twilightarchivescom.py new file mode 100644 index 0000000..2b8fc20 --- /dev/null +++ b/fanficdownloader/adapters/adapter_twilightarchivescom.py @@ -0,0 +1,188 @@ +# -*- 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 +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return TwilightArchivesComAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class TwilightArchivesComAdapter(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.path.split('/',)[2]) + + + # normalized story URL. http://www.twilightarchives.com/read/9353 + self._setURL('http://' + self.getSiteDomain() + '/read/'+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','twa') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%d %b %y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'www.twilightarchives.com' + + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/read/1234" + + def getSiteURLPattern(self): + return re.escape("http://" + self.getSiteDomain()+"/read/")+r"\d+(/d+)?$" + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('h1') + self.story.setMetadata('title',stripHTML(a)) + + # Find the chapters: + chapters=soup.find('ol', {'class' : 'chapters'}) + if chapters != None: + for chapter in chapters.findAll('a', href=re.compile(r'/read/'+self.story.getMetadata('storyId')+"/\d+$")): + self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+chapter['href'])) + else: + self.chapterUrls.append((self.story.getMetadata('title'),url)) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + +# rated, genre, warnings, seires + + summary = soup.find('p', {'class' : 'images'}) + self.setDescription(url,summary) + + for c in soup.findAll('h2', {'class' : 'title'}): + div = c.nextSibling.nextSibling + + if 'Information' in c.text: + for dt in div.findAll('dt'): + dd=dt.nextSibling.nextSibling + + if 'Author' in dt.text: + a=dd.find('a') + self.story.setMetadata('authorId',a['href'].split('/')[2]) + self.story.setMetadata('authorUrl','http://'+self.host+a['href']) + self.story.setMetadata('author',a.text) + + if 'Words' in dt.text: + self.story.setMetadata('numWords', dd.text) + + if 'Published' in dt.text: + self.story.setMetadata('datePublished', makeDate(stripHTML(dd.text), self.dateformat)) + + if 'Updated' in dt.text: + self.story.setMetadata('dateUpdated', makeDate(stripHTML(dd.text), self.dateformat)) + + if 'Status' in dt.text: + if 'Complete' in dd.text: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + if 'Categories' in c.text: + for a in div.findAll('a'): + self.story.addToList('category',a.text) + + if 'Characters' in c.text: + for a in div.findAll('a'): + self.story.addToList('category',a.text) + + if 'Series' in c.text: + a=div.find('a') + series_name = a.text + series_url = 'http://'+self.host+a['href'] + + seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url)) + storyas = seriessoup.find('tbody').findAll('a', href=re.compile(r'^/read/\d+$')) + i=1 + for a in storyas: + if a['href'] == ('/read/'+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl'))) + a=asoup.find('tbody').find('a', href=re.compile(r'^/read/'+self.story.getMetadata('storyId'))) + self.story.setMetadata('rating',a.parent.nextSibling.nextSibling.text) + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'class' : 'size images medium'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_twilightednet.py b/fanficdownloader/adapters/adapter_twilightednet.py new file mode 100644 index 0000000..e70932d --- /dev/null +++ b/fanficdownloader/adapters/adapter_twilightednet.py @@ -0,0 +1,253 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +class TwilightedNetSiteAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + self.story.setMetadata('siteabbrev','tw') + 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 = "" + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId')) + + + @staticmethod + def getSiteDomain(): + return 'www.twilighted.net' + + @classmethod + def getAcceptDomains(cls): + return ['www.twilighted.net','twilighted.net'] + + @classmethod + def getSiteExampleURLs(cls): + return "http://www.twilighted.net/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://")+r"(www\.)?"+re.escape("twilighted.net/viewstory.php?sid=")+r"\d+$" + + def needToLoginCheck(self, data): + if 'Registered Users Only' in data \ + or 'There is no such account on our website' in data \ + or "That password doesn't match the one in our database" in data: + return True + else: + return False + + def 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' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['penname'])) + + d = self._fetchUrl(loginUrl, params) + + if "Member Account" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['penname'])) + raise exceptions.FailedToLogin(url,params['penname']) + return False + else: + return True + + def extractChapterUrlsAndMetadata(self): + + url = self.url+'&index=1' + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # problems with some stories, but only in calibre. I suspect + # issues with different SGML parsers in python. This is a + # nasty hack, but it works. + # twilighted isn't writing <body> ??? wtf? + data = "<html><body>"+data[data.index("</head>"):] + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php")) + 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'])) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + 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) + + 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')) + catstext = [cat.string for cat in cats] + for cat in catstext: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + charstext = [char.string for char in chars] + for char in charstext: + self.story.addToList('characters',char.string) + + ## twilighted.net doesn't use genre. + # if 'Genre' in label: + # genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class')) + # genrestext = [genre.string for genre in genres] + # self.genre = ', '.join(genrestext) + # for genre in genrestext: + # self.story.addToList('genre',genre.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(value.strip(), "%B %d, %Y")) + + if 'Updated' in label: + # there's a stray [ at the end. + #value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(value.strip(), "%B %d, %Y")) + + 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)) + storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + data = self._fetchUrl(url) + # problems with some stories, but only in calibre. I suspect + # issues with different SGML parsers in python. This is a + # nasty hack, but it works. + # twilighted isn't writing <body> ??? wtf? + data = "<html><body>"+data[data.index("</head>"):] + + soup = bs.BeautifulStoneSoup(data, + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + span = soup.find('div', {'id' : 'story'}) + + if None == span: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,span) + +def getClass(): + return TwilightedNetSiteAdapter + diff --git a/fanficdownloader/adapters/adapter_twiwritenet.py b/fanficdownloader/adapters/adapter_twiwritenet.py new file mode 100644 index 0000000..2e9ae98 --- /dev/null +++ b/fanficdownloader/adapters/adapter_twiwritenet.py @@ -0,0 +1,281 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +class TwiwriteNetSiteAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + self.story.setMetadata('siteabbrev','twrt') + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.is_adult = False + self.username = "NoneGiven" # if left empty, twiwrite.net doesn't return any message at all. + self.password = "" + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId')) + + + @staticmethod + def getSiteDomain(): + return 'www.twiwrite.net' + + @classmethod + def getAcceptDomains(cls): + return ['www.twiwrite.net','twiwrite.net'] + + @classmethod + def getSiteExampleURLs(cls): + return "http://www.twiwrite.net/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://")+r"(www\.)?"+re.escape("twiwrite.net/viewstory.php?sid=")+r"\d+$" + + def needToLoginCheck(self, data): + if 'Registered Users Only' in data \ + or 'There is no such account on our website' in data \ + or "That password doesn't match the one in our database" in data: + return True + else: + return False + + def 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' + logger.info("Will now login to URL (%s) as (%s)" % (loginUrl, + params['penname'])) + + d = self._fetchUrl(loginUrl, params) + + if "Member Account" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['penname'])) + raise exceptions.FailedToLogin(url,params['penname']) + return False + else: + return True + + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + # Weirdly, different sites use different warning numbers. + # If the title search below fails, there's a good chance + # you need a different number. print data at that point + # and see what the 'click here to continue' url says. + addurl = "&ageconsent=ok&warning=1" # XXX + else: + addurl="" + + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url) + + 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.") + + if "Contains Explicit Content for mature adults only! May contain graphic violence, mature sexual situations, and explicit language. Read with caution." in data: + raise exceptions.AdultCheckRequired(self.url) + + # problems with some stories, but only in calibre. I suspect + # issues with different SGML parsers in python. This is a + # nasty hack, but it works. + data = data[data.index("<body"):] + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + + pagetitlediv = soup.find('div',id='pagetitle') + + ## Title + a = pagetitlediv.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = pagetitlediv.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)) + + ## <meta name='description' content='<p>Description</p> ...' > + ## Summary, strangely, is in the content attr of a <meta name='description'> tag + ## which is escaped HTML. Unfortunately, we can't use it because they don't + ## escape (') chars in the desc, breakin the tag. + #meta_desc = soup.find('meta',{'name':'description'}) + #metasoup = bs.BeautifulStoneSoup(meta_desc['content']) + #self.story.setMetadata('description',stripHTML(metasoup)) + + 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')) + catstext = [cat.string for cat in cats] + for cat in catstext: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + charstext = [char.string for char in chars] + for char in charstext: + self.story.addToList('characters',char.string) + + if 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=3')) + genrestext = [genre.string for genre in genres] + self.genre = ', '.join(genrestext) + for genre in genrestext: + 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=8')) + warningstext = [warning.string for warning in warnings] + self.warning = ', '.join(warningstext) + for warning in warningstext: + self.story.addToList('warning',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(value.strip(), "%B %d, %Y")) + + if 'Updated' in label: + # there's a stray [ at the end. + value = value[0:-1] + self.story.setMetadata('dateUpdated', makeDate(value.strip(), "%B %d, %Y")) + + 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)) + storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + data = self._fetchUrl(url) + # problems with some stories, but only in calibre. I suspect + # issues with different SGML parsers in python. This is a + # nasty hack, but it works. + data = data[data.index("<body"):] + + soup = bs.BeautifulStoneSoup(data, + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + span = soup.find('div', {'id' : 'story'}) + + if None == span: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,span) + +def getClass(): + return TwiwriteNetSiteAdapter + diff --git a/fanficdownloader/adapters/adapter_voracity2eficcom.py b/fanficdownloader/adapters/adapter_voracity2eficcom.py new file mode 100644 index 0000000..56c4257 --- /dev/null +++ b/fanficdownloader/adapters/adapter_voracity2eficcom.py @@ -0,0 +1,233 @@ +# Software: eFiction +import re +import urllib2 +import urlparse + +from .. import BeautifulSoup + +from base_adapter import BaseSiteAdapter, makeDate +from .. import exceptions + + +def getClass(): + return Voracity2EficComAdapter + + +# yields Tag _and_ NavigableString siblings from the given tag. The +# BeautifulSoup findNextSiblings() method for some reasons only returns either +# NavigableStrings _or_ Tag objects, not both. +def _yield_next_siblings(tag): + sibling = tag.nextSibling + while sibling: + yield sibling + sibling = sibling.nextSibling + + +class Voracity2EficComAdapter(BaseSiteAdapter): + SITE_ABBREVIATION = 'voe' + SITE_DOMAIN = 'voracity2.e-fic.com' + + BASE_URL = 'http://' + SITE_DOMAIN + '/' + LOGIN_URL = BASE_URL + 'user.php?action=login' + VIEW_STORY_URL_TEMPLATE = BASE_URL + 'viewstory.php?sid=%d' + METADATA_URL_SUFFIX = '&index=1' + AGE_CONSENT_URL_SUFFIX = '&ageconsent=ok&warning=4' + + DATETIME_FORMAT = '%m/%d/%Y' + REQUIRED_SKIN = 'Simple Elegance' + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + query_data = urlparse.parse_qs(self.parsedUrl.query) + story_id = query_data['sid'][0] + + self.story.setMetadata('storyId', story_id) + self._setURL(self.VIEW_STORY_URL_TEMPLATE % int(story_id)) + self.story.setMetadata('siteabbrev', self.SITE_ABBREVIATION) + + self.is_logged_in = False + + def _login(self): + # Apparently self.password is only set when login fails, i.e. + # the FailedToLogin exception is raised, so the adapter gets new + # login data and tries again + if self.password: + password = self.password + username = self.username + else: + username = self.getConfig('username') + password = self.getConfig('password') + + parameters = { + 'penname': username, + 'password': password, + 'submit': 'Submit'} + + class CustomizedFailedToLogin(exceptions.FailedToLogin): + def __init__(self, url, passwdonly=False): + # Use username variable from outer scope + exceptions.FailedToLogin.__init__(self, url, username, passwdonly) + + soup = self._customized_fetch_url(self.LOGIN_URL, CustomizedFailedToLogin, parameters) + div = soup.find('div', id='useropts') + if not div: + raise CustomizedFailedToLogin(self.LOGIN_URL) + + self.is_logged_in = True + + def _customized_fetch_url(self, url, exception=None, parameters=None): + if exception: + try: + data = self._fetchUrl(url, parameters) + except urllib2.HTTPError: + raise exception(self.url) + # Just let self._fetchUrl throw the exception, don't catch and + # customize it. + else: + data = self._fetchUrl(url, parameters) + + return BeautifulSoup.BeautifulSoup(data) + + @staticmethod + def getSiteDomain(): + return Voracity2EficComAdapter.SITE_DOMAIN + + @classmethod + def getSiteExampleURLs(cls): + return cls.VIEW_STORY_URL_TEMPLATE % 1234 + + def getSiteURLPattern(self): + return re.escape(self.VIEW_STORY_URL_TEMPLATE[:-2]) + r'\d+$' + + def extractChapterUrlsAndMetadata(self): + soup = self._customized_fetch_url(self.url + self.METADATA_URL_SUFFIX) + + # Check if the story is for "Registered Users Only", i.e. has adult + # content. Based on the "is_adult" attributes either login or raise an + # error. + errortext_div = soup.find('div', {'class': 'errortext'}) + if errortext_div: + error_text = ''.join(errortext_div(text=True)).strip() + if error_text == 'Registered Users Only': + if not (self.is_adult or self.getConfig('is_adult')): + raise exceptions.AdultCheckRequired(self.url) + self._login() + else: + # This case usually occurs when the story doesn't exist, but + # might potentially be something else, so just raise + # FailedToDownload exception with the found error text. + raise exceptions.FailedToDownload(error_text) + + url = ''.join([self.url, self.METADATA_URL_SUFFIX, self.AGE_CONSENT_URL_SUFFIX]) + soup = self._customized_fetch_url(url) + + # If logged in and the skin doesn't match the required skin throw an + # error + if self.is_logged_in: + skin = soup.find('select', {'name': 'skin'}).find('option', selected=True)['value'] + if skin != self.REQUIRED_SKIN: + raise exceptions.FailedToDownload('Required skin "%s" must be set in preferences' % self.REQUIRED_SKIN) + + pagetitle_div = soup.find('div', id='pagetitle') + self.story.setMetadata('title', pagetitle_div.a.string) + + author_anchor = pagetitle_div.a.findNextSibling('a') + url = urlparse.urljoin(self.BASE_URL, author_anchor['href']) + components = urlparse.urlparse(url) + query_data = urlparse.parse_qs(components.query) + + self.story.setMetadata('author', author_anchor.string) + self.story.setMetadata('authorId', query_data['uid']) + self.story.setMetadata('authorUrl', url) + + sort_div = soup.find('div', id='sort') + self.story.setMetadata('reviews', sort_div('a')[1].string) + + for b_tag in soup.find('div', {'class': 'listbox'})('b'): + key = b_tag.string.strip(' :') + try: + value = b_tag.nextSibling.string.strip() + # This can happen with some fancy markup in the summary. Just + # ignore this error and set value to None, the summary parsing + # takes care of this + except AttributeError: + value = None + + if key == 'Summary': + contents = [] + keep_summary_html = self.getConfig('keep_summary_html') + + for sibling in _yield_next_siblings(b_tag): + if isinstance(sibling, BeautifulSoup.Tag): + # Encountered next label, break. This method is the + # safest and most reliable I could think of. Blame + # e-fiction sites that allow their users to include + # arbitrary markup into their summaries and the + # horrible HTML markup. + if sibling.name == 'b' and sibling.findPreviousSibling().name == 'br': + break + + if keep_summary_html: + contents.append(self.utf8FromSoup(self.url, sibling)) + else: + contents.append(''.join(sibling(text=True))) + else: + contents.append(sibling) + + # Remove the preceding break line tag and other crud + contents.pop() + contents.pop() + self.story.setMetadata('description', ''.join(contents)) + + elif key == 'Rating': + self.story.setMetadata('rating', value) + + elif key == 'Category': + for sibling in b_tag.findNextSiblings(['a', 'br']): + if sibling.name == 'br': + break + self.story.addToList('category', sibling.string) + + # Seems to be always "None" for some reason + elif key == 'Characters': + for sibling in b_tag.findNextSiblings(['a', 'br']): + if sibling.name == 'br': + break + self.story.addToList('characters', sibling.string) + + elif key == 'Series': + a = b_tag.findNextSibling('a') + if not a: + continue + self.story.setMetadata('series', a.string) + self.story.setMetadata('seriesUrl', urlparse.urljoin(self.BASE_URL, a['href'])) + + elif key == 'Chapter': + self.story.setMetadata('numChapters', int(value)) + + elif key == 'Completed': + self.story.setMetadata('status', 'Completed' if value == 'Yes' else 'In-Progress') + + elif key == 'Words': + self.story.setMetadata('numWords', value) + + elif key == 'Read': + self.story.setMetadata('readings', value) + + elif key == 'Published': + self.story.setMetadata('datePublished', makeDate(value, self.DATETIME_FORMAT)) + + elif key == 'Updated': + self.story.setMetadata('dateUpdated', makeDate(value, self.DATETIME_FORMAT)) + + for b_tag in soup.find('div', id='output').findNextSiblings('b'): + chapter_anchor = b_tag.a + title = chapter_anchor.string + url = urlparse.urljoin(self.BASE_URL, chapter_anchor['href']) + self.chapterUrls.append((title, url)) + + def getChapterText(self, url): + url += self.AGE_CONSENT_URL_SUFFIX + soup = self._customized_fetch_url(url) + return self.utf8FromSoup(url, soup.find('div', id='story')) diff --git a/fanficdownloader/adapters/adapter_walkingtheplankorg.py b/fanficdownloader/adapters/adapter_walkingtheplankorg.py new file mode 100644 index 0000000..8e0cb19 --- /dev/null +++ b/fanficdownloader/adapters/adapter_walkingtheplankorg.py @@ -0,0 +1,232 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return WalkingThePlankOrgAdapter + +class WalkingThePlankOrgAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/archive/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','wtp') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%b %d, %Y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'www.walkingtheplank.org' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/archive/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/archive/viewstory.php?sid=")+r"\d+$" + + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + # Weirdly, different sites use different warning numbers. + # If the title search below fails, there's a good chance + # you need a different number. print data at that point + # and see what the 'click here to continue' url says. + addurl = "&warning=4" + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + + # 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 "By clicking this link, you acknowledge" in data: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/archive/'+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+'/archive/'+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 "" + + 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')) + catstext = [cat.string for cat in cats] + for cat in catstext: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + charstext = [char.string for char in chars] + for char in charstext: + self.story.addToList('characters',char.string) + + if 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) + for genre in genres: + self.story.addToList('genre',genre.string) + + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) # XXX + 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+'/archive/'+a['href'] + + # use BeautifulSoup HTML parser to make everything easier to find. + seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url)) + storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_whoficcom.py b/fanficdownloader/adapters/adapter_whoficcom.py new file mode 100644 index 0000000..099dcf4 --- /dev/null +++ b/fanficdownloader/adapters/adapter_whoficcom.py @@ -0,0 +1,238 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +class WhoficComSiteAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + self.story.setMetadata('siteabbrev','whof') + 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. + + @staticmethod + def getSiteDomain(): + return 'www.whofic.com' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+"\d+$" + + def extractChapterUrlsAndMetadata(self): + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + # fetch the first chapter. From that we will: + # - determine title, authorname, authorid + # - get chapter list, if not one-shot. + + url = self.url+'&chapter=1' + logger.debug("URL: "+url) + + # use BeautifulSoup HTML parser to make everything easier to find. + try: + soup = bs.BeautifulSoup(self._fetchUrl(url)) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + # pull title(title) and author from the HTML title. + title = stripHTML(soup.find('title')) + logger.debug('Title: %s' % title) + title = title.split('::')[1].strip() + self.story.setMetadata('title',title.split(' by ')[0].strip()) + self.story.setMetadata('author',title.split(' by ')[1].strip()) + + # Find authorid and URL from... author url. + a = soup.find('a', href=re.compile(r"viewuser.php")) + self.story.setMetadata('authorId',a['href'].split('=')[1]) + self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href']) + + # Find the chapter selector + select = soup.find('select', { 'name' : 'chapter' } ) + + if select is None: + # no selector found, so it's a one-chapter story. + self.chapterUrls.append((self.story.getMetadata('title'),url)) + else: + allOptions = select.findAll('option') + for o in allOptions: + url = self.url + "&chapter=%s" % o['value'] + # just in case there's tags, like <i> in chapter titles. + title = "%s" % o + title = re.sub(r'<[^>]+>','',title) + self.chapterUrls.append((title,url)) + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + + ## Whofic.com puts none of the other meta data in the chapters + ## or even the story chapter index page. Need to scrape the + ## author page to find it. + + # <table width="100%" bordercolor="#333399" border="0" cellspacing="0" cellpadding="2"><tr><td> + # <b><a href="viewstory.php?sid=38220">Accompaniment 2</a></b> by <a href="viewuser.php?uid=12412">clandestinemiscreant</a> [<a href="reviews.php?sid=38220">Reviews</a> - <a href="reviews.php?sid=38220">0</a>] <br> + # This is a series of short stories written as an accompaniment to Season 2, Season 28 for us oldies, and each is unrelated except for that one factor. Each story is canon, in that it does not change established events at time of airing, based on things mentioned and/or implied and missing or deleted scenes that were not seen in the final aired episodes.<br> + # <font size="-1"><b><a href="categories.php?catid=15">Tenth Doctor</a></b> - All Ages - None - Humor, Hurt/Comfort, Romance<br> + # <i>Characters:</i> Rose Tyler<br> + # <i>Series:</i> None<br> + # <i>Published:</i> 2010.08.15 - <i>Updated:</i> 2010.08.16 - <i>Chapters:</i> 4 - <i>Completed:</i> Yes - <i>Word Count:</i> 4890 </font> + # </td></tr></table> + + logger.debug("Author URL: "+self.story.getMetadata('authorUrl')) + soup = bs.BeautifulStoneSoup(self._fetchUrl(self.story.getMetadata('authorUrl')), + selfClosingTags=('br')) # normalize <br> tags to <br /> + # find this story in the list, parse it's metadata based on + # lots of assumptions about the html, since there's little + # tagging. + # Found a story once that had the story URL in the desc for a + # series on the same author's page. Now using the reviews + # link instead to find the appropriate metadata. + a = soup.find('a', href=re.compile(r'reviews.php\?sid='+self.story.getMetadata('storyId'))) + metadata = a.findParent('td') + metadatachunks = self.utf8FromSoup(None,metadata).split('<br />') + # process metadata for this story. + self.setDescription(url,metadatachunks[1]) + #self.story.setMetadata('description', metadatachunks[1]) + + # First line of the stuff with ' - ' separators + moremeta = metadatachunks[2] + moremeta = re.sub(r'<[^>]+>','',moremeta) # strip tags. + + moremetaparts = moremeta.split(' - ') + + # first part is category--whofic.com has categories + # Doctor One-11, Torchwood, etc. We're going to + # prepend any with 'Doctor' or 'Era' (Multi-Era, Other + # Era) as 'Doctor Who'. + # + # Also push each in as 'extra tags'. + category = moremetaparts[0] + if 'Doctor' in category or 'Era' in category : + self.story.addToList('category','Doctor Who') + + for cat in category.split(', '): + self.story.addToList('category',cat) + + # next in that line is age rating. + self.story.setMetadata('rating',moremetaparts[1]) + + # after that is a possible list fo specific warnings, + # Explicit Violence, Swearing, etc + if "None" not in moremetaparts[2]: + for warn in moremetaparts[2].split(', '): + self.story.addToList('warnings',warn) + + # then genre. It's another comma list. All together + # in genre, plus each in extra tags. + genre=moremetaparts[3] + for g in genre.split(r', '): + self.story.addToList('genre',g) + + # line 3 is characters. + chars = metadatachunks[3] + charsearch="<i>Characters:</i>" + if charsearch in chars: + chars = chars[metadatachunks[3].index(charsearch)+len(charsearch):] + for c in chars.split(','): + if c.strip() != u'None': + self.story.addToList('characters',c) + + # the next line is stuff with ' - ' separators *and* names--with tags. + moremeta = metadatachunks[5] + moremeta = re.sub(r'<[^>]+>','',moremeta) # strip tags. + + moremetaparts = moremeta.split(' - ') + + for part in moremetaparts: + (name,value) = part.split(': ') + name=name.strip() + value=value.strip() + if name == 'Published': + self.story.setMetadata('datePublished', makeDate(value, '%Y.%m.%d')) + if name == 'Updated': + self.story.setMetadata('dateUpdated', makeDate(value, '%Y.%m.%d')) + if name == 'Completed': + if value == 'Yes': + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + if name == 'Word Count': + self.story.setMetadata('numWords', value) + + try: + # Find Series name from series URL. + a = metadata.find('a', href=re.compile(r"series.php\?seriesid=\d+")) + series_name = a.string + series_url = 'http://'+self.host+'/'+a['href'] + + # use BeautifulSoup HTML parser to make everything easier to find. + seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url)) + storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + + # hardly a great identifier, I know, but whofic really doesn't + # give us anything better to work with. + span = soup.find('span', {'style' : 'font-size: 100%;'}) + + if None == span: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + span.name='div' + return self.utf8FromSoup(url,span) + +def getClass(): + return WhoficComSiteAdapter + diff --git a/fanficdownloader/adapters/adapter_wizardtalesnet.py b/fanficdownloader/adapters/adapter_wizardtalesnet.py new file mode 100644 index 0000000..3845196 --- /dev/null +++ b/fanficdownloader/adapters/adapter_wizardtalesnet.py @@ -0,0 +1,303 @@ +# -*- coding: utf-8 -*- + +# Copyright 2012 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return WizardTalesNetAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class WizardTalesNetAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','wzt') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%m/%d/%Y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'www.wizardtales.net' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$" + + ## 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' + logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl, + params['penname'])) + + d = self._fetchUrl(loginUrl, params) + + if "Member Account" not in d : #Member Account + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['penname'])) + raise exceptions.FailedToLogin(url,params['penname']) + return False + else: + return True + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + if self.is_adult or self.getConfig("is_adult"): + # Weirdly, different sites use different warning numbers. + # If the title search below fails, there's a good chance + # you need a different number. print data at that point + # and see what the 'click here to continue' url says. + addurl = "&ageconsent=ok&warning=4" + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if self.needToLoginCheck(data): + # need to log in for this one. + self.performLogin(url) + data = self._fetchUrl(url) + + m = re.search(r"'viewstory.php\?sid=\d+((?:&ageconsent=ok)?&warning=\d+)'",data) + if m != None: + if self.is_adult or self.getConfig("is_adult"): + # We tried the default and still got a warning, so + # let's pull the warning number from the 'continue' + # link and reload data. + addurl = m.group(1) + # correct stupid & error in url. + addurl = addurl.replace("&","&") + url = self.url+'&index=1'+addurl + logger.debug("URL 2nd try: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + else: + raise exceptions.AdultCheckRequired(self.url) + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + pt = soup.find('div', {'id' : 'pagetitle'}) + a = pt.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = pt.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) + + rating=pt.text.split('[')[1].split(']')[0] + self.story.setMetadata('rating', rating) + + # 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. + + # <span class="label">Rated:</span> NC-17<br /> etc + content=soup.find('div',{'class' : 'content'}) + + for genre in content.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')): + self.story.addToList('genre',genre.string) + + for warning in content.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')): + self.story.addToList('warnings',warning.string) + + labels = content.findAll('b') + + value = labels[0].previousSibling + svalue = "" + while value != None: + val = value + value = value.previousSibling + while "Categories" not in val: + svalue += str(val) + val = val.nextSibling + self.setDescription(url,svalue) + + + + 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) + + if 'Word count' in label: + self.story.setMetadata('numWords', stripHTML(value).split(': ')[1].split(';')[0]) + + if 'Categories' in label: + cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories')) + for cat in cats: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + for char in chars: + self.story.addToList('characters',char.string) + + if 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) + for genre in genres: + self.story.addToList('genre',genre.string) + + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=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).split(': ')[1].split(';')[0], self.dateformat)) + + if 'Updated' in label: + self.story.setMetadata('dateUpdated', makeDate(stripHTML(value).split(': ')[1], 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) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.findAll('div', {'id' : 'story'})[1] + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_wolverineandroguecom.py b/fanficdownloader/adapters/adapter_wolverineandroguecom.py new file mode 100644 index 0000000..d2d88a3 --- /dev/null +++ b/fanficdownloader/adapters/adapter_wolverineandroguecom.py @@ -0,0 +1,219 @@ +# -*- 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 +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +def getClass(): + return WolverineAndRogueComAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class WolverineAndRogueComAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + # normalized story URL. + self._setURL('http://' + self.getSiteDomain() + '/wrfa/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','wrfa') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%m/%d/%Y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'www.wolverineandrogue.com' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/wrfa/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/wrfa/viewstory.php?sid=")+r"\d+$" + + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1' + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if "Access denied. This story has not been validated by the adminstrators of this site." in data: + raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.") + + # use BeautifulSoup HTML parser to make everything easier to find. + soup = bs.BeautifulSoup(data) + # print data + + # Now go hunting for all the meta data and the chapter list. + + ## Title + pt = soup.find('div', {'id' : 'pagetitle'}) + a = pt.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + a = pt.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+'/wrfa/'+a['href']) + self.story.setMetadata('author',a.string) + + rating=pt.text.split('(')[1].split(')')[0] + self.story.setMetadata('rating', rating) + + # 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+'/wrfa/'+chapter['href'])) + + 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. + + # <span class="label">Rated:</span> NC-17<br /> etc + content=soup.find('div',{'class' : 'content'}) + labels = soup.findAll('span',{'class':'label'}) + + value = labels[0].previousSibling + svalue = "" + while value != None: + val = value + value = value.previousSibling + while "Categories" not in str(val): + svalue += str(val) + val = val.nextSibling + self.setDescription(url,svalue) + + + for labelspan in labels: + value = labelspan.nextSibling + label = labelspan.string + + if 'Rated' in label: + self.story.setMetadata('rating', value) + + if 'Word count' in label: + self.story.setMetadata('numWords', value.split(' -')[0]) + + if 'Categories' in label: + cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories')) + for cat in cats: + self.story.addToList('category',cat.string) + + if 'Characters' in label: + chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters')) + for char in chars: + self.story.addToList('characters',char.string) + + if 'Genre' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) + for genre in genres: + self.story.addToList('genre',genre.string) + + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) + for warning in warnings: + self.story.addToList('warnings',warning.string) + + if 'Complete' 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(value.split(' -')[0], 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+'/wrfa/'+a['href'] + + # use BeautifulSoup HTML parser to make everything easier to find. + seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url)) + # can't use ^viewstory...$ in case of higher rated stories with javascript href. + storyas = seriessoup.findAll('a', href=re.compile(r'viewstory.php\?sid=\d+')) + i=1 + for a in storyas: + # skip 'report this' and 'TOC' links + if 'contact.php' not in a['href'] and 'index' not in a['href']: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulStoneSoup(self._fetchUrl(url), + selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags. + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) diff --git a/fanficdownloader/adapters/adapter_wraithbaitcom.py b/fanficdownloader/adapters/adapter_wraithbaitcom.py new file mode 100644 index 0000000..589ec22 --- /dev/null +++ b/fanficdownloader/adapters/adapter_wraithbaitcom.py @@ -0,0 +1,234 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import time +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + + +def getClass(): + return WraithBaitComAdapter + +# Class name has to be unique. Our convention is camel case the +# sitename with Adapter at the end. www is skipped. +class WraithBaitComAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.decode = ["Windows-1252", + "utf8"] # 1252 is a superset of iso-8859-1. + # Most sites that claim to be + # iso-8859-1 (and some that claim to be + # utf8) are really windows-1252. + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + # get storyId from url--url validation guarantees query is only sid=1234 + self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1]) + + + + self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId')) + + # Each adapter needs to have a unique site abbreviation. + self.story.setMetadata('siteabbrev','wb') + + # The date format will vary from site to site. + # http://docs.python.org/library/datetime.html#strftime-strptime-behavior + self.dateformat = "%d %b %Y" + + @staticmethod # must be @staticmethod, don't remove it. + def getSiteDomain(): + # The site domain. Does have www here, if it uses it. + return 'www.wraithbait.com' + + @classmethod + def getSiteExampleURLs(cls): + return "http://"+cls.getSiteDomain()+"/viewstory.php?sid=1234" + + def getSiteURLPattern(self): + return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$" + + ## Getting the chapter list and the meta data, plus 'is adult' checking. + def extractChapterUrlsAndMetadata(self): + + 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=12" + else: + addurl="" + + # index=1 makes sure we see the story chapter index. Some + # sites skip that for one-chapter stories. + url = self.url+'&index=1'+addurl + logger.debug("URL: "+url) + + try: + data = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + if "for adults only" in data: + 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 + pt = soup.find('div', {'id' : 'pagetitle'}) + a = pt.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$")) + self.story.setMetadata('title',stripHTML(a)) + + # Find authorid and URL from... author url. + alist = pt.findAll('a', href=re.compile(r"viewuser.php\?uid=\d+")) + for a in alist: + self.story.addToList('authorId',a['href'].split('=')[1]) + self.story.addToList('authorUrl','http://'+self.host+'/'+a['href']) + self.story.addToList('author',a.string) + + rating=pt.text.split('[')[1].split(']')[0] + self.story.setMetadata('rating', rating) + + st = soup.find('div', {'class' : 'storytitle'}) + a = st.findAll('a', href=re.compile(r'reviews.php\?type=ST&item='+self.story.getMetadata('storyId')+"$"))[1] # second one. + self.story.setMetadata('reviews',stripHTML(a)) + + # Find the chapters: + for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")): + # include author on chapters if multiple authors. + if len(alist) > 1: + add = " by %s"%stripHTML(chapter.findNext('a', href=re.compile(r"viewuser.php\?uid=\d+"))) + else: + add = "" + # just in case there's tags, like <i> in chapter titles. + self.chapterUrls.append((stripHTML(chapter)+add,'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 "" + + info = soup.find('div', {'class' : 'small'}) + + word=info.find(text=re.compile("Word count:")).split(':') + self.story.setMetadata('numWords', word[1]) + + cats = info.findAll('a',href=re.compile(r'browse.php\?type=categories&id=\d')) + for cat in cats: + if "General" != cat.string: + self.story.addToList('category',cat.string) + + chars = info.findAll('a',href=re.compile(r'browse.php\?type=characters&charid=\d')) + for char in chars: + self.story.addToList('characters',char.string) + + completed=info.find(text=re.compile("Completed: Yes")) + if completed != None: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + + date=soup.find('div',{'class' : 'bottom'}) + pd=date.find(text=re.compile("Published:")).string.split(': ') + self.story.setMetadata('datePublished', makeDate(stripHTML(pd[1].split(' U')[0]), self.dateformat)) + self.story.setMetadata('dateUpdated', makeDate(stripHTML(pd[2]), self.dateformat)) + + # <span class="label">Rated:</span> NC-17<br /> etc + labels = soup.findAll('span',{'class':'label'}) + pub=0 + for labelspan in labels: + value = labelspan.nextSibling + label = labelspan.string + + if 'Genres' in label: + genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) + for genre in genres: + self.story.addToList('genre',genre.string) + + if 'Warnings' in label: + warnings = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=2')) + for warning in warnings: + self.story.addToList('warnings',warning.string) + + 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)) + storyas = seriessoup.findAll('a', href=re.compile(r'^viewstory.php\?sid=\d+$')) + i=1 + for a in storyas: + if a['href'] == ('viewstory.php?sid='+self.story.getMetadata('storyId')): + self.setSeries(series_name, i) + self.story.setMetadata('seriesUrl',series_url) + break + i+=1 + + except: + # I find it hard to care if the series parsing fails + pass + + info.extract() + summary = soup.find('div', {'class' : 'content'}) + self.setDescription(url,summary) + + # grab the text for an individual chapter. + def getChapterText(self, url): + + logger.debug('Getting chapter text from: %s' % url) + + soup = bs.BeautifulSoup(self._fetchUrl(url)) + + div = soup.find('div', {'id' : 'story'}) + + if None == div: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url,div) + diff --git a/fanficdownloader/adapters/base_adapter.py b/fanficdownloader/adapters/base_adapter.py new file mode 100644 index 0000000..5bf9c84 --- /dev/null +++ b/fanficdownloader/adapters/base_adapter.py @@ -0,0 +1,605 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import re +import datetime +import time +import logging +import urllib +import urllib2 as u2 +import urlparse as up +import cookielib as cl +from functools import partial +import pickle + +import bs4 + +from ..htmlcleanup import stripHTML +from ..htmlheuristics import replace_br_with_p + +logger = logging.getLogger(__name__) + +try: + from google.appengine.api import apiproxy_stub_map + def urlfetch_timeout_hook(service, call, request, response): + if call != 'Fetch': + return + # Make the default deadline 10 seconds instead of 5. + if not request.has_deadline(): + request.set_deadline(10.0) + + apiproxy_stub_map.apiproxy.GetPreCallHooks().Append( + 'urlfetch_timeout_hook', urlfetch_timeout_hook, 'urlfetch') + logger.info("Hook to make default deadline 10.0 installed.") +except: + pass + #logger.info("Hook to make default deadline 10.0 NOT installed--not using appengine") + +from ..story import Story +from ..gziphttp import GZipProcessor +from ..configurable import Configurable +from ..htmlcleanup import removeEntities, removeAllEntities, stripHTML +from ..exceptions import InvalidStoryURL + +try: + import chardet +except ImportError: + chardet = None + +class BaseSiteAdapter(Configurable): + + @classmethod + def matchesSite(cls,site): + return site in cls.getAcceptDomains() + + @classmethod + def getAcceptDomains(cls): + return [cls.getSiteDomain()] + + def validateURL(self): + return re.match(self.getSiteURLPattern(), self.url) + + @staticmethod + def get_empty_cookiejar(): + return cl.LWPCookieJar() + + @staticmethod + def get_empty_pagecache(): + return {} + + def __init__(self, configuration, url): + Configurable.__init__(self, configuration) + + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + self.password = "" + self.is_adult=False + + self.override_sleep = None + self.cookiejar = self.get_empty_cookiejar() + self.opener = u2.build_opener(u2.HTTPCookieProcessor(self.cookiejar),GZipProcessor()) + # self.opener = u2.build_opener(u2.HTTPCookieProcessor(),GZipProcessor()) + ## Specific UA because too many sites are blocking the default python UA. + self.opener.addheaders = [('User-Agent', self.getConfig('user_agent'))] + self.storyDone = False + self.metadataDone = False + self.story = Story(configuration) + self.story.setMetadata('site',self.getConfigSection()) + self.story.setMetadata('dateCreated',datetime.datetime.now()) + self.chapterUrls = [] # tuples of (chapter title,chapter url) + self.chapterFirst = None + self.chapterLast = None + self.oldchapters = None + self.oldimgs = None + self.oldcover = None # (data of existing cover html, data of existing cover image) + self.calibrebookmark = None + self.logfile = None + + self.pagecache = self.get_empty_pagecache() + + ## order of preference for decoding. + self.decode = ["utf8", + "Windows-1252"] # 1252 is a superset of + # iso-8859-1. Most sites that + # claim to be iso-8859-1 (and + # some that claim to be utf8) + # are really windows-1252. + self._setURL(url) + if not self.validateURL(): + raise InvalidStoryURL(url, + self.getSiteDomain(), + self.getSiteExampleURLs()) + + def get_cookiejar(self): + return self.cookiejar + + def set_cookiejar(self,cj): + self.cookiejar = cj + self.opener = u2.build_opener(u2.HTTPCookieProcessor(self.cookiejar),GZipProcessor()) + self.opener.addheaders = [('User-Agent', self.getConfig('user_agent'))] + + def load_cookiejar(self,filename): + ''' + Needs to be called after adapter create, but before any fetchs + are done. Takes file *name*. + ''' + self.get_cookiejar().load(filename, ignore_discard=True, ignore_expires=True) + + # def save_cookiejar(self,filename): + # ''' + # Assumed to be a FileCookieJar if self.cookiejar set. + # Takes file *name*. + # ''' + # self.get_cookiejar().save(filename, ignore_discard=True, ignore_expires=True) + + # def save_pagecache(self,filename): + # ''' + # Writes pickle of pagecache to file *name* + # ''' + # with open(filename, 'wb') as f: + # pickle.dump(self.get_pagecache(), + # f,protocol=pickle.HIGHEST_PROTOCOL) + + # def load_pagecache(self,filename): + # ''' + # Reads pickle of pagecache from file *name* + # ''' + # with open(filename, 'rb') as f: + # self.set_pagecache(pickle.load(f)) + + def get_pagecache(self): + return self.pagecache + + def set_pagecache(self,d): + self.pagecache=d + + def _get_cachekey(self, url, parameters=None, headers=None): + keylist=[url] + if parameters != None: + keylist.append('&'.join('{0}={1}'.format(key, val) for key, val in sorted(parameters.items()))) + if headers != None: + keylist.append('&'.join('{0}={1}'.format(key, val) for key, val in sorted(headers.items()))) + return '?'.join(keylist) + + def _has_cachekey(self,cachekey): + return self.use_pagecache() and cachekey in self.get_pagecache() + + def _get_from_pagecache(self,cachekey): + if self.use_pagecache(): + return self.get_pagecache().get(cachekey) + else: + return None + + def _set_to_pagecache(self,cachekey,data): + if self.use_pagecache(): + self.get_pagecache()[cachekey] = data + + def use_pagecache(self): + ''' + adapters that will work with the page cache need to implement + this and change it to True. + ''' + return False + + # def story_load(self,filename): + # d = pickle.load(self.story.metadata,filename) + # self.story.metadata = d['metadata'] + # self.chapterUrls = d['chapterlist'] + # self.story.metadataDone = True + + def _setURL(self,url): + self.url = url + self.parsedUrl = up.urlparse(url) + self.host = self.parsedUrl.netloc + self.path = self.parsedUrl.path + self.story.setMetadata('storyUrl',self.url) + +## website encoding(s)--in theory, each website reports the character +## encoding they use for each page. In practice, some sites report it +## incorrectly. Each adapter has a default list, usually "utf8, +## Windows-1252" or "Windows-1252, utf8". The special value 'auto' +## will call chardet and use the encoding it reports if it has +90% +## confidence. 'auto' is not reliable. + def _decode(self,data): + if self.getConfig('website_encodings'): + decode = self.getConfigList('website_encodings') + else: + decode = self.decode + + for code in decode: + try: + #print code + if code == "auto": + if not chardet: + logger.info("chardet not available, skipping 'auto' encoding") + continue + detected = chardet.detect(data) + #print detected + if detected['confidence'] > 0.9: + code=detected['encoding'] + else: + continue + return data.decode(code) + except: + logger.debug("code failed:"+code) + pass + logger.info("Could not decode story, tried:%s Stripping non-ASCII."%decode) + return "".join([x for x in data if ord(x) < 128]) + + # Assumes application/x-www-form-urlencoded. parameters, headers are dict()s + def _postUrl(self, url, + parameters={}, + headers={}, + extrasleep=None, + usecache=True): + ''' + When should cache be cleared or not used? logins... + + extrasleep is primarily for ffnet adapter which has extra + sleeps. Passed into fetchs so it can be bypassed when + cache hits. + ''' + cachekey=self._get_cachekey(url, parameters, headers) + if usecache and self._has_cachekey(cachekey): + logger.debug("#####################################\npagecache HIT: %s"%cachekey) + return self._get_from_pagecache(cachekey) + + logger.debug("#####################################\npagecache MISS: %s"%cachekey) + self.do_sleep(extrasleep) + + ## u2.Request assumes POST when data!=None. Also assumes data + ## is application/x-www-form-urlencoded. + if 'Content-type' not in headers: + headers['Content-type']='application/x-www-form-urlencoded' + if 'Accept' not in headers: + headers['Accept']="text/html,*/*" + req = u2.Request(url, + data=urllib.urlencode(parameters), + headers=headers) + data = self._decode(self.opener.open(req,None,float(self.getConfig('connect_timeout',30.0))).read()) + self._set_to_pagecache(cachekey,data) + return data + + def _fetchUrlRaw(self, url, + parameters=None, + extrasleep=None, + usecache=True): + ''' + When should cache be cleared or not used? logins... + + extrasleep is primarily for ffnet adapter which has extra + sleeps. Passed into fetchs so it can be bypassed when + cache hits. + ''' + cachekey=self._get_cachekey(url, parameters) + if usecache and self._has_cachekey(cachekey): + logger.debug("#####################################\npagecache HIT: %s"%cachekey) + return self._get_from_pagecache(cachekey) + + logger.debug("#####################################\npagecache MISS: %s"%cachekey) + self.do_sleep(extrasleep) + if parameters != None: + data = self.opener.open(url.replace(' ','%20'),urllib.urlencode(parameters),float(self.getConfig('connect_timeout',30.0))).read() + else: + data = self.opener.open(url.replace(' ','%20'),None,float(self.getConfig('connect_timeout',30.0))).read() + self._set_to_pagecache(cachekey,data) + return data + + def set_sleep(self,val): + print("\n===========\n set sleep time %s\n==========="%val) + self.override_sleep = val + + def do_sleep(self,extrasleep=None): + if extrasleep: + time.sleep(float(extrasleep)) + if self.override_sleep: + time.sleep(float(self.override_sleep)) + elif self.getConfig('slow_down_sleep_time'): + time.sleep(float(self.getConfig('slow_down_sleep_time'))) + + # parameters is a dict() + def _fetchUrl(self, url, + parameters=None, + usecache=True, + extrasleep=None): + + excpt=None + for sleeptime in [0, 0.5, 4, 9]: + time.sleep(sleeptime) + try: + return self._decode(self._fetchUrlRaw(url, + parameters=parameters, + usecache=usecache, + extrasleep=extrasleep)) + except u2.HTTPError, he: + excpt=he + if he.code == 404: + logger.warn("Caught an exception reading URL: %s Exception %s."%(unicode(url),unicode(he))) + break # break out on 404 + # except Exception, e: + # excpt=e + # logger.warn("Caught an exception reading URL: %s Exception %s."%(unicode(url),unicode(e))) + + logger.error("Giving up on %s" %url) + logger.exception(excpt) + raise(excpt) + + # Limit chapters to download. Input starts at 1, list starts at 0 + def setChaptersRange(self,first=None,last=None): + if first: + self.chapterFirst=int(first)-1 + if last: + self.chapterLast=int(last)-1 + + # Does the download the first time it's called. + def getStory(self): + if not self.storyDone: + self.getStoryMetadataOnly(get_cover=True) + + for index, (title,url) in enumerate(self.chapterUrls): + if (self.chapterFirst!=None and index < self.chapterFirst) or \ + (self.chapterLast!=None and index > self.chapterLast): + self.story.addChapter(url, + removeEntities(title), + None) + else: + if self.oldchapters and index < len(self.oldchapters): + data = self.utf8FromSoup(None, + self.oldchapters[index], + partial(cachedfetch,self._fetchUrlRaw,self.oldimgs)) + else: + data = self.getChapterText(url) + self.story.addChapter(url, + removeEntities(title), + removeEntities(data)) + self.storyDone = True + + # include image, but no cover from story, add default_cover_image cover. + if self.getConfig('include_images') and \ + not self.story.cover and \ + self.getConfig('default_cover_image'): + self.story.addImgUrl(None, + #self.getConfig('default_cover_image'), + self.story.formatFileName(self.getConfig('default_cover_image'), + self.getConfig('allow_unsafe_filename')), + self._fetchUrlRaw, + cover=True) + + # no new cover, set old cover, if there is one. + if not self.story.cover and self.oldcover: + self.story.oldcover = self.oldcover + + # cheesy way to carry calibre bookmark file forward across update. + if self.calibrebookmark: + self.story.calibrebookmark = self.calibrebookmark + if self.logfile: + self.story.logfile = self.logfile + + return self.story + + def getStoryMetadataOnly(self,get_cover=True): + if not self.metadataDone: + self.doExtractChapterUrlsAndMetadata(get_cover=get_cover) + + if not self.story.getMetadataRaw('dateUpdated'): + self.story.setMetadata('dateUpdated',self.story.getMetadataRaw('datePublished')) + + self.metadataDone = True + return self.story + + def hookForUpdates(self,chaptercount): + "Usually not needed." + return chaptercount + + ############################### + + @staticmethod + def getSiteDomain(): + "Needs to be overriden in each adapter class." + return 'no such domain' + + @classmethod + def getConfigSection(cls): + "Only needs to be overriden if != site domain." + return cls.getSiteDomain() + + @classmethod + def stripURLParameters(cls,url): + "Only needs to be overriden if URL contains more than one parameter" + ## remove any trailing '&' parameters--?sid=999 will be left. + ## that's all that any of the current adapters need or want. + return re.sub(r"&.*$","",url) + + ## URL pattern validation is done *after* picking an adaptor based + ## on domain instead of *as* the adaptor selector so we can offer + ## the user example(s) for that particular site. + ## Override validateURL(self) instead if you need more control. + def getSiteURLPattern(self): + "Used to validate URL. Should be override in each adapter class." + return '^http://'+re.escape(self.getSiteDomain()) + + @classmethod + def getSiteExampleURLs(cls): + """ + Return a string of space separated example URLs. + Needs to be overriden in each adapter class. It's the adapter + writer's responsibility to make sure the example(s) pass the + validateURL method. + """ + return 'no such example' + + def doExtractChapterUrlsAndMetadata(self,get_cover=True): + ''' + There are a handful of adapters that fetch a cover image while + collecting metadata. That isn't needed while *just* + collecting metadata in FG in plugin. Those few will override + this instead of extractChapterUrlsAndMetadata() + ''' + return self.extractChapterUrlsAndMetadata() + + def extractChapterUrlsAndMetadata(self): + "Needs to be overriden in each adapter class. Populates self.story metadata and self.chapterUrls" + pass + + def getChapterText(self, url): + "Needs to be overriden in each adapter class." + pass + + # Just for series, in case we choose to change how it's stored or represented later. + def setSeries(self,name,num): + if self.getConfig('collect_series'): + self.story.setMetadata('series','%s [%s]'%(name, int(num))) + + def setDescription(self,url,svalue): + #print("\n\nsvalue:\n%s\n"%svalue) + if self.getConfig('keep_summary_html'): + if isinstance(svalue,basestring): + # bs4/html5lib add html, header and body tags, which + # we don't want. utf8FromSoup will strip the body tags for us. + svalue = bs4.BeautifulSoup(svalue,"html5lib").body + self.story.setMetadata('description',self.utf8FromSoup(url,svalue)) + else: + self.story.setMetadata('description',stripHTML(svalue)) + #print("\n\ndescription:\n"+self.story.getMetadata('description')+"\n\n") + + def setCoverImage(self,storyurl,imgurl): + if self.getConfig('include_images'): + self.story.addImgUrl(storyurl,imgurl,self._fetchUrlRaw,cover=True, + coverexclusion=self.getConfig('cover_exclusion_regexp')) + + # bs3 & bs4 are different here. + # will move to a bs3 vs bs4 block if there's lots of changes. + def get_attr_keys(self,soup): + if hasattr(soup, '_getAttrMap') and getattr(soup, '_getAttrMap') is not None: + # bs3 + #print "bs3 attrs:%s"%soup._getAttrMap().keys() + return soup._getAttrMap().keys() + elif hasattr(soup, 'attrs') and isinstance(soup.attrs,dict): + #print "bs4 attrs:%s"%soup.attrs.keys() + # bs4 + return soup.attrs.keys() + return [] + + # This gives us a unicode object, not just a string containing bytes. + # (I gave soup a unicode string, you'd think it could give it back...) + # Now also does a bunch of other common processing for us. + def utf8FromSoup(self,url,soup,fetch=None): + if not fetch: + fetch=self._fetchUrlRaw + + acceptable_attributes = ['href','name','class','id'] + if self.getConfig("keep_style_attr"): + acceptable_attributes.append('style') + #print("include_images:"+self.getConfig('include_images')) + if self.getConfig('include_images'): + acceptable_attributes.extend(('src','alt','longdesc')) + for img in soup.findAll('img'): + # some pre-existing epubs have img tags that had src stripped off. + if img.has_key('src'): + (img['src'],img['longdesc'])=self.story.addImgUrl(url,img['src'],fetch, + coverexclusion=self.getConfig('cover_exclusion_regexp')) + + for attr in self.get_attr_keys(soup): + if attr not in acceptable_attributes: + del soup[attr] ## strip all tag attributes except href and name + + try: + # as a generator, each tag will be returned even if there's a + # mismatch at the end. + for t in soup.recursiveChildGenerator(): # findAll(recursive=True): + for attr in self.get_attr_keys(t): + if attr not in acceptable_attributes: + del t[attr] ## strip all tag attributes except acceptable_attributes + + # these are not acceptable strict XHTML. But we do already have + # CSS classes of the same names defined + if t and hasattr(t,'name') and t.name is not None: + if t.name in ('u'): + t['class']=t.name + t.name='span' + if t.name in ('center'): + t['class']=t.name + t.name='div' + # removes paired, but empty non paragraph tags. + if t.name not in ('p') and t.string != None and len(t.string.strip()) == 0 : + t.extract() + except AttributeError, ae: + print("Error parsing HTML, probably poor input HTML. %s"%ae) + + retval = unicode(soup) + + if self.getConfig('nook_img_fix') and not self.getConfig('replace_br_with_p'): + # if the <img> tag doesn't have a div or a p around it, + # nook gets confused and displays it on every page after + # that under the text for the rest of the chapter. + retval = re.sub(r"(?!<(div|p)>)\s*(?P<imgtag><img[^>]+>)\s*(?!</(div|p)>)", + "<div>\g<imgtag></div>",retval) + + # Don't want body tags in chapter html--writers add them. + # This is primarily for epub updates. + retval = re.sub(r"</?body[^>]*>\r?\n?","",retval) + + if self.getConfig("replace_br_with_p"): + # Apply heuristic processing to replace <br> paragraph + # breaks with <p> tags. + retval = replace_br_with_p(retval) + + if self.getConfig('replace_hr'): + # replacing a self-closing tag with a container tag in the + # soup is more difficult than it first appears. So cheat. + retval = retval.replace("<hr />","<div class='center'>* * *</div>") + + return retval + + def make_soup(self,data): + ''' + Convenience method for getting a bs4 soup. Older and + non-updated adapters call the included bs3 library themselves. + ''' + return bs4.BeautifulSoup(data,'html5lib') + +def cachedfetch(realfetch,cache,url): + if url in cache: + return cache[url] + else: + return realfetch(url) + +fullmon = {u"January":u"01", u"February":u"02", u"March":u"03", u"April":u"04", u"May":u"05", + u"June":u"06","July":u"07", u"August":u"08", u"September":u"09", u"October":u"10", + u"November":u"11", u"December":u"12" } + +def makeDate(string,dateform): + # Surprise! Abstracting this turned out to be more useful than + # just saving bytes. + + # fudge english month names for people who's locale is set to + # non-english. All our current sites date in english, even if + # there's non-english content. -- ficbook.net now makes that a + # lie. It has to do something even more complicated to get + # Russian month names correct everywhere. + do_abbrev = "%b" in dateform + + if u"%B" in dateform or do_abbrev: + dateform = dateform.replace(u"%B",u"%m").replace(u"%b",u"%m") + for (name,num) in fullmon.items(): + if do_abbrev: + name = name[:3] # first three for abbrev + if name in string: + string = string.replace(name,num) + break + + return datetime.datetime.strptime(string.encode('utf-8'),dateform.encode('utf-8')) + diff --git a/fanficdownloader/adapters/base_efiction_adapter.py b/fanficdownloader/adapters/base_efiction_adapter.py new file mode 100644 index 0000000..403a28a --- /dev/null +++ b/fanficdownloader/adapters/base_efiction_adapter.py @@ -0,0 +1,426 @@ +# -*- coding: utf-8 -*- + +# Copyright 2014 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Software: eFiction +# import time +# import urllib +import logging +logger = logging.getLogger(__name__) +import re +import urllib2 + +from .. import BeautifulSoup as bs +from ..htmlcleanup import stripHTML +from .. import exceptions as exceptions + +from base_adapter import BaseSiteAdapter, makeDate + +""" +This is a generic adapter for eFiction based archives (see +http://fanlore.org/wiki/List_of_eFiction_Archives for a list). + +Most of them share common traits: + * No HTTPS + * 'www.' is optional + * Default story template is 'viewstory.php' with arguments + * 'sid' the storyId + * 'chapter' for chapters (will be thrown away anyway by + stripURLParameters in base_adapter + Use Printable version which is easier to parse and has everything in one + page and cache between extractChapterUrlsAndMetadata and getChapterText +""" + +# PHP constants +_RUSERSONLY = 'Registered Users Only' +_NOSUCHACCOUNT = "There is no such account on our website" +_WRONGPASSWORD = "That password doesn't match the one in our database" +_USERACCOUNT = 'Member Account' + +# Regular expressions +_REGEX_WARING_PARAM = re.compile("warning=(?P<warningId>\d+)") +_REGEX_CHAPTER_B = re.compile("^(?P<chapterId>\d+)\.") +_REGEX_CHAPTER_PARAM = re.compile("chapter=(?P<chapterId>\d+)$") +_REGEX_CHAPTER_FRAGMENT = re.compile("^#(?P<chapterId>\d+)$") +_REGEX_DOESNT_START_WITH_HTTP = re.compile("^(?!http)") + +class BaseEfictionAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + self.story.setMetadata('siteabbrev',self.getSiteAbbrev()) + self.decode = self.getEncoding() + storyId = re.compile(self.getSiteURLPattern()).match(self.url).group('storyId') + self.story.setMetadata('storyId', storyId) + self._setURL(self.getViewStoryUrl(storyId)) + self.triedLoggingIn = False + self.triedAcceptWarnings = False + self.username = "NoneGiven" # if left empty, site doesn't return any message at all. + + @classmethod + def getAcceptDomains(cls): + return [cls.getSiteDomain(),'www.' + cls.getSiteDomain()] + + @classmethod + def getSiteExampleURLs(cls): + return cls.getViewStoryUrl('1234') + ' ' + cls.getViewStoryUrl('1234') + '&chapter=2' + + @classmethod + def getSiteURLPattern(self): + return r"http://(www\.)?%s%s/%s\?sid=(?P<storyId>\d+)" % (self.getSiteDomain(), self.getPathToArchive(), self.getViewStoryPhpName()) + + @classmethod + def getEncoding(cls): + """ + Return an array of character encodings to try to decode the HTML with + """ + return ["Windows-1252", "utf8"] + + + @classmethod + def getPathToArchive(cls): + """ + Get the path segment of the archive, default '/'. + + In many cases, it's '/archive' or '/fanfiction' + """ + return "" + + @classmethod + def getViewStoryPhpName(cls): + """ + Get the name of the story PHP script, by default 'viewstory.php' + """ + return "viewstory.php" + + @classmethod + def getViewUserPhpName(cls): + """ + Get the name of the user PHP script, by default 'viewuser.php' + """ + return "viewuser.php" + + @classmethod + def getUserPhpName(cls): + """ + Get the name of the user PHP script, by default 'viewuser.php' + """ + return "user.php" + + @classmethod + def getDateFormat(self): + """ + Describe the date format of this site in terms of strftime + See http://docs.python.org/library/datetime.html#strftime-strptime-behavior + """ + return "%d %b %Y" + + @classmethod + def getUrlForPhp(self, php): + return "http://%s%s/%s" % (self.getSiteDomain(), self.getPathToArchive(), php) + + @classmethod + def getViewStoryUrl(self, storyId): + """ + Get the URL to a user page on this site. + """ + return "%s?sid=%s" % (self.getUrlForPhp(self.getViewStoryPhpName()), storyId) + + @classmethod + def getViewUserUrl(self, userId): + """ + Get the URL to a user page on this site. + """ + return "%s?sid=%s" % (self.getUrlForPhp(self.getViewUserPhpName()), userId) + + @classmethod + def getLoginUrl(self): + """ + Get the URL to the login page on this site. + """ + return "%s?action=login" % self.getUrlForPhp(self.getUserPhpName()) + + @classmethod + def getMessageRegisteredUsersOnly(self): + """ + Constant _RUSERSONLY defined in languages/en.php + """ + return _RUSERSONLY + + @classmethod + def getMessageThereIsNoSuchAccount(self): + """ + Constant _NOSUCHACCOUNT defined in languages/en.php + """ + return _NOSUCHACCOUNT + + @classmethod + def getMessageWrongPassword(self): + """ + Constant _WRONGPASSWORD defined in languages/en.php + """ + return _WRONGPASSWORD + + @classmethod + def getMessageMemberAccount(self): + """ + Constant _USERACCOUNT defined in languages/en.php + """ + return _USERACCOUNT + + ## Login seems to be reasonably standard across eFiction sites. + @classmethod + def needToLoginCheck(self, html): + """ + Return whether the HTML contains either of _RUSERSONLY, _NOSUCHACCOUNT or _WRONGPASSWORD + """ + return getMessageRegisteredUsersOnly() in html \ + or getMessageThereIsNoSuchAccount in html \ + or getMessageWrongPassword in html + + def _fetch_to_soup(self, url): + """ + Fetch a HTML document, fix it and parse it to BeautifulSoup. + + Replaces old characters, broken meta-tags, non-self-closing hr/br. + + Makes image links absolute so they can be downloaded + """ + try: + html = self._fetchUrl(url) + except urllib2.HTTPError, e: + if e.code == 404: + raise exceptions.StoryDoesNotExist(self.url) + else: + raise e + + # Some site use old, old-school Comments <!- comment -> (single dash) + html = re.sub("<!-.+?->", "", html) + + # There is a problem with meta tags on some sites where spaces aren't + # properly encoded + html = re.sub("<meta[^<>]+>(.*</meta>)?", "", html) + + # fix non-closing hr/br + html = html.replace("<hr>", "<hr/>") + html = html.replace("<br>", "<br/>") + + soup = bs.BeautifulSoup(html, selfClosingTags=['br','hr']) # otherwise soup eats the br/hr tags.) + + ## fix all local image 'src' to absolute + for img in soup.findAll("img", {"src": _REGEX_DOESNT_START_WITH_HTTP}): + # TODO handle '../../' and so on + if img['src'].startswith('/'): + img['src'] = img['src'][1:] + img['src'] = "http://%s%s/%s" % (self.getSiteDomain(), self.getPathToArchive(), img['src']) + + return soup + + 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' + + logger.debug("Will now login to URL (%s) as (%s)" % (self.getLoginUrl(), params['penname'])) + + d = self._fetchUrl(self.getLoginUrl(), params) + + if self.getMessageMemberAccount() not in d : #Member Account + logger.info("Failed to login to URL <%s> as '%s'" % (self.getLoginUrl(), params['penname'])) + raise exceptions.FailedToLogin(url, params['penname']) + return False + else: + return True + + def handleMetadataPair(self, key, value): + """ + Handles a key-value pair of story metadata. + + Returns straight away if the value is 'None' (that's a string) + + Can be overridden by subclasses:: + def handleMetadataPair(self, key, value): + if key == 'MyCustomKey': + self.story.setMetadata('somekye', value) + else: + super(NameOfMyAdapter, self).handleMetadata(key, value) + """ + # logger.debug("metadata: '%s' == '%s'" % (key, value)) + if value == 'None': + return + elif key == 'Summary': + self.setDescription(self.url, value) + elif 'Genre' in key: + for val in re.split("\s*,\s*", value): + self.story.addToList('genre', val) + elif 'Warning' in key: + for val in re.split("\s*,\s*", value): + self.story.addToList('warnings', val) + elif 'Characters' in key: + for val in re.split("\s*,\s*", value): + self.story.addToList('characters', val) + elif 'Categories' in key: + for val in re.split("\s*,\s*", value): + self.story.addToList('category', val) + elif 'Challenges' in key: + for val in re.split("\s*,\s*", value): + # TODO this should be an official field I guess + self.story.addToList('challenge', val) + elif key == 'Chapters': + self.story.setMetadata('numChapters', int(value)) + elif key == 'Rating': + self.story.setMetadata('rating', value) + elif key == 'Word count': + self.story.setMetadata('numWords', value) + elif key == 'Completed': + if 'Yes' in value: + self.story.setMetadata('status', 'Completed') + else: + self.story.setMetadata('status', 'In-Progress') + elif key == 'Read': + # TODO this should be an official field I guess + self.story.setMetadata('readings', value) + elif key == 'Published': + self.story.setMetadata('datePublished', makeDate(value, self.getDateFormat())) + elif key == 'Updated': + self.story.setMetadata('dateUpdated', makeDate(value, self.getDateFormat())) + elif key == 'Pairing': + for val in re.split("\s*,\s*", value): + self.story.addToList('ships', val) + elif key == 'Series': + ## TODO is not a link in the printable view, so no seriesURL possible + self.story.setMetadata('series', value) + else: + logger.info("Unhandled metadata pair: '%s' : '%s'" % (key, value)) + + def extractChapterUrlsAndMetadata(self): + printUrl = self.url + '&action=printable&textsize=0&chapter=' + if self.getConfig('bulk_load'): + printUrl += 'all' + else: + printUrl += '1' + + + soup = self._fetch_to_soup(printUrl) + + ## Handle warnings and login checks + errorDiv = soup.find("div", "errortext") + while errorDiv is not None: + if self.getMessageRegisteredUsersOnly() in errorDiv.prettify(): + if not self.triedLoggingIn: + self.performLogin(self.url) + soup = self._fetch_to_soup(printUrl) + errorDiv = soup.find("div", "errortext") + self.triedLoggingIn = True + else: + raise exceptions.FailedToLogin(self.url, str(errorDiv)) + else: + warningLink = errorDiv.find("a") + if warningLink is not None and ( \ + 'ageconsent' in warningLink['href'] \ + or 'warning' in warningLink['href']): + if not self.triedAcceptWarnings: + if not (self.is_adult or self.getConfig("is_adult")): + raise exceptions.AdultCheckRequired(self.url) + # XXX Using this method, we're independent of # getHighestWarningLevel + printUrl += "&ageconsent=ok&warning=%s" % (_REGEX_WARING_PARAM.search(warningLink['href']).group(1)) + # printUrl += "&ageconsent=ok&warning=%s" % self.getHighestWarningLevel() + soup = self._fetch_to_soup(printUrl) + errorDiv = soup.find("div", "errortext") + self.triedAcceptWarnings = True + else: + raise exception.FailedToDownload(self.url, str(errorDiv)) + else: + raise exception.FailedToDownload(self.url, str(errorDiv)) + + # title and author + pagetitleDiv = soup.find("div", {"id": "pagetitle"}) + if pagetitleDiv.find('a') is None: + raise execeptions.FailedToDownload("Couldn't find title and author") + self.story.setMetadata('title', pagetitleDiv.find("a").text) + authorLink = pagetitleDiv.findAll("a")[1] + self.story.setMetadata('author', authorLink.text) + self.story.setMetadata('authorId', re.search("\d+", authorLink['href']).group(0)) + self.story.setMetadata('authorUrl', self.getViewUserUrl(self.story.getMetadata('authorId'))) + + ## Parse the infobox + labelSpans = soup.find("div", "infobox").find("div", "content").findAll("span", "label") + for labelSpan in labelSpans: + valueStr = "" + nextEl = labelSpan.nextSibling + while nextEl is not None and not (\ + type(nextEl) is bs.Tag \ + and nextEl.name == "span" \ + and nextEl['class'] =='label' \ + ): + ## must string copy nextEl or nextEl will change trees + if (type(nextEl) is bs.Tag): + valueStr += nextEl.prettify() + else: + valueStr += str(nextEl) + nextEl = nextEl.nextSibling + key = labelSpan.text.strip() + + ## strip trailing line breaks + valueStr = re.sub("<br />", "", valueStr) + + ## strip trailing colons + key = re.sub("\s*:\s*$", "", key) + + ## strip whitespace + key = key.strip() + valueStr = stripHTML(valueStr) + + self.handleMetadataPair(key, valueStr) + + ## Chapter URLs + + # If we didn't bulk-load the whole chapter we now need to load + # the non-printable HTML version of the landing page (i.e. the story + # URL to get the Chapter titles + if not self.getConfig('bulk_load'): + soup = self._fetch_to_soup(self.url + '&index=1') + + chapterLinks = [] + for b in soup.findAll("b", text=_REGEX_CHAPTER_B): + chapterId = _REGEX_CHAPTER_B.search(b).group('chapterId') + chapterLink = b.findNext("a") + chapterLink['href'] = "%s&chapter=%s" % (self.url, chapterId) + self.chapterUrls.append((chapterLink.text, chapterLink['href'])) + + ## Store reference to soup for getChapterText + self.html = soup + + def getChapterText(self, url): + if self.getConfig('bulk_load'): + logger.debug('Cached chapter text from <%s>' % url) + anchor = _REGEX_CHAPTER_PARAM.search(url).group(1) + chapterDiv = self.html.find("a", {"name": anchor}).parent.findNext("div", "chapter") + else: + logger.debug('Download chapter text from <%s>' % url) + soup = self._fetch_to_soup(url + '&action=printable') + chapterDiv = soup.find("div", "chapter") + return self.utf8FromSoup(self.url, chapterDiv) + +def getClass(): + return BaseEfictionAdapter diff --git a/fanficdownloader/configurable.py b/fanficdownloader/configurable.py new file mode 100644 index 0000000..9b6d35f --- /dev/null +++ b/fanficdownloader/configurable.py @@ -0,0 +1,196 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import ConfigParser, re + +# All of the writers(epub,html,txt) and adapters(ffnet,twlt,etc) +# inherit from Configurable. The config file(s) uses ini format: +# [sections] with key:value settings. +# +# [defaults] +# titlepage_entries: category,genre, status +# [www.whofic.com] +# titlepage_entries: category,genre, status,dateUpdated,rating +# [epub] +# titlepage_entries: category,genre, status,datePublished,dateUpdated,dateCreated +# [www.whofic.com:epub] +# titlepage_entries: category,genre, status,datePublished +# [overrides] +# titlepage_entries: category + +class Configuration(ConfigParser.SafeConfigParser): + + def __init__(self, site, fileform): + ConfigParser.SafeConfigParser.__init__(self) + self.sectionslist = ['defaults'] + + if site.startswith("www."): + sitewith = site + sitewithout = site.replace("www.","") + else: + sitewith = "www."+site + sitewithout = site + + self.addConfigSection(sitewith) + self.addConfigSection(sitewithout) + if fileform: + self.addConfigSection(fileform) + self.addConfigSection(sitewith+":"+fileform) + self.addConfigSection(sitewithout+":"+fileform) + self.addConfigSection("overrides") + + self.listTypeEntries = [ + 'category', + 'genre', + 'characters', + 'ships', + 'warnings', + 'extratags', + 'author', + 'authorId', + 'authorUrl', + 'lastupdate', + ] + + self.validEntries = self.listTypeEntries + [ + 'series', + 'seriesUrl', + 'language', + 'status', + 'datePublished', + 'dateUpdated', + 'dateCreated', + 'rating', + 'numChapters', + 'numWords', + 'site', + 'storyId', + 'title', + 'storyUrl', + 'description', + 'formatname', + 'formatext', + 'siteabbrev', + 'version', + # internal stuff. + 'authorHTML', + 'seriesHTML', + 'langcode', + 'output_css', + ] + + def addConfigSection(self,section): + self.sectionslist.insert(0,section) + + def isListType(self,key): + return key in self.listTypeEntries or self.hasConfig("include_in_"+key) + + def isValidMetaEntry(self, key): + return key in self.getValidMetaList() + + def getValidMetaList(self): + return self.validEntries + self.getConfigList("extra_valid_entries") + + # used by adapters & writers, non-convention naming style + def hasConfig(self, key): + return self.has_config(self.sectionslist, key) + + def has_config(self, sections, key): + for section in sections: + try: + self.get(section,key) + #print("found %s in section [%s]"%(key,section)) + return True + except: + try: + self.get(section,"add_to_"+key) + #print("found add_to_%s in section [%s]"%(key,section)) + return True + except: + pass + + return False + + # used by adapters & writers, non-convention naming style + def getConfig(self, key, default=""): + return self.get_config(self.sectionslist,key,default) + + def get_config(self, sections, key, default=""): + val = default + for section in sections: + try: + val = self.get(section,key) + if val and val.lower() == "false": + val = False + #print "getConfig(%s)=[%s]%s" % (key,section,val) + break + except (ConfigParser.NoOptionError, ConfigParser.NoSectionError), e: + pass + + for section in sections[::-1]: + # 'martian smiley' [::-1] reverses list by slicing whole list with -1 step. + try: + val = val + self.get(section,"add_to_"+key) + #print "getConfig(add_to_%s)=[%s]%s" % (key,section,val) + except (ConfigParser.NoOptionError, ConfigParser.NoSectionError), e: + pass + + return val + + # split and strip each. + def get_config_list(self, sections, key): + vlist = re.split(r'(?<!\\),',self.get_config(sections,key)) # don't split on \, + vlist = filter( lambda x : x !='', [ v.strip().replace('\,',',') for v in vlist ]) + #print "vlist("+key+"):"+str(vlist) + return vlist + + # used by adapters & writers, non-convention naming style + def getConfigList(self, key): + return self.get_config_list(self.sectionslist, key) + +# extended by adapter, writer and story for ease of calling configuration. +class Configurable(object): + + def __init__(self, configuration): + self.configuration = configuration + + def isListType(self,key): + return self.configuration.isListType(key) + + def isValidMetaEntry(self, key): + return self.configuration.isValidMetaEntry(key) + + def getValidMetaList(self): + return self.configuration.getValidMetaList() + + def hasConfig(self, key): + return self.configuration.hasConfig(key) + + def has_config(self, sections, key): + return self.configuration.has_config(sections, key) + + def getConfig(self, key, default=""): + return self.configuration.getConfig(key,default) + + def get_config(self, sections, key, default=""): + return self.configuration.get_config(sections,key,default) + + def getConfigList(self, key): + return self.configuration.getConfigList(key) + + def get_config_list(self, sections, key): + return self.configuration.get_config_list(sections,key) diff --git a/fanficdownloader/epubutils.py b/fanficdownloader/epubutils.py new file mode 100644 index 0000000..2e1b041 --- /dev/null +++ b/fanficdownloader/epubutils.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2014, Jim Miller' +__docformat__ = 'restructuredtext en' + +import logging +logger = logging.getLogger(__name__) + +import re, os, traceback +from zipfile import ZipFile +from xml.dom.minidom import parseString + +import bs4 as bs + +def get_dcsource(inputio): + return get_update_data(inputio,getfilecount=False,getsoups=False)[0] + +def get_dcsource_chaptercount(inputio): + return get_update_data(inputio,getfilecount=True,getsoups=False)[:2] # (source,filecount) + +def get_update_data(inputio, + getfilecount=True, + getsoups=True): + epub = ZipFile(inputio, 'r') # works equally well with inputio as a path or a blob + + ## Find the .opf file. + container = epub.read("META-INF/container.xml") + containerdom = parseString(container) + rootfilenodelist = containerdom.getElementsByTagName("rootfile") + rootfilename = rootfilenodelist[0].getAttribute("full-path") + + contentdom = parseString(epub.read(rootfilename)) + firstmetadom = contentdom.getElementsByTagName("metadata")[0] + try: + source=firstmetadom.getElementsByTagName("dc:source")[0].firstChild.data.encode("utf-8") + except: + source=None + + ## Save the path to the .opf file--hrefs inside it are relative to it. + relpath = get_path_part(rootfilename) + + oldcover = None + calibrebookmark = None + logfile = None + # Looking for pre-existing cover. + for item in contentdom.getElementsByTagName("reference"): + if item.getAttribute("type") == "cover": + # there is a cover (x)html file, save the soup for it. + href=relpath+item.getAttribute("href") + oldcoverhtmlhref = href + oldcoverhtmldata = epub.read(href) + oldcoverhtmltype = "application/xhtml+xml" + for item in contentdom.getElementsByTagName("item"): + if( relpath+item.getAttribute("href") == oldcoverhtmlhref ): + oldcoverhtmltype = item.getAttribute("media-type") + break + soup = bs.BeautifulSoup(oldcoverhtmldata.decode("utf-8")) + src = None + # first img or image tag. + imgs = soup.findAll('img') + if imgs: + src = get_path_part(href)+imgs[0]['src'] + else: + imgs = soup.findAll('image') + if imgs: + src=get_path_part(href)+imgs[0]['xlink:href'] + + if not src: + continue + try: + # remove all .. and the path part above it, if present. + # Mostly for epubs edited by Sigil. + src = re.sub(r"([^/]+/\.\./)","",src) + #print("epubutils: found pre-existing cover image:%s"%src) + oldcoverimghref = src + oldcoverimgdata = epub.read(src) + for item in contentdom.getElementsByTagName("item"): + if( relpath+item.getAttribute("href") == oldcoverimghref ): + oldcoverimgtype = item.getAttribute("media-type") + break + oldcover = (oldcoverhtmlhref,oldcoverhtmltype,oldcoverhtmldata,oldcoverimghref,oldcoverimgtype,oldcoverimgdata) + except Exception as e: + logger.warn("Cover Image %s not found"%src) + logger.warn("Exception: %s"%(unicode(e))) + traceback.print_exc() + + filecount = 0 + soups = [] # list of xhmtl blocks + images = {} # dict() longdesc->data + if getfilecount: + # spin through the manifest--only place there are item tags. + for item in contentdom.getElementsByTagName("item"): + # First, count the 'chapter' files. FFDL uses file0000.xhtml, + # but can also update epubs downloaded from Twisting the + # Hellmouth, which uses chapter0.html. + if( item.getAttribute("media-type") == "application/xhtml+xml" ): + href=relpath+item.getAttribute("href") + #print("---- item href:%s path part: %s"%(href,get_path_part(href))) + if re.match(r'.*/log_page\.x?html',href): + try: + logfile = epub.read(href).decode("utf-8") + except: + pass # corner case I bumped into while testing. + if re.match(r'.*/(file|chapter)\d+\.x?html',href): + if getsoups: + soup = bs.BeautifulSoup(epub.read(href).decode("utf-8")) + for img in soup.findAll('img'): + newsrc='' + longdesc='' + try: + newsrc=get_path_part(href)+img['src'] + # remove all .. and the path part above it, if present. + # Mostly for epubs edited by Sigil. + newsrc = re.sub(r"([^/]+/\.\./)","",newsrc) + longdesc=img['longdesc'] + data = epub.read(newsrc) + images[longdesc] = data + img['src'] = img['longdesc'] + except Exception as e: + logger.warn("Image %s not found!\n(originally:%s)"%(newsrc,longdesc)) + logger.warn("Exception: %s"%(unicode(e))) + traceback.print_exc() + soup = soup.find('body') + # ffdl epubs have chapter title h3 + h3 = soup.find('h3') + if h3: + h3.extract() + # TtH epubs have chapter title h2 + h2 = soup.find('h2') + if h2: + h2.extract() + + for skip in soup.findAll(attrs={'class':'skip_on_ffdl_update'}): + skip.extract() + + soups.append(soup) + + filecount+=1 + + try: + calibrebookmark = epub.read("META-INF/calibre_bookmarks.txt") + except: + pass + + #for k in images.keys(): + #print("\tlongdesc:%s\n\tData len:%s\n"%(k,len(images[k]))) + return (source,filecount,soups,images,oldcover,calibrebookmark,logfile) + +def get_path_part(n): + relpath = os.path.dirname(n) + if( len(relpath) > 0 ): + relpath=relpath+"/" + return relpath + +def get_story_url_from_html(inputio,_is_good_url=None): + + #print("get_story_url_from_html called") + epub = ZipFile(inputio, 'r') # works equally well with inputio as a path or a blob + + ## Find the .opf file. + container = epub.read("META-INF/container.xml") + containerdom = parseString(container) + rootfilenodelist = containerdom.getElementsByTagName("rootfile") + rootfilename = rootfilenodelist[0].getAttribute("full-path") + + contentdom = parseString(epub.read(rootfilename)) + #firstmetadom = contentdom.getElementsByTagName("metadata")[0] + + ## Save the path to the .opf file--hrefs inside it are relative to it. + relpath = get_path_part(rootfilename) + + # spin through the manifest--only place there are item tags. + for item in contentdom.getElementsByTagName("item"): + # First, count the 'chapter' files. FFDL uses file0000.xhtml, + # but can also update epubs downloaded from Twisting the + # Hellmouth, which uses chapter0.html. + #print("---- item:%s"%item) + if( item.getAttribute("media-type") == "application/xhtml+xml" ): + filehref=relpath+item.getAttribute("href") + soup = bs.BeautifulSoup(epub.read(filehref).decode("utf-8")) + for link in soup.findAll('a',href=re.compile(r'^http.*')): + ahref=link['href'] + #print("href:(%s)"%ahref) + # hack for bad ficsaver ffnet URLs. + m = re.match(r"^http://www.fanfiction.net/s(?P<id>\d+)//$",ahref) + if m != None: + ahref="http://www.fanfiction.net/s/%s/1/"%m.group('id') + if _is_good_url == None or _is_good_url(ahref): + return ahref + return None diff --git a/fanficdownloader/exceptions.py b/fanficdownloader/exceptions.py new file mode 100644 index 0000000..2960add --- /dev/null +++ b/fanficdownloader/exceptions.py @@ -0,0 +1,86 @@ +# -*- 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. +# + +## A few exceptions for different things for adapters + +class FailedToDownload(Exception): + def __init__(self,error): + self.error=error + + def __str__(self): + return self.error + +class InvalidStoryURL(Exception): + def __init__(self,url,domain,example): + self.url=url + self.domain=domain + self.example=example + + def __str__(self): + return "Bad Story URL: (%s) for site: (%s) Example: (%s)" % (self.url, self.domain, self.example) + +class FailedToLogin(Exception): + def __init__(self,url, username, passwdonly=False): + self.url=url + self.username=username + self.passwdonly=passwdonly + + def __str__(self): + if self.passwdonly: + return "URL Failed, password required: (%s) " % (self.url) + else: + return "Failed to Login for URL: (%s) with username: (%s)" % (self.url, self.username) + +class AdultCheckRequired(Exception): + def __init__(self,url): + self.url=url + + def __str__(self): + return "Story requires confirmation of adult status: (%s)" % self.url + +class StoryDoesNotExist(Exception): + def __init__(self,url): + self.url=url + + def __str__(self): + return "Story does not exist: (%s)" % self.url + +class UnknownSite(Exception): + def __init__(self,url,supported_sites_list): + self.url=url + self.supported_sites_list=supported_sites_list + self.supported_sites_list.sort() + + def __str__(self): + return "Unknown Site(%s). Supported sites: (%s)" % (self.url, ", ".join(self.supported_sites_list)) + +class FailedToWriteOutput(Exception): + def __init__(self,error): + self.error=error + + def __str__(self): + return self.error + +class RegularExpresssionFailed(Exception): + def __init__(self,error,regex,line): + self.error=error + self.regex=regex + self.line=line + + def __str__(self): + return "Regular Expression Error '%s' in regex '%s' in line '%s'"%(self.error,self.regex,self.line) + diff --git a/fanficdownloader/geturls.py b/fanficdownloader/geturls.py new file mode 100644 index 0000000..b66202d --- /dev/null +++ b/fanficdownloader/geturls.py @@ -0,0 +1,175 @@ +# -*- 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 re +import urlparse +import urllib2 as u2 + +from BeautifulSoup import BeautifulSoup +from gziphttp import GZipProcessor + +import adapters +from configurable import Configuration +from exceptions import UnknownSite + +def get_urls_from_page(url,configuration=None,normalize=False): + + if not configuration: + configuration = Configuration("test1.com","EPUB") + + data = None + adapter = None + try: + adapter = adapters.getAdapter(configuration,url,anyurl=True) + + # special stuff to log into archiveofourown.org, if possible. + # Unlike most that show the links to 'adult' stories, but protect + # them, AO3 doesn't even show them if not logged in. Only works + # with saved user/pass--not going to prompt for list. + if 'archiveofourown.org' in url: + if adapter.getConfig("username"): + if adapter.getConfig("is_adult"): + if '?' in url: + addurl = "&view_adult=true" + else: + addurl = "?view_adult=true" + else: + addurl="" + # just to get an authenticity_token. + data = adapter._fetchUrl(url+addurl) + # login the session. + adapter.performLogin(url,data) + # get the list page with logged in session. + + # this way it uses User-Agent or other special settings. Only AO3 + # is doing login. + data = adapter._fetchUrl(url,usecache=False) + except UnknownSite: + # no adapter with anyurl=True, must be a random site. + opener = u2.build_opener(u2.HTTPCookieProcessor(),GZipProcessor()) + data = opener.open(url).read() + + # kludge because I don't see it on enough sites to be worth generalizing yet. + restrictsearch=None + if 'scarvesandcoffee.net' in url: + restrictsearch=('div',{'id':'mainpage'}) + + return get_urls_from_html(data,url,configuration,normalize,restrictsearch) + +def get_urls_from_html(data,url=None,configuration=None,normalize=False,restrictsearch=None): + + normalized = [] # normalized url + retlist = [] # orig urls. + + if not configuration: + configuration = Configuration("test1.com","EPUB") + + soup = BeautifulSoup(data) + if restrictsearch: + soup = soup.find(*restrictsearch) + #print("restrict search:%s"%soup) + + for a in soup.findAll('a'): + if a.has_key('href'): + #print("a['href']:%s"%a['href']) + href = form_url(url,a['href']) + #print("1 urlhref:%s"%href) + # this (should) catch normal story links, some javascript + # 'are you old enough' links, and 'Report This' links. + # The 'normalized' set prevents duplicates. + if 'story.php' in a['href']: + #print("trying:%s"%a['href']) + m = re.search(r"(?P<sid>(view)?story\.php\?(sid|psid|no|story|stid)=\d+)",a['href']) + if m != None: + href = form_url(a['href'] if '//' in a['href'] else url, + m.group('sid')) + + try: + href = href.replace('&index=1','') + #print("2 urlhref:%s"%href) + adapter = adapters.getAdapter(configuration,href) + #print("found adapter") + if adapter.story.getMetadata('storyUrl') not in normalized: + normalized.append(adapter.story.getMetadata('storyUrl')) + retlist.append(href) + except Exception, e: + #print e + pass + + if normalize: + return normalized + else: + return retlist + +def get_urls_from_text(data,configuration=None,normalize=False): + + normalized = [] # normalized url + retlist = [] # orig urls. + data=unicode(data) + + if not configuration: + configuration = Configuration("test1.com","EPUB") + + for href in re.findall('http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', data): + # this (should) catch normal story links, some javascript + # 'are you old enough' links, and 'Report This' links. + # The 'normalized' set prevents duplicates. + if 'story.php' in href: + m = re.search(r"(?P<sid>(view)?story\.php\?(sid|psid|no|story|stid)=\d+)",href) + if m != None: + href = form_url(href,m.group('sid')) + try: + href = href.replace('&index=1','') + adapter = adapters.getAdapter(configuration,href) + if adapter.story.getMetadata('storyUrl') not in normalized: + normalized.append(adapter.story.getMetadata('storyUrl')) + retlist.append(href) + except: + pass + + if normalize: + return normalized + else: + return retlist + +def form_url(parenturl,url): + url = url.strip() # ran across an image with a space in the + # src. Browser handled it, so we'd better, too. + + if "//" in url or parenturl == None: + returl = url + else: + parsedUrl = urlparse.urlparse(parenturl) + if url.startswith("/") : + returl = urlparse.urlunparse( + (parsedUrl.scheme, + parsedUrl.netloc, + url, + '','','')) + else: + toppath="" + if parsedUrl.path.endswith("/"): + toppath = parsedUrl.path + else: + toppath = parsedUrl.path[:parsedUrl.path.rindex('/')] + returl = urlparse.urlunparse( + (parsedUrl.scheme, + parsedUrl.netloc, + toppath + '/' + url, + '','','')) + return returl + diff --git a/fanficdownloader/gziphttp.py b/fanficdownloader/gziphttp.py new file mode 100644 index 0000000..76049ee --- /dev/null +++ b/fanficdownloader/gziphttp.py @@ -0,0 +1,38 @@ +## Borrowed from http://techknack.net/python-urllib2-handlers/ + +import urllib2 +from gzip import GzipFile +from StringIO import StringIO + +class GZipProcessor(urllib2.BaseHandler): + """A handler to add gzip capabilities to urllib2 requests + """ + def http_request(self, req): + req.add_header("Accept-Encoding", "gzip") + return req + https_request = http_request + + def http_response(self, req, resp): + #print("Content-Encoding:%s"%resp.headers.get("Content-Encoding")) + if resp.headers.get("Content-Encoding") == "gzip": + gz = GzipFile( + fileobj=StringIO(resp.read()), + mode="r" + ) +# resp.read = gz.read +# resp.readlines = gz.readlines +# resp.readline = gz.readline +# resp.next = gz.next + old_resp = resp + resp = urllib2.addinfourl(gz, old_resp.headers, old_resp.url, old_resp.code) + resp.msg = old_resp.msg + return resp + https_response = http_response + +# brave new world - 1:30 w/o, 1:10 with? 40 chapters, so 20s from sleeps. +# with gzip, no sleep: 47.469 +# w/o gzip, no sleep: 47.736 + +# I Am What I Am 67 chapters +# w/o gzip: 57.168 +# w/ gzip: 40.692 diff --git a/fanficdownloader/html.py b/fanficdownloader/html.py new file mode 100644 index 0000000..22fb40a --- /dev/null +++ b/fanficdownloader/html.py @@ -0,0 +1,126 @@ +#!/usr/bin/python +# Copyright(c) 2009 Andrew Chatham and Vijay Pandurangan + +import re +import sys +import StringIO +import urllib + +from BeautifulSoup import BeautifulSoup + +class HtmlProcessor: + WHITESPACE_RE = re.compile(r'\s') + # Look for </blockquote <p> + BAD_TAG_RE = re.compile(r'<[^>]+<', re.MULTILINE) + + def __init__(self, html, unfill=0): + self.unfill = unfill + html = self._ProcessRawHtml(html) + self._soup = BeautifulSoup(html) + if self._soup.title.contents: + self.title = self._soup.title.contents[0] + else: + self.title = None + + def _ProcessRawHtml(self, html): + new_html, count = HtmlProcessor.BAD_TAG_RE.subn('<', html) + if count: + print >>sys.stderr, 'Replaced %d bad tags' % count + return new_html + + def _StubInternalAnchors(self): + '''Replace each internal anchor with a fixed-size filepos anchor. + + Looks for every anchor with <a href="#myanchor"> and replaces that + with <a filepos="00000000050">. Stores anchors in self._anchor_references''' + self._anchor_references = [] + anchor_num = 0 + # anchor links + anchorlist = self._soup.findAll('a', href=re.compile('^#')) + # treat reference tags like a tags for TOCTOP. + anchorlist.extend(self._soup.findAll('reference', href=re.compile('^#'))) + for anchor in anchorlist: + self._anchor_references.append((anchor_num, anchor['href'])) + del anchor['href'] + anchor['filepos'] = '%.10d' % anchor_num + anchor_num += 1 + + def _ReplaceAnchorStubs(self): + # TODO: Browsers allow extra whitespace in the href names. + # use __str__ instead of prettify--it inserts extra spaces. + assembled_text = self._soup.__str__('utf8') + del self._soup # shouldn't touch this anymore + for anchor_num, original_ref in self._anchor_references: + ref = urllib.unquote(original_ref[1:]) # remove leading '#' + # Find the position of ref in the utf-8 document. + # TODO(chatham): Using regexes and looking for name= would be better. + newpos = assembled_text.rfind(ref.encode('utf-8')) + if newpos == -1: + print >>sys.stderr, 'Could not find anchor "%s"' % original_ref + continue + newpos += len(ref) + 2 # don't point into the middle of the <a name> tag + old_filepos = 'filepos="%.10d"' % anchor_num + new_filepos = 'filepos="%.10d"' % newpos + assert assembled_text.find(old_filepos) != -1 + assembled_text = assembled_text.replace(old_filepos, new_filepos, 1) + return assembled_text + + def _FixPreTags(self): + '''Replace <pre> tags with HTML-ified text.''' + pres = self._soup.findAll('pre') + for pre in pres: + pre.replaceWith(self._FixPreContents(str(pre.contents[0]))) + + def _FixPreContents(self, text): + if self.unfill: + line_splitter = '\n\n' + line_joiner = '<p>' + else: + line_splitter = '\n' + line_joiner = '<br>' + lines = [] + for line in text.split(line_splitter): + lines.append(self.WHITESPACE_RE.subn(' ', line)[0]) + return line_joiner.join(lines) + + def _RemoveUnsupported(self): + '''Remove any tags which the kindle cannot handle.''' + # TODO(chatham): <link> tags to script? + unsupported_tags = ('script', 'style') + for tag_type in unsupported_tags: + for element in self._soup.findAll(tag_type): + element.extract() + + def RenameAnchors(self, prefix): + '''Rename every internal anchor to have the given prefix, then + return the contents of the body tag.''' + for anchor in self._soup.findAll('a', href=re.compile('^#')): + anchor['href'] = '#' + prefix + anchor['href'][1:] + for a in self._soup.findAll('a'): + if a.get('name'): + a['name'] = prefix + a['name'] + + # TODO(chatham): figure out how to fix this. sometimes body comes out + # as NoneType. + content = [] + if self._soup.body is not None: + content = [unicode(c) for c in self._soup.body.contents] + return '\n'.join(content) + + def CleanHtml(self): + # TODO(chatham): fix_html_br, fix_html + self._RemoveUnsupported() + self._StubInternalAnchors() + self._FixPreTags() + return self._ReplaceAnchorStubs() + + +if __name__ == '__main__': + FILE ='/tmp/documentation.html' + #FILE = '/tmp/multipre.html' + FILE = '/tmp/view.html' + import codecs + d = open(FILE).read() + h = HtmlProcessor(d) + s = h.CleanHtml() + #print s diff --git a/fanficdownloader/html2text.py b/fanficdownloader/html2text.py new file mode 100644 index 0000000..1996527 --- /dev/null +++ b/fanficdownloader/html2text.py @@ -0,0 +1,453 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""html2text: Turn HTML into equivalent Markdown-structured text.""" +__version__ = "2.37" +__author__ = "Aaron Swartz (me@aaronsw.com)" +__copyright__ = "(C) 2004-2008 Aaron Swartz. GNU GPL 3." +__contributors__ = ["Martin 'Joey' Schulze", "Ricardo Reyes", "Kevin Jay North"] + +# TODO: +# Support decoded entities with unifiable. + +if not hasattr(__builtins__, 'True'): True, False = 1, 0 +import re, sys, urllib, htmlentitydefs, codecs, StringIO, types +import sgmllib +import urlparse +sgmllib.charref = re.compile('&#([xX]?[0-9a-fA-F]+)[^0-9a-fA-F]') + +try: from textwrap import wrap +except: pass + +# Use Unicode characters instead of their ascii psuedo-replacements +UNICODE_SNOB = 0 + +# Put the links after each paragraph instead of at the end. +LINKS_EACH_PARAGRAPH = 0 + +# Wrap long lines at position. 0 for no wrapping. (Requires Python 2.3.) +BODY_WIDTH = 78 + +# Don't show internal links (href="#local-anchor") -- corresponding link targets +# won't be visible in the plain text file anyway. +SKIP_INTERNAL_LINKS = False + +### Entity Nonsense ### + +def name2cp(k): + if k == 'apos': return ord("'") + if hasattr(htmlentitydefs, "name2codepoint"): # requires Python 2.3 + return htmlentitydefs.name2codepoint[k] + else: + k = htmlentitydefs.entitydefs[k] + if k.startswith("&#") and k.endswith(";"): return int(k[2:-1]) # not in latin-1 + return ord(codecs.latin_1_decode(k)[0]) + +unifiable = {'rsquo':"'", 'lsquo':"'", 'rdquo':'"', 'ldquo':'"', +'copy':'(C)', 'mdash':'--', 'nbsp':' ', 'rarr':'->', 'larr':'<-', 'middot':'*', +'ndash':'-', 'oelig':'oe', 'aelig':'ae', +'agrave':'a', 'aacute':'a', 'acirc':'a', 'atilde':'a', 'auml':'a', 'aring':'a', +'egrave':'e', 'eacute':'e', 'ecirc':'e', 'euml':'e', +'igrave':'i', 'iacute':'i', 'icirc':'i', 'iuml':'i', +'ograve':'o', 'oacute':'o', 'ocirc':'o', 'otilde':'o', 'ouml':'o', +'ugrave':'u', 'uacute':'u', 'ucirc':'u', 'uuml':'u'} + +unifiable_n = {} + +for k in unifiable.keys(): + unifiable_n[name2cp(k)] = unifiable[k] + +def charref(name): + if name[0] in ['x','X']: + c = int(name[1:], 16) + else: + c = int(name) + + if not UNICODE_SNOB and c in unifiable_n.keys(): + return unifiable_n[c] + else: + return unichr(c) + +def entityref(c): + if not UNICODE_SNOB and c in unifiable.keys(): + return unifiable[c] + else: + try: name2cp(c) + except KeyError: return "&" + c + else: return unichr(name2cp(c)) + +def replaceEntities(s): + s = s.group(1) + if s[0] == "#": + return charref(s[1:]) + else: return entityref(s) + +r_unescape = re.compile(r"&(#?[xX]?(?:[0-9a-fA-F]+|\w{1,8}));") +def unescape(s): + return r_unescape.sub(replaceEntities, s) + +def fixattrs(attrs): + # Fix bug in sgmllib.py + if not attrs: return attrs + newattrs = [] + for attr in attrs: + newattrs.append((attr[0], unescape(attr[1]))) + return newattrs + +### End Entity Nonsense ### + +def onlywhite(line): + """Return true if the line does only consist of whitespace characters.""" + for c in line: + if c is not ' ' and c is not ' ': + return c is ' ' + return line + +def optwrap(text,wrap_width=BODY_WIDTH): + """Wrap all paragraphs in the provided text.""" + + if not wrap_width: + return text + + assert wrap, "Requires Python 2.3." + result = '' + newlines = 0 + for para in text.split("\n"): + if len(para) > 0: + if para[0] is not ' ' and para[0] is not '-' and para[0] is not '*': + for line in wrap(para, wrap_width): + result += line + "\n" + result += "\n" + newlines = 2 + else: + if not onlywhite(para): + result += para + "\n" + newlines = 1 + else: + if newlines < 2: + result += "\n" + newlines += 1 + return result + +def hn(tag): + if tag[0] == 'h' and len(tag) == 2: + try: + n = int(tag[1]) + if n in range(1, 10): return n + except ValueError: return 0 + +class _html2text(sgmllib.SGMLParser): + def __init__(self, out=None, baseurl=''): + sgmllib.SGMLParser.__init__(self) + + if out is None: self.out = self.outtextf + else: self.out = out + self.outtext = u'' + self.quiet = 0 + self.p_p = 0 + self.outcount = 0 + self.start = 1 + self.space = 0 + self.a = [] + self.astack = [] + self.acount = 0 + self.list = [] + self.blockquote = 0 + self.pre = 0 + self.startpre = 0 + self.lastWasNL = 0 + self.abbr_title = None # current abbreviation definition + self.abbr_data = None # last inner HTML (for abbr being defined) + self.abbr_list = {} # stack of abbreviations to write later + self.baseurl = baseurl + + def outtextf(self, s): + self.outtext += s + + def close(self): + sgmllib.SGMLParser.close(self) + + self.pbr() + self.o('', 0, 'end') + + return self.outtext + + def handle_charref(self, c): + self.o(charref(c)) + + def handle_entityref(self, c): + self.o(entityref(c)) + + def unknown_starttag(self, tag, attrs): + self.handle_tag(tag, attrs, 1) + + def unknown_endtag(self, tag): + self.handle_tag(tag, None, 0) + + def previousIndex(self, attrs): + """ returns the index of certain set of attributes (of a link) in the + self.a list + + If the set of attributes is not found, returns None + """ + if not attrs.has_key('href'): return None + + i = -1 + for a in self.a: + i += 1 + match = 0 + + if a.has_key('href') and a['href'] == attrs['href']: + if a.has_key('title') or attrs.has_key('title'): + if (a.has_key('title') and attrs.has_key('title') and + a['title'] == attrs['title']): + match = True + else: + match = True + + if match: return i + + def handle_tag(self, tag, attrs, start): + attrs = fixattrs(attrs) + + if hn(tag): + self.p() + if start: self.o(hn(tag)*"#" + ' ') + + if tag in ['p', 'div']: self.p() + + if tag == "br" and start: self.o(" \n") + + if tag == "hr" and start: + self.p() + self.o("* * *") + self.p() + + if tag in ["head", "style", 'script']: + if start: self.quiet += 1 + else: self.quiet -= 1 + + if tag in ["body"]: + self.quiet = 0 # sites like 9rules.com never close <head> + + if tag == "blockquote": + if start: + self.p(); self.o('> ', 0, 1); self.start = 1 + self.blockquote += 1 + else: + self.blockquote -= 1 + self.p() + + if tag in ['em', 'i', 'u']: self.o("_") + if tag in ['strong', 'b']: self.o("**") + if tag == "code" and not self.pre: self.o('`') #TODO: `` `this` `` + if tag == "abbr": + if start: + attrsD = {} + for (x, y) in attrs: attrsD[x] = y + attrs = attrsD + + self.abbr_title = None + self.abbr_data = '' + if attrs.has_key('title'): + self.abbr_title = attrs['title'] + else: + if self.abbr_title != None: + self.abbr_list[self.abbr_data] = self.abbr_title + self.abbr_title = None + self.abbr_data = '' + + if tag == "a": + if start: + attrsD = {} + for (x, y) in attrs: attrsD[x] = y + attrs = attrsD + if attrs.has_key('href') and not (SKIP_INTERNAL_LINKS and attrs['href'].startswith('#')): + self.astack.append(attrs) + self.o("[") + else: + self.astack.append(None) + else: + if self.astack: + a = self.astack.pop() + if a: + i = self.previousIndex(a) + if i is not None: + a = self.a[i] + else: + self.acount += 1 + a['count'] = self.acount + a['outcount'] = self.outcount + self.a.append(a) + self.o("][" + `a['count']` + "]") + + if tag == "img" and start: + attrsD = {} + for (x, y) in attrs: attrsD[x] = y + attrs = attrsD + if attrs.has_key('src'): + attrs['href'] = attrs['src'] + alt = attrs.get('alt', '') + i = self.previousIndex(attrs) + if i is not None: + attrs = self.a[i] + else: + self.acount += 1 + attrs['count'] = self.acount + attrs['outcount'] = self.outcount + self.a.append(attrs) + self.o("![") + self.o(alt) + self.o("]["+`attrs['count']`+"]") + + if tag == 'dl' and start: self.p() + if tag == 'dt' and not start: self.pbr() + if tag == 'dd' and start: self.o(' ') + if tag == 'dd' and not start: self.pbr() + + if tag in ["ol", "ul"]: + if start: + self.list.append({'name':tag, 'num':0}) + else: + if self.list: self.list.pop() + + self.p() + + if tag == 'li': + if start: + self.pbr() + if self.list: li = self.list[-1] + else: li = {'name':'ul', 'num':0} + self.o(" "*len(self.list)) #TODO: line up <ol><li>s > 9 correctly. + if li['name'] == "ul": self.o("* ") + elif li['name'] == "ol": + li['num'] += 1 + self.o(`li['num']`+". ") + self.start = 1 + else: + self.pbr() + + if tag in ["table", "tr"] and start: self.p() + if tag == 'td': self.pbr() + + if tag == "pre": + if start: + self.startpre = 1 + self.pre = 1 + else: + self.pre = 0 + self.p() + + def pbr(self): + if self.p_p == 0: self.p_p = 1 + + def p(self): self.p_p = 2 + + def o(self, data, puredata=0, force=0): + if self.abbr_data is not None: self.abbr_data += data + + if not self.quiet: + if puredata and not self.pre: + data = re.sub('\s+', ' ', data) + if data and data[0] == ' ': + self.space = 1 + data = data[1:] + if not data and not force: return + + if self.startpre: + #self.out(" :") #TODO: not output when already one there + self.startpre = 0 + + bq = (">" * self.blockquote) + if not (force and data and data[0] == ">") and self.blockquote: bq += " " + + if self.pre: + bq += " " + data = data.replace("\n", "\n"+bq) + + if self.start: + self.space = 0 + self.p_p = 0 + self.start = 0 + + if force == 'end': + # It's the end. + self.p_p = 0 + self.out("\n") + self.space = 0 + + + if self.p_p: + self.out(('\n'+bq)*self.p_p) + self.space = 0 + + if self.space: + if not self.lastWasNL: self.out(' ') + self.space = 0 + + if self.a and ((self.p_p == 2 and LINKS_EACH_PARAGRAPH) or force == "end"): + if force == "end": self.out("\n") + + newa = [] + for link in self.a: + if self.outcount > link['outcount']: + self.out(" ["+`link['count']`+"]: " + urlparse.urljoin(self.baseurl, link['href'])) + if link.has_key('title'): self.out(" ("+link['title']+")") + self.out("\n") + else: + newa.append(link) + + if self.a != newa: self.out("\n") # Don't need an extra line when nothing was done. + + self.a = newa + + if self.abbr_list and force == "end": + for abbr, definition in self.abbr_list.items(): + self.out(" *[" + abbr + "]: " + definition + "\n") + + self.p_p = 0 + self.out(data) + self.lastWasNL = data and data[-1] == '\n' + self.outcount += 1 + + def handle_data(self, data): + if r'\/script>' in data: self.quiet -= 1 + self.o(data, 1) + + def unknown_decl(self, data): pass + +def wrapwrite(text): sys.stdout.write(text.encode('utf8')) + +def html2text_file(html, out=wrapwrite, baseurl=''): + h = _html2text(out, baseurl) + h.feed(html) + h.feed("") + return h.close() + +def html2text(html, baseurl='', wrap_width=BODY_WIDTH): + return optwrap(html2text_file(html, None, baseurl),wrap_width) + +if __name__ == "__main__": + baseurl = '' + if sys.argv[1:]: + arg = sys.argv[1] + if arg.startswith('http://'): + baseurl = arg + j = urllib.urlopen(baseurl) + try: + from feedparser import _getCharacterEncoding as enc + except ImportError: + enc = lambda x, y: ('utf-8', 1) + text = j.read() + encoding = enc(j.headers, text)[0] + if encoding == 'us-ascii': encoding = 'utf-8' + data = text.decode(encoding) + + else: + encoding = 'utf8' + if len(sys.argv) > 2: + encoding = sys.argv[2] + data = open(arg, 'r').read().decode(encoding) + else: + data = sys.stdin.read().decode('utf8') + wrapwrite(html2text(data, baseurl)) diff --git a/fanficdownloader/htmlcleanup.py b/fanficdownloader/htmlcleanup.py new file mode 100644 index 0000000..325f5e1 --- /dev/null +++ b/fanficdownloader/htmlcleanup.py @@ -0,0 +1,485 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import logging +logger = logging.getLogger(__name__) + +import re + +def _unirepl(match): + "Return the unicode string for a decimal number" + if match.group(1).startswith('x'): + radix=16 + s = match.group(1)[1:] + else: + radix=10 + s = match.group(1) + try: + value = int(s, radix) + retval = "%s%s"%(unichr(value),match.group(2)) + except: + # This way, at least if there's more of entities out there + # that fail, it doesn't blow the entire download. + logger.warn("Numeric entity translation failed, skipping: &#x%s%s"%(match.group(1),match.group(2))) + retval = "" + return retval + +def _replaceNumberEntities(data): + # The same brokenish entity parsing in SGMLParser that inserts ';' + # after non-entities will also insert ';' incorrectly after number + # entities, including part of the next word if it's a-z. + # "Don't—ever—do—that—again," becomes + # "Don't—e;ver—d;o—that—a;gain," + # Also need to allow for 5 digit decimal entities 法 + # Last expression didn't allow for 2 digit hex correctly: é + p = re.compile(r'&#(x[0-9a-fA-F]{,4}|[0-9]{,5})([0-9a-fA-F]*?);') + return p.sub(_unirepl, data) + +def _replaceNotEntities(data): + # not just \w or \S. regexp from c:\Python25\lib\sgmllib.py + # (or equiv), SGMLParser, entityref + p = re.compile(r'&([a-zA-Z][-.a-zA-Z0-9]*);') + return p.sub(r'&\1', data) + +def stripHTML(soup): + if isinstance(soup,basestring) or hasattr(soup, 'bs3'): + return removeAllEntities(re.sub(r'<[^>]+>','',"%s" % soup)).strip() + else: + # bs4 already converts all the entities to UTF8 chars. + return soup.get_text(strip=True) + +def conditionalRemoveEntities(value): + if isinstance(value,basestring): + return removeEntities(value).strip() + else: + return value + +def removeAllEntities(text): + # Remove < < and & + return removeEntities(text).replace('<', '<').replace('>', '>').replace('&', '&') + +def removeEntities(text): + if text is None: + return u"" + + if not isinstance(text,basestring): + return unicode(text) + + try: + t = text.decode('utf-8') + except UnicodeEncodeError, e: + try: + t = text.encode ('ascii', 'xmlcharrefreplace') + except UnicodeEncodeError, e: + t = text + text = t + # replace numeric versions of [&<>] with named versions, + # then replace named versions with actual characters, + text = re.sub(r'�*38;','&',text) + text = re.sub(r'�*60;','<',text) + text = re.sub(r'�*62;','>',text) + + # replace remaining � entities with unicode value, such as ' -> ' + text = _replaceNumberEntities(text) + + # replace several named entities with character, such as — -> - + # see constants.py for the list. + # reverse sort will put entities with ; before the same one without, when valid. + for e in reversed(sorted(entities.keys())): + v = entities[e] + try: + text = text.replace(e, v) + except UnicodeDecodeError, ex: + # for the pound symbol in constants.py + text = text.replace(e, v.decode('utf-8')) + + # SGMLParser, and in turn, BeautifulStoneSoup doesn't parse + # entities terribly well and inserts (;) after something that + # it thinks might be an entity. AT&T becomes AT&T; All of my + # attempts to fix this by changing the input to + # BeautifulStoneSoup break something else instead. But at + # this point, there should be *no* real entities left, so find + # these not-entities and removing them here should be safe. + text = _replaceNotEntities(text) + + # < < and & are the only html entities allowed in xhtml, put those back. + return text.replace('&', '&').replace('&lt', '<').replace('&gt', '>') + +# entity list from http://code.google.com/p/doctype/wiki/CharacterEntitiesConsistent +entities = { 'á' : 'á', + 'Á' : 'Á', + 'Á' : 'Á', + 'á' : 'á', + 'â' : 'â', + 'Â' : 'Â', + 'Â' : 'Â', + 'â' : 'â', + '´' : '´', + '´' : '´', + 'Æ' : 'Æ', + 'æ' : 'æ', + 'Æ' : 'Æ', + 'æ' : 'æ', + 'à' : 'à', + 'À' : 'À', + 'À' : 'À', + 'à' : 'à', + 'ℵ' : 'ℵ', + 'α' : 'α', + 'Α' : 'Α', + '&' : '&', + '&' : '&', + '&' : '&', + '&' : '&', + '∧' : '∧', + '∠' : '∠', + 'å' : 'å', + 'Å' : 'Å', + 'Å' : 'Å', + 'å' : 'å', + '≈' : '≈', + 'ã' : 'ã', + 'Ã' : 'Ã', + 'Ã' : 'Ã', + 'ã' : 'ã', + 'ä' : 'ä', + 'Ä' : 'Ä', + 'Ä' : 'Ä', + 'ä' : 'ä', + '„' : '„', + 'β' : 'β', + 'Β' : 'Β', + '¦' : '¦', + '¦' : '¦', + '•' : '•', + '∩' : '∩', + 'ç' : 'ç', + 'Ç' : 'Ç', + 'Ç' : 'Ç', + 'ç' : 'ç', + '¸' : '¸', + '¸' : '¸', + '¢' : '¢', + '¢' : '¢', + 'χ' : 'χ', + 'Χ' : 'Χ', + 'ˆ' : 'ˆ', + '♣' : '♣', + '≅' : '≅', + '©' : '©', + '©' : '©', + '©' : '©', + '©' : '©', + '↵' : '↵', + '∪' : '∪', + '¤' : '¤', + '¤' : '¤', + '†' : '†', + '‡' : '‡', + '↓' : '↓', + '⇓' : '⇓', + '°' : '°', + '°' : '°', + 'δ' : 'δ', + 'Δ' : 'Δ', + '♦' : '♦', + '÷' : '÷', + '÷' : '÷', + 'é' : 'é', + 'É' : 'É', + 'É' : 'É', + 'é' : 'é', + 'ê' : 'ê', + 'Ê' : 'Ê', + 'Ê' : 'Ê', + 'ê' : 'ê', + 'è' : 'è', + 'È' : 'È', + 'È' : 'È', + 'è' : 'è', + '∅' : '∅', + ' ' : ' ', + ' ' : ' ', + 'ε' : 'ε', + 'Ε' : 'Ε', + '≡' : '≡', + 'η' : 'η', + 'Η' : 'Η', + 'ð' : 'ð', + 'Ð' : 'Ð', + 'Ð' : 'Ð', + 'ð' : 'ð', + 'ë' : 'ë', + 'Ë' : 'Ë', + 'Ë' : 'Ë', + 'ë' : 'ë', + '€' : '€', + '∃' : '∃', + 'ƒ' : 'ƒ', + '∀' : '∀', + '½' : '½', + '½' : '½', + '¼' : '¼', + '¼' : '¼', + '¾' : '¾', + '¾' : '¾', + '⁄' : '⁄', + 'γ' : 'γ', + 'Γ' : 'Γ', + '≥' : '≥', + #'>' : '>', + #'>' : '>', + #'>' : '>', + #'>' : '>', + '↔' : '↔', + '⇔' : '⇔', + '♥' : '♥', + '…' : '…', + 'í' : 'í', + 'Í' : 'Í', + 'Í' : 'Í', + 'í' : 'í', + 'î' : 'î', + 'Î' : 'Î', + 'Î' : 'Î', + 'î' : 'î', + '¡' : '¡', + '¡' : '¡', + 'ì' : 'ì', + 'Ì' : 'Ì', + 'Ì' : 'Ì', + 'ì' : 'ì', + 'ℑ' : 'ℑ', + '∞' : '∞', + '∫' : '∫', + 'ι' : 'ι', + 'Ι' : 'Ι', + '¿' : '¿', + '¿' : '¿', + '∈' : '∈', + 'ï' : 'ï', + 'Ï' : 'Ï', + 'Ï' : 'Ï', + 'ï' : 'ï', + 'κ' : 'κ', + 'Κ' : 'Κ', + 'λ' : 'λ', + 'Λ' : 'Λ', + '«' : '«', + '«' : '«', + '←' : '←', + '⇐' : '⇐', + '⌈' : '⌈', + '“' : '“', + '≤' : '≤', + '⌊' : '⌊', + '∗' : '∗', + '◊' : '◊', + '‎' : '‎', + '‹' : '‹', + '‘' : '‘', + #'<' : '<', + #'<' : '<', + #'<' : '<', + #'<' : '<', + '¯' : '¯', + '¯' : '¯', + '—' : '—', + 'µ' : 'µ', + 'µ' : 'µ', + '·' : '·', + '·' : '·', + '−' : '−', + 'μ' : 'μ', + 'Μ' : 'Μ', + '∇' : '∇', + ' ' : ' ', + ' ' : ' ', + '–' : '–', + '≠' : '≠', + '∋' : '∋', + '¬' : '¬', + '¬' : '¬', + '∉' : '∉', + '⊄' : '⊄', + 'ñ' : 'ñ', + 'Ñ' : 'Ñ', + 'Ñ' : 'Ñ', + 'ñ' : 'ñ', + 'ν' : 'ν', + 'Ν' : 'Ν', + 'ó' : 'ó', + 'Ó' : 'Ó', + 'Ó' : 'Ó', + 'ó' : 'ó', + 'ô' : 'ô', + 'Ô' : 'Ô', + 'Ô' : 'Ô', + 'ô' : 'ô', + 'Œ' : 'Œ', + 'œ' : 'œ', + 'ò' : 'ò', + 'Ò' : 'Ò', + 'Ò' : 'Ò', + 'ò' : 'ò', + '‾' : '‾', + 'ω' : 'ω', + 'Ω' : 'Ω', + 'ο' : 'ο', + 'Ο' : 'Ο', + '⊕' : '⊕', + '∨' : '∨', + 'ª' : 'ª', + 'ª' : 'ª', + 'º' : 'º', + 'º' : 'º', + 'ø' : 'ø', + 'Ø' : 'Ø', + 'Ø' : 'Ø', + 'ø' : 'ø', + 'õ' : 'õ', + 'Õ' : 'Õ', + 'Õ' : 'Õ', + 'õ' : 'õ', + '⊗' : '⊗', + 'ö' : 'ö', + 'Ö' : 'Ö', + 'Ö' : 'Ö', + 'ö' : 'ö', + '¶' : '¶', + '¶' : '¶', + '∂' : '∂', + '‰' : '‰', + '⊥' : '⊥', + 'φ' : 'φ', + 'Φ' : 'Φ', + 'π' : 'π', + 'Π' : 'Π', + 'ϖ' : 'ϖ', + '±' : '±', + '±' : '±', + '£' : '£', + '£' : '£', + '′' : '′', + '″' : '″', + '∏' : '∏', + '∝' : '∝', + 'ψ' : 'ψ', + 'Ψ' : 'Ψ', + '"' : '"', + '"' : '"', + '"' : '"', + '"' : '"', + '√' : '√', + '»' : '»', + '»' : '»', + '→' : '→', + '⇒' : '⇒', + '⌉' : '⌉', + '”' : '”', + 'ℜ' : 'ℜ', + '®' : '®', + '®' : '®', + '®' : '®', + '®' : '®', + '⌋' : '⌋', + 'ρ' : 'ρ', + 'Ρ' : 'Ρ', + '‏' : '‏', + '›' : '›', + '’' : '’', + '‚' : '‚', + 'š' : 'š', + 'Š' : 'Š', + '⋅' : '⋅', + '§' : '§', + '§' : '§', + '­' : '­', # strange optional hyphenation control character, not just a dash + '­' : '­', + 'σ' : 'σ', + 'Σ' : 'Σ', + 'ς' : 'ς', + '∼' : '∼', + '♠' : '♠', + '⊂' : '⊂', + '⊆' : '⊆', + '∑' : '∑', + '¹' : '¹', + '¹' : '¹', + '²' : '²', + '²' : '²', + '³' : '³', + '³' : '³', + '⊃' : '⊃', + '⊇' : '⊇', + 'ß' : 'ß', + 'ß' : 'ß', + 'τ' : 'τ', + 'Τ' : 'Τ', + '∴' : '∴', + 'θ' : 'θ', + 'Θ' : 'Θ', + 'ϑ' : 'ϑ', + ' ' : ' ', + 'þ' : 'þ', + 'Þ' : 'Þ', + 'Þ' : 'Þ', + 'þ' : 'þ', + '˜' : '˜', + '×' : '×', + '×' : '×', + '™' : '™', + 'ú' : 'ú', + 'Ú' : 'Ú', + 'Ú' : 'Ú', + 'ú' : 'ú', + '↑' : '↑', + '⇑' : '⇑', + 'û' : 'û', + 'Û' : 'Û', + 'Û' : 'Û', + 'û' : 'û', + 'ù' : 'ù', + 'Ù' : 'Ù', + 'Ù' : 'Ù', + 'ù' : 'ù', + '¨' : '¨', + '¨' : '¨', + 'ϒ' : 'ϒ', + 'υ' : 'υ', + 'Υ' : 'Υ', + 'ü' : 'ü', + 'Ü' : 'Ü', + 'Ü' : 'Ü', + 'ü' : 'ü', + '℘' : '℘', + 'ξ' : 'ξ', + 'Ξ' : 'Ξ', + 'ý' : 'ý', + 'Ý' : 'Ý', + 'Ý' : 'Ý', + 'ý' : 'ý', + '¥' : '¥', + '¥' : '¥', + 'ÿ' : 'ÿ', + 'Ÿ' : 'Ÿ', + 'ÿ' : 'ÿ', + 'ζ' : 'ζ', + 'Ζ' : 'Ζ', + '‍' : '‍', # strange spacing control character, not just a space + '‌' : '‌', # strange spacing control character, not just a space + } diff --git a/fanficdownloader/htmlheuristics.py b/fanficdownloader/htmlheuristics.py new file mode 100644 index 0000000..52a78df --- /dev/null +++ b/fanficdownloader/htmlheuristics.py @@ -0,0 +1,348 @@ +# -*- coding: utf-8 -*- + +# Copyright 2013 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import logging +logger = logging.getLogger(__name__) +import re +import codecs +import BeautifulSoup as bs +import HtmlTagStack as stack + +from . import exceptions as exceptions + +def replace_br_with_p(body): + + # Ascii character (and Unicode as well) xA0 is a non-breaking space, ascii code 160. + # However, Python Regex does not recognize it as a whitespace, so we'll be changing it to a reagular space. + body = body.replace(u'\xa0', u' ') + + if body.find('>') == -1 or body.rfind('<') == -1: + return body + + # logger.debug(u'BODY start.: ' + body[:250]) + # logger.debug(u'BODY end...: ' + body[-250:]) + # logger.debug(u'BODY.......: ' + body) + + # clean breaks (<br />), removing whitespaces between them. + body = re.sub(r'\s*<br[^>]*>\s*', r'<br />', body) + + # change surrounding div to a p and remove attrs Top surrounding + # tag in all cases now should be div, to just strip the first and + # last tags. + if is_valid_block(body) and body.find('<div') == 0: + body = body[body.index('>')+1:body.rindex('<')] + + body = soup_up_div(u'<div>' + body + u'</div>') + + body = body[body.index('>')+1:body.rindex('<')] + + # Find all bexisting blocks with p, pre and blockquote tags, we need to shields break tags inside those. + # This is for "lenient" mode, however it is also used to clear break tags before and after the block elements. + blocksRegex = re.compile(r'(\s*<br\ />\s*)*\s*<(pre|p|blockquote|table)([^>]*)>(.+?)</\2>\s*(\s*<br\ />\s*)*', re.DOTALL) + body = blocksRegex.sub(r'\n<\2\3>\4</\2>\n', body) + + # if aggressive mode = true + # blocksRegex = re.compile(r'(\s*<br\ */*>\s*)*\s*<(pre)([^>]*)>(.+?)</\2>\s*(\s*<br\ */*>\s*)*', re.DOTALL) + # In aggressive mode, we also check breakes inside blockquotes, meaning we can get orphaned paragraph tags. + # body = re.sub(r'<blockquote([^>]*)>(.+?)</blockquote>', r'<blockquote\1><p>\2</p></blockquote>', body, re.DOTALL) + # end aggressive mode + + blocks = blocksRegex.finditer(body) + # For our replacements to work, we need to work backwards, so we reverse the iterator. + blocksList = [] + for match in blocks: + blocksList.insert(0, match) + + for match in blocksList: + group4 = match.group(4).replace(u'<br />', u'{br /}') + body = body[:match.start(4)] + group4 + body[match.end(4):] + + # change surrounding div to a p and remove attrs Top surrounding + # tag in all cases now should be div, to just strip the first and + # last tags. + # body = u'<p>' + body + u'</p>' + + # Nuke div tags surrounding a HR tag. + body = re.sub(r'<div[^>]+>\s*<hr[^>]+>\s*</div>', r'\n<hr />\n', body) + + # So many people add formatting to their HR tags, and ePub does not allow those, we are supposed to use css. + # This nukes the hr tag attributes. + body = re.sub(r'\s*<hr[^>]+>\s*', r'\n<hr />\n', body) + + # Remove leading and trailing breaks from HR tags + body = re.sub(r'\s*(<br\ \/>)*\s*<hr\ \/>\s*(<br\ \/>)*\s*', r'\n<hr />\n', body) + # Nuking breaks leading paragraps that may be in the body. They are eventually treated as <p><br /></p> + body = re.sub(r'\s*(<br\ \/>)+\s*<p', r'\n<p></p>\n<p', body) + # Nuking breaks trailing paragraps that may be in the body. They are eventually treated as <p><br /></p> + body = re.sub(r'</p>\s*(<br\ \/>)+\s*', r'</p>\n<p></p>\n', body) + + # Because a leading or trailing non break tag will break the following code, we have to mess around rather badly for a few lines. + body = body.replace(u'[',u'&squareBracketStart;') + body = body.replace(u']',u'&squareBracketEnd;') + body = body.replace(u'<br />',u'[br /]') + + breaksRegexp = [ + re.compile(r'([^\]])(\[br\ \/\])([^\[])'), + re.compile(r'([^\]])(\[br\ \/\]){2}([^\[])'), + re.compile(r'([^\]])(\[br\ \/\]){3}([^\[])'), + re.compile(r'([^\]])(\[br\ \/\]){4}([^\[])'), + re.compile(r'([^\]])(\[br\ \/\]){5}([^\[])'), + re.compile(r'([^\]])(\[br\ \/\]){6}([^\[])'), + re.compile(r'([^\]])(\[br\ \/\]){7}([^\[])'), + re.compile(r'([^\]])(\[br\ \/\]){8}([^\[])'), + re.compile(r'(\[br\ \/\]){9,}')] + + breaksCount = [ + len(breaksRegexp[0].findall(body)), + len(breaksRegexp[1].findall(body)), + len(breaksRegexp[2].findall(body)), + len(breaksRegexp[3].findall(body)), + len(breaksRegexp[4].findall(body)), + len(breaksRegexp[5].findall(body)), + len(breaksRegexp[6].findall(body)), + len(breaksRegexp[7].findall(body))] + + breaksMax = 0 + breaksMaxIndex = 0; + + for i in range(1,len(breaksCount)): + if breaksCount[i] >= breaksMax: + breaksMax = breaksCount[i] + breaksMaxIndex = i + + lines = body.split(u'[br /]') + contentLines = 0; + contentLinesSum = 0; + longestLineLength = 0; + averageLineLength = 0; + + for line in lines: + lineLen = len(line.strip()) + if lineLen > 0: + contentLines += 1 + contentLinesSum += lineLen + if lineLen > longestLineLength: + longestLineLength = lineLen + + if contentLines == 0: + contentLines = 1 + + averageLineLength = contentLinesSum/contentLines + + logger.debug(u'---') + logger.debug(u'Lines.............: ' + unicode(len(lines))) + logger.debug(u'contentLines......: ' + unicode(contentLines)) + logger.debug(u'contentLinesSum...: ' + unicode(contentLinesSum)) + logger.debug(u'longestLineLength.: ' + unicode(longestLineLength)) + logger.debug(u'averageLineLength.: ' + unicode(averageLineLength)) + + if breaksMaxIndex == len(breaksCount)-1 and breaksMax < 2: + breaksMaxIndex = 0 + breaksMax = breaksCount[0] + + logger.debug(u'---') + logger.debug(u'breaks 1: ' + unicode(breaksCount[0])) + logger.debug(u'breaks 2: ' + unicode(breaksCount[1])) + logger.debug(u'breaks 3: ' + unicode(breaksCount[2])) + logger.debug(u'breaks 4: ' + unicode(breaksCount[3])) + logger.debug(u'breaks 5: ' + unicode(breaksCount[4])) + logger.debug(u'breaks 6: ' + unicode(breaksCount[5])) + logger.debug(u'breaks 7: ' + unicode(breaksCount[6])) + logger.debug(u'breaks 8: ' + unicode(breaksCount[7])) + logger.debug(u'----') + logger.debug(u'max found: ' + unicode(breaksMax)) + logger.debug(u'max Index: ' + unicode(breaksMaxIndex)) + logger.debug(u'----') + + if breaksMaxIndex > 0 and breaksCount[0] > breaksMax and averageLineLength < 90: + body = breaksRegexp[0].sub(r'\1 \n\3', body) + + # Find all instances of consecutive breaks less than otr equal to the max count use most often + # replase those tags to inverted p tag pairs, those with more connsecutive breaks are replaced them with a horisontal line + for i in range(len(breaksCount)): + # if i > 0 or breaksMaxIndex == 0: + if i <= breaksMaxIndex: + logger.debug(unicode(i) + u' <= breaksMaxIndex (' + unicode(breaksMaxIndex) + u')') + body = breaksRegexp[i].sub(r'\1</p>\n<p>\3', body) + elif i == breaksMaxIndex+1: + logger.debug(unicode(i) + u' == breaksMaxIndex+1 (' + unicode(breaksMaxIndex+1) + u')') + body = breaksRegexp[i].sub(r'\1</p>\n<p><br/></p>\n<p>\3', body) + else: + logger.debug(unicode(i) + u' > breaksMaxIndex+1 (' + unicode(breaksMaxIndex+1) + u')') + body = breaksRegexp[i].sub(r'\1</p>\n<hr />\n<p>\3', body) + + body = breaksRegexp[8].sub(r'</p>\n<hr />\n<p>', body) + + # Reverting the square brackets + body = body.replace(u'[', u'<') + body = body.replace(u']', u'>') + body = body.replace(u'&squareBracketStart;', u'[') + body = body.replace(u'&squareBracketEnd;', u']') + + body = body.replace(u'{p}', u'<p>') + body = body.replace(u'{/p}', u'</p>') + + # If for some reason, a third break makes its way inside the paragraph, preplace that with the empty paragraph for the additional linespaing. + body = re.sub(r'<p>\s*(<br\ \/>)+', r'<p><br /></p>\n<p>', body) + + # change empty p tags to include a br to force spacing. + body = re.sub(r'<p>\s*</p>', r'<p><br/></p>', body) + + # Clean up hr tags, and add inverted p tag pairs + body = re.sub(r'(<div[^>]+>)*\s*<hr\ \/>\s*(</div>)*', r'\n<hr />\n', body) + + # Clean up hr tags, and add inverted p tag pairs + body = re.sub(r'\s*<hr\ \/>\s*', r'</p>\n<hr />\n<p>', body) + + # Because the previous regexp may cause trouble if the hr tag already had a p tag pair around it, w nee dot repair that. + # Repeated opening p tags are condenced to one. As we added the extra leading opening p tags, we can safely assume that + # the last in such a chain must be the original. Lets keep its attributes if they are there. + body = re.sub(r'\s*(<p[^>]*>\s*)+<p([^>]*)>\s*', r'\n<p\2>', body) + # Repeated closing p tags are condenced to one + body = re.sub(r'\s*(<\/\s*p>\s*){2,}', r'</p>\n', body) + + # superflous cleaning, remove whitespaces traling opening p tags. These does affect formatting. + body = re.sub(r'\s*<p([^>]*)>\s*', r'\n<p\1>', body) + # superflous cleaning, remove whitespaces leading closing p tags. These does not affect formatting. + body = re.sub(r'\s*</p>\s*', r'</p>\n', body) + + # Remove empty tag pairs + body = re.sub(r'\s*<(\S+)[^>]*>\s*</\1>', r'', body) + + body = body.replace(u'{br /}', u'<br />') + body = body.strip() + + # re-wrap in div tag. + body = u'<div>\n' + body + u'</div>\n' + + # return body + return tag_sanitizer(body) + +def is_valid_block(block): + return unicode(block).find('<') == 0 and unicode(block).find('<!') != 0 + +def soup_up_div(body): + blockTags = ['address', 'blockquote', 'del', 'div', 'dl', 'fieldset', 'form', 'ins', 'noscript', 'ol', 'p', 'pre', 'table', 'ul'] + recurseTags = ['blockquote', 'div', 'noscript'] + + tag = body[:body.index('>')+1] + tagend = body[body.rindex('<'):] + + body = body.replace(u'<br />', u'[br /]') + + soup = bs.BeautifulSoup(body) + + body = u'' + lastElement = 1 # 1 = block, 2 = nested, 3 = invalid + + for i in soup.contents[0]: + if unicode(i).strip().__len__() > 0: + s = unicode(i) + if type(i) == bs.Tag: + if i.name in blockTags: + if lastElement > 1: + body = body.strip(r'\s*(\[br\ \/\]\s*)*\s*') + body += u'{/p}' + + lastElement = 1 + + if i.name in recurseTags: + s = soup_up_div(s) + + body += s.strip() + '\n' + else: + if lastElement == 1: + body = body.strip(r'\s*(\[br\ \/\]\s*)*\s*') + body += u'{p}' + + lastElement = 2 + body += s + elif type(i) == bs.Comment: + body += s + else: + if lastElement == 1: + body = body.strip(r'\s*(\[br\ \/\]\s*)*\s*') + body += u'{p}' + + lastElement = 3 + body += s + + if lastElement > 1: + body = body.strip(r'\s*(\[br\ \/\]\s*)*\s*') + body += u'{/p}' + + body = body.replace(u'[br /]', u'<br />') + + return tag + body + tagend + + +def is_end_tag(tag): + return re.match(r'</([^\ >]+)>', tag) != None + +def is_comment_tag(tag): + return re.match(r'<\!\-\-([^>]+)>', tag) != None + +def is_closed_tag(tag): + return re.match(r'<(.+?)/>', tag) != None + +def tag_sanitizer(html): + blockTags = ['address', 'blockquote', 'del', 'div', 'dl', 'fieldset', 'form', 'ins', 'noscript', 'ol', 'pre', 'table', 'ul'] + + body = u'' + tags = re.findall(r'(<[^>]+>)([^<]*)', html) + + for rTag in tags: + name = stack.get_tag_name(rTag[0]) + is_end = is_end_tag(rTag[0]) + is_closed = is_closed_tag(rTag[0]) or is_comment_tag(rTag[0]) + + # is_comment = is_comment_tag(rTag[0]) + # logger.debug(u'%s > isEnd: %s > isClosed: %s > isComment: %s'%(name, unicode(is_end), unicode(is_closed), unicode(is_comment))) + # logger.debug(u'> %s%s\n'%(rTag[0], rTag[1])) + + if name in blockTags: + body += rTag[0] + body += rTag[1] + elif name == u'p': + if is_end: + body += stack.spool_end() + body += rTag[0] + body += rTag[1] + elif is_closed: + body += rTag[0] + body += rTag[1] + else: + body += rTag[0] + body += stack.spool_start() + body += rTag[1] + else: + if is_end: + t = stack.get_last() + tn = stack.get_tag_name(t) + rTn = stack.get_tag_name(rTag[0]) + if tn == rTn: + body += rTag[0] + stack.pop() + elif not is_closed: + stack.push(rTag[0]) + body += rTag[0] + else: + body += rTag[0] + + body += rTag[1] + stack.flush() + return body diff --git a/fanficdownloader/mobi.py b/fanficdownloader/mobi.py new file mode 100644 index 0000000..7a52715 --- /dev/null +++ b/fanficdownloader/mobi.py @@ -0,0 +1,386 @@ +#!/usr/bin/python +# Copyright(c) 2009 Andrew Chatham and Vijay Pandurangan + + +import StringIO +import struct +import time +import random +import logging + +logger = logging.getLogger(__name__) + +from html import HtmlProcessor + +# http://wiki.mobileread.com/wiki/MOBI +# http://membres.lycos.fr/microfirst/palm/pdb.html + +encoding = { + 'UTF-8' : 65001, + 'latin-1' : 1252, +} + +languages = {"en-us" : 0x0409, + "sv" : 0x041d, + "fi" : 0x000b, + "en" : 0x0009, + "en-gb" : 0x0809} + +def ToHex(s): + v = ['%.2x' % ord(c) for c in s] + return ' '.join(v) + +class _SubEntry: + def __init__(self, pos, html_data): + self.pos = pos + self.html = HtmlProcessor(html_data) + self.title = self.html.title + self._name = 'mobi_article_%d' % pos + if not self.title: + self.title = 'Article %d' % self.pos + + def TocLink(self): + return '<a href="#%s_MOBI_START">%.80s</a>' % (self._name, self.title) + + def Anchor(self): + return '<a name="%s_MOBI_START">' % self._name + + def Body(self): + return self.html.RenameAnchors(self._name + '_') + +class Converter: + def __init__(self, refresh_url='', title='Unknown', author='Unknown', publisher='Unknown'): + self._header = Header() + self._header.SetTitle(title) + self._header.SetAuthor(author) + self._header.SetPublisher(publisher) + self._refresh_url = refresh_url + + def ConvertString(self, s): + out = StringIO.StringIO() + self._ConvertStringToFile(s, out) + return out.getvalue() + + def ConvertStrings(self, html_strs): + out = StringIO.StringIO() + self._ConvertStringsToFile(html_strs, out) + return out.getvalue() + + def ConvertFile(self, html_file, out_file): + self._ConvertStringToFile(open(html_file,'rb').read(), + open(out_file, 'wb')) + + def ConvertFiles(self, html_files, out_file): + html_strs = [open(f,'rb').read() for f in html_files] + self._ConvertStringsToFile(html_strs, open(out_file, 'wb')) + + def MakeOneHTML(self, html_strs): + """This takes a list of HTML strings and returns a big HTML file with + all contents consolidated. It constructs a table of contents and adds + anchors within the text + """ + title_html = [] + toc_html = [] + body_html = [] + + PAGE_BREAK = '<mbp:pagebreak>' + + # pull out the title page, assumed first html_strs. + htmltitle = html_strs[0] + entrytitle = _SubEntry(1, htmltitle) + title_html.append(entrytitle.Body()) + + title_html.append(PAGE_BREAK) + toc_html.append('<a name="TOCTOP"><h3>Table of Contents</h3><br />') + + for pos, html in enumerate(html_strs[1:]): + entry = _SubEntry(pos+1, html) + toc_html.append('%s<br />' % entry.TocLink()) + + # give some space between bodies of work. + body_html.append(PAGE_BREAK) + + body_html.append(entry.Anchor()) + + body_html.append(entry.Body()) + + # TODO: this title can get way too long with RSS feeds. Not sure how to fix + # cheat slightly and use the <a href> code to set filepos in references. + header = '''<html> +<head> +<title>Bibliorize %s GMT + + + + + +''' % time.ctime(time.time()) + + footer = '' + all_html = header + '\n'.join(title_html + toc_html + body_html) + footer + #print "%s" % all_html.encode('utf8') + return all_html + + def _ConvertStringsToFile(self, html_strs, out_file): + try: + tmp = self.MakeOneHTML(html_strs) + self._ConvertStringToFile(tmp, out_file) + except Exception, e: + logger.error('Error %s', e) + #logger.debug('Details: %s' % html_strs) + + def _ConvertStringToFile(self, html_data, out): + html = HtmlProcessor(html_data) + data = html.CleanHtml() + + # collect offsets of '' tags, use to make index list. + # indexlist = [] # list of (offset,length) tuples. + # not in current use. + + # j=0 + # lastj=0 + # while True: + # j=data.find('',lastj+10) # plus a bit so we find the next. + # if j < 0: + # break + # indexlist.append((lastj,j-lastj)) + # print "index offset: %d length: %d" % (lastj,j-lastj) + # lastj=j + + records = [] +# title = html.title +# if title: +# self._header.SetTitle(title) + record_id = 1 + for start_pos in range(0, len(data), Record.MAX_SIZE): + end = min(len(data), start_pos + Record.MAX_SIZE) + record_data = data[start_pos:end] + records.append(self._header.AddRecord(record_data, record_id)) + #print "HTML Record %03d: (size:%d) [[%s ... %s]]" % ( record_id, len(record_data), record_data[:20], record_data[-20:] ) + record_id += 1 + self._header.SetImageRecordIndex(record_id) + records[0:0] = [self._header.MobiHeader()] + + header, rec_offset = self._header.PDBHeader(len(records)) + out.write(header) + for record in records: + record.WriteHeader(out, rec_offset) + #print "rec_offset: %d len(record.data): %d" % (rec_offset,len(record.data)) + rec_offset += (len(record.data)+1) # plus one for trailing null + + # Write to nuls for some reason + out.write('\0\0') + for record in records: + record.WriteData(out) + out.write('\0') + # needs a trailing null, I believe it indicates zero length 'overlap'. + # otherwise, the readers eat the last char of each html record. + # Calibre writes another 6-7 bytes of stuff after that, but we seem + # to be getting along without it. + +class Record: + MAX_SIZE = 4096 + INDEX_LEN = 8 + _unique_id_seed = 28 # should be arbitrary, but taken from MobiHeader + + # TODO(chatham): Record compression doesn't look that hard. + + def __init__(self, data, record_id): + assert len(data) <= self.MAX_SIZE + self.data = data + if record_id != 0: + self._id = record_id + else: + Record._unique_id_seed += 1 + self._id = 0 + + def __repr__(self): + return 'Record: id=%d len=%d' % (self._id, len(self.data)) + + def _SetUniqueId(self): + Record._unique_id_seed += 1 + # TODO(chatham): Wraparound crap + self._id = Record._unique_id_seed + + def WriteData(self, out): + out.write(self.data) + + def WriteHeader(self, out, rec_offset): + attributes = 64 # dirty? + header = struct.pack('>IbbH', + rec_offset, + attributes, + 0, self._id) + assert len(header) == Record.INDEX_LEN + out.write(header) + +EXTH_HEADER_FIELDS = { + 'author' : 100, + 'publisher' : 101, +} + +class Header: + EPOCH_1904 = 2082844800 + + def __init__(self): + self._length = 0 + self._record_count = 0 + self._title = '2008_2_34' + self._author = 'Unknown author' + self._publisher = 'Unknown publisher' + self._first_image_index = 0 + + def SetAuthor(self, author): + self._author = author.encode('ascii','ignore') + + def SetTitle(self, title): + # TODO(chatham): Reevaluate whether this needs to be ASCII. + # maybe just do sys.setdefaultencoding('utf-8')? Problems + # appending self._title with other things. + self._title = title.encode('ascii','ignore') + + def SetPublisher(self, publisher): + self._publisher = publisher.encode('ascii','ignore') + + def AddRecord(self, data, record_id): + self.max_record_size = max(Record.MAX_SIZE, len(data)) + self._record_count += 1 + self._length += len(data) + return Record(data, record_id) + + def _ReplaceWord(self, data, pos, word): + return data[:pos] + struct.pack('>I', word) + data[pos+4:] + + def PalmDocHeader(self): + compression = 1 # no compression + unused = 0 + encryption_type = 0 # no ecryption + records = self._record_count + 1 # the header record itself + palmdoc_header = struct.pack('>HHIHHHH', + compression, + unused, + self._length, + records, + Record.MAX_SIZE, + encryption_type, + unused) + assert len(palmdoc_header) == 16 + return palmdoc_header + + def PDBHeader(self, num_records): + HEADER_LEN = 32+2+2+9*4 + RECORD_INDEX_HEADER_LEN = 6 + RESOURCE_INDEX_LEN = 10 + + index_len = RECORD_INDEX_HEADER_LEN + num_records * Record.INDEX_LEN + rec_offset = HEADER_LEN + index_len + 2 + + short_title = self._title[0:31] + attributes = 0 + version = 0 + ctime = self.EPOCH_1904 + int(time.time()) + mtime = self.EPOCH_1904 + int(time.time()) + backup_time = self.EPOCH_1904 + int(time.time()) + modnum = 0 + appinfo_offset = 0 + sort_offset = 0 + type = 'BOOK' + creator = 'MOBI' + id_seed = 36 + header = struct.pack('>32sHHII', + short_title, attributes, version, + ctime, mtime) + header += struct.pack('>IIII', backup_time, modnum, + appinfo_offset, sort_offset) + header += struct.pack('>4s4sI', + type, creator, id_seed) + next_record = 0 # not used? + header += struct.pack('>IH', next_record, num_records) + return header, rec_offset + + def _GetExthHeader(self): + # They set author, publisher, coveroffset, thumboffset + data = {'author' : self._author, + 'publisher' : self._publisher, + } + # Turn string type names into EXTH typeids. + r = [] + for key, value in data.items(): + typeid = EXTH_HEADER_FIELDS[key] + length_encoding_len = 8 + r.append(struct.pack('>LL', typeid, len(value) + length_encoding_len,) + value) + content = ''.join(r) + + # Pad to word boundary + while len(content) % 4: + content += '\0' + TODO_mysterious = 12 + exth = 'EXTH' + struct.pack('>LL', len(content) + TODO_mysterious, len(data)) + content + return exth + + def SetImageRecordIndex(self, idx): + self._first_image_index = idx + + def MobiHeader(self): + exth_header = self._GetExthHeader(); + palmdoc_header = self.PalmDocHeader() + + fs = 0xffffffff + + # Record 0 + header_len = 0xE4 # TODO + mobi_type = 2 # BOOK + text_encoding = encoding['UTF-8'] + unique_id = random.randint(1, 1<<32) + creator_version = 4 + reserved = '%c' % 0xff * 40 + nonbook_index = fs + full_name_offset = header_len + len(palmdoc_header) + len(exth_header) # put full name after header + language = languages['en-us'] + unused = 0 + mobi_header = struct.pack('>4sIIIII40sIIIIII', + 'MOBI', + header_len, + mobi_type, + text_encoding, + unique_id, + creator_version, + reserved, + nonbook_index, + full_name_offset, + len(self._title), + language, + fs, fs) + assert len(mobi_header) == 104 - 16 + + unknown_fields = chr(0) * 32 + drm_offset = 0 + drm_count = 0 + drm_size = 0 + drm_flags = 0 + exth_flags = 0x50 + header_end = chr(0) * 64 + mobi_header += struct.pack('>IIIIIII', + creator_version, + self._first_image_index, + fs, + unused, + fs, + unused, + exth_flags) + mobi_header += '\0' * 112 # TODO: Why this much padding? + # Set some magic offsets to be 0xFFFFFFF. + for pos in (0x94, 0x98, 0xb0, 0xb8, 0xc0, 0xc8, 0xd0, 0xd8, 0xdc): + mobi_header = self._ReplaceWord(mobi_header, pos, fs) + + # 16 bytes? + padding = '\0' * 48 * 4 # why? + total_header = palmdoc_header + mobi_header + exth_header + self._title + padding + + return self.AddRecord(total_header, 0) + +if __name__ == '__main__': + import sys + m = Converter(title='Testing Mobi', author='Mobi Author', publisher='mobi converter') + m.ConvertFiles(sys.argv[1:], 'test.mobi') + #m.ConvertFile(sys.argv[1], 'test.mobi') diff --git a/fanficdownloader/story.py b/fanficdownloader/story.py new file mode 100644 index 0000000..d3bad27 --- /dev/null +++ b/fanficdownloader/story.py @@ -0,0 +1,873 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import os, re +import urlparse +import string +from math import floor +from functools import partial +import logging +logger = logging.getLogger(__name__) +import urlparse as up + +import exceptions +from htmlcleanup import conditionalRemoveEntities, removeAllEntities +from configurable import Configurable + +SPACE_REPLACE=u'\s' +SPLIT_META=u'\,' + +# Create convert_image method depending on which graphics lib we can +# load. Preferred: calibre, PIL, none + +imagetypes = { + 'jpg':'image/jpeg', + 'jpeg':'image/jpeg', + 'png':'image/png', + 'gif':'image/gif', + 'svg':'image/svg+xml', + } + +try: + from calibre.utils.magick import Image + convtype = {'jpg':'JPG', 'png':'PNG'} + + def convert_image(url,data,sizes,grayscale, + removetrans,imgtype="jpg",background='#ffffff'): + export = False + img = Image() + img.load(data) + + owidth, oheight = img.size + nwidth, nheight = sizes + scaled, nwidth, nheight = fit_image(owidth, oheight, nwidth, nheight) + if scaled: + img.size = (nwidth, nheight) + export = True + + if normalize_format_name(img.format) != imgtype: + export = True + + if removetrans and img.has_transparent_pixels(): + canvas = Image() + canvas.create_canvas(int(img.size[0]), int(img.size[1]), str(background)) + canvas.compose(img) + img = canvas + export = True + + if grayscale and img.type != "GrayscaleType": + img.type = "GrayscaleType" + export = True + + if export: + return (img.export(convtype[imgtype]),imgtype,imagetypes[imgtype]) + else: + logger.debug("image used unchanged") + return (data,imgtype,imagetypes[imgtype]) + +except: + + # No calibre routines, try for PIL for CLI. + try: + import Image + from StringIO import StringIO + convtype = {'jpg':'JPEG', 'png':'PNG'} + def convert_image(url,data,sizes,grayscale, + removetrans,imgtype="jpg",background='#ffffff'): + export = False + img = Image.open(StringIO(data)) + + owidth, oheight = img.size + nwidth, nheight = sizes + scaled, nwidth, nheight = fit_image(owidth, oheight, nwidth, nheight) + if scaled: + img = img.resize((nwidth, nheight),Image.ANTIALIAS) + export = True + + if normalize_format_name(img.format) != imgtype: + if img.mode == "P": + # convert pallete gifs to RGB so jpg save doesn't fail. + img = img.convert("RGB") + export = True + + if removetrans and img.mode == "RGBA": + background = Image.new('RGBA', img.size, background) + # Paste the image on top of the background + background.paste(img, img) + img = background.convert('RGB') + export = True + + if grayscale and img.mode != "L": + img = img.convert("L") + export = True + + if export: + outsio = StringIO() + img.save(outsio,convtype[imgtype]) + return (outsio.getvalue(),imgtype,imagetypes[imgtype]) + else: + logger.debug("image used unchanged") + return (data,imgtype,imagetypes[imgtype]) + + except: + # No calibre or PIL, simple pass through with mimetype. + def convert_image(url,data,sizes,grayscale, + removetrans,imgtype="jpg",background='#ffffff'): + return no_convert_image(url,data) + +## also used for explicit no image processing. +def no_convert_image(url,data): + parsedUrl = up.urlparse(url) + + ext=parsedUrl.path[parsedUrl.path.rfind('.')+1:].lower() + + if ext not in imagetypes: + logger.debug("no_convert_image url:%s - no known extension"%url) + # doesn't have extension? use jpg. + ext='jpg' + + return (data,ext,imagetypes[ext]) + +def normalize_format_name(fmt): + if fmt: + fmt = fmt.lower() + if fmt == 'jpeg': + fmt = 'jpg' + return fmt + +def fit_image(width, height, pwidth, pheight): + ''' + Fit image in box of width pwidth and height pheight. + @param width: Width of image + @param height: Height of image + @param pwidth: Width of box + @param pheight: Height of box + @return: scaled, new_width, new_height. scaled is True iff new_width and/or new_height is different from width or height. + ''' + scaled = height > pheight or width > pwidth + if height > pheight: + corrf = pheight/float(height) + width, height = floor(corrf*width), pheight + if width > pwidth: + corrf = pwidth/float(width) + width, height = pwidth, floor(corrf*height) + if height > pheight: + corrf = pheight/float(height) + width, height = floor(corrf*width), pheight + + return scaled, int(width), int(height) + +try: + # doesn't really matter what, just checking for appengine. + from google.appengine.api import apiproxy_stub_map + + is_appengine = True +except: + is_appengine = False + + +# The list comes from ffnet, the only multi-language site we support +# at the time of writing. Values are taken largely from pycountry, +# but with some corrections and guesses. +langs = { + "English":"en", + "Spanish":"es", + "French":"fr", + "German":"de", + "Chinese":"zh", + "Japanese":"ja", + "Dutch":"nl", + "Portuguese":"pt", + "Russian":"ru", + "Italian":"it", + "Bulgarian":"bg", + "Polish":"pl", + "Hungarian":"hu", + "Hebrew":"he", + "Arabic":"ar", + "Swedish":"sv", + "Norwegian":"no", + "Danish":"da", + "Finnish":"fi", + "Filipino":"fil", + "Esperanto":"eo", + "Hindi":"hi", + "Punjabi":"pa", + "Farsi":"fa", + "Greek":"el", + "Romanian":"ro", + "Albanian":"sq", + "Serbian":"sr", + "Turkish":"tr", + "Czech":"cs", + "Indonesian":"id", + "Croatian":"hr", + "Catalan":"ca", + "Latin":"la", + "Korean":"ko", + "Vietnamese":"vi", + "Thai":"th", + "Devanagari":"hi", + } + +def re_compile(regex,line): + try: + return re.compile(regex) + except Exception, e: + raise exceptions.RegularExpresssionFailed(e,regex,line) + +class InExMatch: + keys = [] + regex = None + match = None + negate = False + + def __init__(self,line): + if "=~" in line: + (self.keys,self.match) = line.split("=~") + self.match = self.match.replace(SPACE_REPLACE,' ') + self.regex = re_compile(self.match,line) + elif "!~" in line: + (self.keys,self.match) = line.split("!~") + self.match = self.match.replace(SPACE_REPLACE,' ') + self.regex = re_compile(self.match,line) + self.negate = True + elif "==" in line: + (self.keys,self.match) = line.split("==") + self.match = self.match.replace(SPACE_REPLACE,' ') + elif "!=" in line: + (self.keys,self.match) = line.split("!=") + self.match = self.match.replace(SPACE_REPLACE,' ') + self.negate = True + self.keys = map( lambda x: x.strip(), self.keys.split(",") ) + + # For conditional, only one key + def is_key(self,key): + return key == self.keys[0] + + # For conditional, only one key + def key(self): + return self.keys[0] + + def in_keys(self,key): + return key in self.keys + + def is_match(self,value): + retval = False + if self.regex: + if self.regex.search(value): + retval = True + #print(">>>>>>>>>>>>>%s=~%s r: %s,%s=%s"%(self.match,value,self.negate,retval,self.negate != retval)) + else: + retval = self.match == value + #print(">>>>>>>>>>>>>%s==%s r: %s,%s=%s"%(self.match,value,self.negate,retval, self.negate != retval)) + + return self.negate != retval + + def __str__(self): + if self.negate: + f='!' + else: + f='=' + if self.regex: + s='~' + else: + s='=' + return u'InExMatch(%s %s%s %s)'%(self.keys,f,s,self.match) + +class Story(Configurable): + + def __init__(self, configuration): + Configurable.__init__(self, configuration) + try: + ## calibre plugin will set externally to match PI version. + self.metadata = {'version':os.environ['CURRENT_VERSION_ID']} + except: + self.metadata = {'version':'4.4'} + self.replacements = [] + self.in_ex_cludes = {} + self.chapters = [] # chapters will be tuples of (title,html) + self.imgurls = [] + self.imgtuples = [] + + self.cover=None # *href* of new cover image--need to create html. + self.oldcover=None # (oldcoverhtmlhref,oldcoverhtmltype,oldcoverhtmldata,oldcoverimghref,oldcoverimgtype,oldcoverimgdata) + self.calibrebookmark=None # cheesy way to carry calibre bookmark file forward across update. + self.logfile=None # cheesy way to carry log file forward across update. + + ## Look for config parameter, split and add each to metadata field. + for (config,metadata) in [("extracategories","category"), + ("extragenres","genre"), + ("extracharacters","characters"), + ("extraships","ships"), + ("extrawarnings","warnings")]: + for val in self.getConfigList(config): + self.addToList(metadata,val) + + self.setReplace(self.getConfig('replace_metadata')) + + in_ex_clude_list = ['include_metadata_pre','exclude_metadata_pre', + 'include_metadata_post','exclude_metadata_post'] + for ie in in_ex_clude_list: + ies = self.getConfig(ie) + # print("%s %s"%(ie,ies)) + if ies: + iel = [] + self.in_ex_cludes[ie] = self.set_in_ex_clude(ies) + + def join_list(self, key, vallist): + return self.getConfig("join_string_"+key,u", ").replace(SPACE_REPLACE,' ').join(map(unicode, vallist)) + + def setMetadata(self, key, value, condremoveentities=True): + + # keep as list type, but set as only value. + if self.isList(key): + self.addToList(key,value,condremoveentities=condremoveentities,clear=True) + else: + ## still keeps < < and & + if condremoveentities: + self.metadata[key]=conditionalRemoveEntities(value) + else: + self.metadata[key]=value + + if key == "language": + try: + # getMetadata not just self.metadata[] to do replace_metadata. + self.setMetadata('langcode',langs[self.getMetadata(key)]) + except: + self.setMetadata('langcode','en') + + if key == 'dateUpdated' and value: + # Last Update tags for Bill. + self.addToList('lastupdate',value.strftime("Last Update Year/Month: %Y/%m")) + self.addToList('lastupdate',value.strftime("Last Update: %Y/%m/%d")) + + + ## metakey[,metakey]=~pattern + ## metakey[,metakey]==string + ## *for* part lines. Effect only when trailing conditional key=~regexp matches + ## metakey[,metakey]=~pattern[&&metakey=~regexp] + ## metakey[,metakey]==string[&&metakey=~regexp] + ## metakey[,metakey]=~pattern[&&metakey==string] + ## metakey[,metakey]==string[&&metakey==string] + def set_in_ex_clude(self,setting): + dest = [] + # print("set_in_ex_clude:"+setting) + for line in setting.splitlines(): + if line: + (match,condmatch)=(None,None) + if "&&" in line: + (line,conditional) = line.split("&&") + condmatch = InExMatch(conditional) + match = InExMatch(line) + dest.append([match,condmatch]) + return dest + + def do_in_ex_clude(self,which,value,key): + if value and which in self.in_ex_cludes: + include = 'include' in which + keyfound = False + found = False + for (match,condmatch) in self.in_ex_cludes[which]: + keyfndnow = False + if match.in_keys(key): + # key in keys and either no conditional, or conditional matched + if condmatch == None or condmatch.is_key(key): + keyfndnow = True + else: + condval = self.getMetadata(condmatch.key()) + keyfndnow = condmatch.is_match(condval) + keyfound |= keyfndnow + # print("match:%s %s\ncondmatch:%s %s\n\tkeyfound:%s\n\tfound:%s"%( + # match,value,condmatch,condval,keyfound,found)) + if keyfndnow: + found = isinstance(value,basestring) and match.is_match(value) + if found: + # print("match:%s %s\n\tkeyfndnow:%s\n\tfound:%s"%( + # match,value,keyfndnow,found)) + if not include: + value = None + break + if include and keyfound and not found: + value = None + return value + + + ## Two or three part lines. Two part effect everything. + ## Three part effect only those key(s) lists. + ## pattern=>replacement + ## metakey,metakey=>pattern=>replacement + ## *Five* part lines. Effect only when trailing conditional key=>regexp matches + ## metakey[,metakey]=>pattern=>replacement[&&metakey=>regexp] + def setReplace(self,replace): + for line in replace.splitlines(): + # print("replacement line:%s"%line) + (metakeys,regexp,replacement,condkey,condregexp)=(None,None,None,None,None) + if "&&" in line: + (line,conditional) = line.split("&&") + (condkey,condregexp) = conditional.split("=>") + if "=>" in line: + parts = line.split("=>") + if len(parts) > 2: + metakeys = map( lambda x: x.strip(), parts[0].split(",") ) + (regexp,replacement)=parts[1:] + else: + (regexp,replacement)=parts + + if regexp: + regexp = re_compile(regexp,line) + if condregexp: + condregexp = re_compile(condregexp,line) + # A way to explicitly include spaces in the + # replacement string. The .ini parser eats any + # trailing spaces. + replacement=replacement.replace(SPACE_REPLACE,' ') + self.replacements.append([metakeys,regexp,replacement,condkey,condregexp]) + + def doReplacements(self,value,key,return_list=False,seen_list=[]): + value = self.do_in_ex_clude('include_metadata_pre',value,key) + value = self.do_in_ex_clude('exclude_metadata_pre',value,key) + + retlist = [value] + for replaceline in self.replacements: + if replaceline in seen_list: # recursion on pattern, bail + # print("bailing on %s"%replaceline) + continue + #print("replacement tuple:%s"%replaceline) + (metakeys,regexp,replacement,condkey,condregexp) = replaceline + if (metakeys == None or key in metakeys) \ + and isinstance(value,basestring) \ + and regexp.search(value): + doreplace=True + if condkey and condkey != key: # prevent infinite recursion. + condval = self.getMetadata(condkey) + doreplace = condval != None and condregexp.search(condval) + + if doreplace: + # split into more than one list entry if + # SPLIT_META present in replacement string. Split + # first, then regex sub, then recurse call replace + # on each. Break out of loop, each split element + # handled individually by recursion call. + if SPLIT_META in replacement: + retlist = [] + for splitrepl in replacement.split(SPLIT_META): + retlist.extend(self.doReplacements(regexp.sub(splitrepl,value), + key, + return_list=True, + seen_list=seen_list+[replaceline])) + break + else: + # print("replacement,value:%s,%s->%s"%(replacement,value,regexp.sub(replacement,value))) + value = regexp.sub(replacement,value) + retlist = [value] + + for val in retlist: + retlist = map(partial(self.do_in_ex_clude,'include_metadata_post',key=key),retlist) + retlist = map(partial(self.do_in_ex_clude,'exclude_metadata_post',key=key),retlist) + # value = self.do_in_ex_clude('include_metadata_post',value,key) + # value = self.do_in_ex_clude('exclude_metadata_post',value,key) + + if return_list: + return retlist + else: + return self.join_list(key,retlist) + + def getMetadataRaw(self,key): + if self.isValidMetaEntry(key) and self.metadata.has_key(key): + return self.metadata[key] + + def getMetadata(self, key, + removeallentities=False, + doreplacements=True): + value = None + if not self.isValidMetaEntry(key): + return value + + if self.isList(key): + # join_string = self.getConfig("join_string_"+key,u", ").replace(SPACE_REPLACE,' ') + # value = join_string.join(self.getList(key, removeallentities, doreplacements=True)) + value = self.join_list(key,self.getList(key, removeallentities, doreplacements=True)) + if doreplacements: + value = self.doReplacements(value,key+"_LIST") + return value + elif self.metadata.has_key(key): + value = self.metadata[key] + if value: + if key == "numWords": + value = commaGroups(value) + if key == "numChapters": + value = commaGroups("%d"%value) + if key in ("dateCreated"): + value = value.strftime(self.getConfig(key+"_format","%Y-%m-%d %H:%M:%S")) + if key in ("datePublished","dateUpdated"): + value = value.strftime(self.getConfig(key+"_format","%Y-%m-%d")) + + if doreplacements: + value=self.doReplacements(value,key) + if removeallentities and value != None: + return removeAllEntities(value) + else: + return value + else: #if self.getConfig("default_value_"+key): + return self.getConfig("default_value_"+key) + + def getAllMetadata(self, + removeallentities=False, + doreplacements=True, + keeplists=False): + ''' + All single value *and* list value metadata as strings (unless + keeplists=True, then keep lists). + ''' + allmetadata = {} + + # special handling for authors/authorUrls + linkhtml="%s" + if self.isList('author'): # more than one author, assume multiple authorUrl too. + htmllist=[] + for i, v in enumerate(self.getList('author')): + aurl = self.getList('authorUrl')[i] + auth = v + # make sure doreplacements & removeallentities are honored. + if doreplacements: + aurl=self.doReplacements(aurl,'authorUrl') + auth=self.doReplacements(auth,'author') + if removeallentities: + aurl=removeAllEntities(aurl) + auth=removeAllEntities(auth) + + htmllist.append(linkhtml%('author',aurl,auth)) + # join_string = self.getConfig("join_string_authorHTML",u", ").replace(SPACE_REPLACE,' ') + self.setMetadata('authorHTML',self.join_list("join_string_authorHTML",htmllist)) + else: + self.setMetadata('authorHTML',linkhtml%('author',self.getMetadata('authorUrl', removeallentities, doreplacements), + self.getMetadata('author', removeallentities, doreplacements))) + + self.extendList("extratags",self.getConfigList("extratags")) + + if self.getMetadataRaw('seriesUrl'): + self.setMetadata('seriesHTML',linkhtml%('series',self.getMetadata('seriesUrl', removeallentities, doreplacements), + self.getMetadata('series', removeallentities, doreplacements))) + elif self.getMetadataRaw('series'): + self.setMetadata('seriesHTML',self.getMetadataRaw('series')) + + # logger.debug("make_linkhtml_entries:%s"%self.getConfig('make_linkhtml_entries')) + for k in self.getConfigList('make_linkhtml_entries'): + # Assuming list, because it has to be site specific and + # they are all lists. Bail if kUrl list not the same + # length. + # logger.debug("\nk:%s\nlist:%s\nlistURL:%s"%(k,self.getList(k),self.getList(k+'Url'))) + if len(self.getList(k+'Url')) != len(self.getList(k)): + continue + htmllist=[] + for i, v in enumerate(self.getList(k)): + url = self.getList(k+'Url')[i] + # make sure doreplacements & removeallentities are honored. + if doreplacements: + url=self.doReplacements(url,k+'Url') + v=self.doReplacements(v,k) + if removeallentities: + url=removeAllEntities(url) + v=removeAllEntities(v) + + htmllist.append(linkhtml%(k,url,v)) + # join_string = self.getConfig("join_string_"+k+"HTML",u", ").replace(SPACE_REPLACE,' ') + self.setMetadata(k+'HTML',self.join_list("join_string_"+k+"HTML",htmllist)) + + for k in self.getValidMetaList(): + if self.isList(k) and keeplists: + allmetadata[k] = self.getList(k, removeallentities, doreplacements) + else: + allmetadata[k] = self.getMetadata(k, removeallentities, doreplacements) + + return allmetadata + + # just for less clutter in adapters. + def extendList(self,listname,l): + for v in l: + self.addToList(listname,v.strip()) + + def addToList(self,listname,value,condremoveentities=True,clear=False): + if value==None: + return + if condremoveentities: + value = conditionalRemoveEntities(value) + if clear or not self.isList(listname) or not listname in self.metadata: + # Calling addToList to a non-list meta will overwrite it. + self.metadata[listname]=[] + # prevent duplicates. + if not value in self.metadata[listname]: + self.metadata[listname].append(value) + + if listname == 'category' and self.getConfig('add_genre_when_multi_category') and len(self.metadata[listname]) > 1: + self.addToList('genre',self.getConfig('add_genre_when_multi_category')) + + def isList(self,listname): + 'Everything set with an include_in_* is considered a list.' + return self.isListType(listname) or \ + ( self.isValidMetaEntry(listname) and self.metadata.has_key(listname) \ + and isinstance(self.metadata[listname],list) ) + + def getList(self,listname, + removeallentities=False, + doreplacements=True, + includelist=[]): + #print("getList(%s,%s)"%(listname,includelist)) + retlist = [] + + if not self.isValidMetaEntry(listname): + return retlist + + # includelist prevents infinite recursion of include_in_'s + if self.hasConfig("include_in_"+listname) and listname not in includelist: + for k in self.getConfigList("include_in_"+listname): + retlist.extend(self.getList(k,removeallentities=False, + doreplacements=doreplacements,includelist=includelist+[listname])) + else: + + if not self.isList(listname): + retlist = [self.getMetadata(listname,removeallentities=False, + doreplacements=doreplacements)] + else: + retlist = self.getMetadataRaw(listname) + + if retlist: + if doreplacements: + newretlist = [] + for val in retlist: + newretlist.extend(self.doReplacements(val,listname,return_list=True)) + retlist = newretlist + + if removeallentities: + retlist = map(removeAllEntities,retlist) + + retlist = filter( lambda x : x!=None and x!='' ,retlist) + + # reorder ships so b/a and c/b/a become a/b and a/b/c. Only on '/', + # use replace_metadata to change separator first if needed. + # ships=>[ ]*(/|&|&)[ ]*=>/ + if listname == 'ships' and self.getConfig('sort_ships') and retlist: + retlist = [ '/'.join(sorted(x.split('/'))) for x in retlist ] + + if retlist: + if listname in ('author','authorUrl','authorId') or self.getConfig('keep_in_order_'+listname): + # need to retain order for author & authorUrl so the + # two match up. + return retlist + else: + # remove dups and sort. + return sorted(list(set(retlist))) + else: + return [] + + def getSubjectTags(self, removeallentities=False): + # set to avoid duplicates subject tags. + subjectset = set() + + tags_list = self.getConfigList("include_subject_tags") + self.getConfigList("extra_subject_tags") + + # metadata all go into dc:subject tags, but only if they are configured. + for (name,value) in self.getAllMetadata(removeallentities=removeallentities,keeplists=True).iteritems(): + if name in tags_list: + if isinstance(value,list): + for tag in value: + subjectset.add(tag) + else: + subjectset.add(value) + + if None in subjectset: + subjectset.remove(None) + if '' in subjectset: + subjectset.remove('') + + return list(subjectset | set(self.getConfigList("extratags"))) + + def addChapter(self, url, title, html): + if self.getConfig('strip_chapter_numbers') and \ + self.getConfig('chapter_title_strip_pattern'): + title = re.sub(self.getConfig('chapter_title_strip_pattern'),"",title) + self.chapters.append( (url,title,html) ) + + def getChapters(self,fortoc=False): + "Chapters will be tuples of (title,html)" + retval = [] + ## only add numbers if more than one chapter. + if len(self.chapters) > 1 and \ + (self.getConfig('add_chapter_numbers') == "true" \ + or (self.getConfig('add_chapter_numbers') == "toconly" and fortoc)) \ + and self.getConfig('chapter_title_add_pattern'): + for index, (url,title,html) in enumerate(self.chapters): + retval.append( (url, + string.Template(self.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() + # fall back default: + if not template: + template="${title}-${siteabbrev}_${storyId}${formatext}" + + if not allowunsafefilename: + values={} + pattern = re_compile(self.getConfig("output_filename_safepattern",r"[^a-zA-Z0-9_\. \[\]\(\)&'-]+"),"output_filename_safepattern") + for k in origvalues.keys(): + values[k]=re.sub(pattern,'_', removeAllEntities(self.getMetadata(k))) + + return string.Template(template).substitute(values).encode('utf8') + + # pass fetch in from adapter in case we need the cookies collected + # as well as it's a base_story class method. + def addImgUrl(self,parenturl,url,fetch,cover=False,coverexclusion=None): + + # otherwise it saves the image in the epub even though it + # isn't used anywhere. + if cover and self.getConfig('never_make_cover'): + return (None,None) + + url = url.strip() # ran across an image with a space in the + # src. Browser handled it, so we'd better, too. + + # appengine (web version) isn't allowed to do images--just + # gets too big too fast and breaks things. + if is_appengine: + return (None,None) + + if url.startswith("http") or url.startswith("file") or parenturl == None: + imgurl = url + else: + parsedUrl = urlparse.urlparse(parenturl) + if url.startswith("//") : + imgurl = urlparse.urlunparse( + (parsedUrl.scheme, + '', + url, + '','','')) + elif url.startswith("/") : + imgurl = urlparse.urlunparse( + (parsedUrl.scheme, + parsedUrl.netloc, + url, + '','','')) + else: + toppath="" + if parsedUrl.path.endswith("/"): + toppath = parsedUrl.path + else: + toppath = parsedUrl.path[:parsedUrl.path.rindex('/')] + imgurl = urlparse.urlunparse( + (parsedUrl.scheme, + parsedUrl.netloc, + toppath + '/' + url, + '','','')) + #print("\n===========\nparsedUrl.path:%s\ntoppath:%s\nimgurl:%s\n\n"%(parsedUrl.path,toppath,imgurl)) + + # apply coverexclusion to explicit covers, too. Primarily for ffnet imageu. + if cover and coverexclusion and re.search(coverexclusion,imgurl): + return (None,None) + + prefix='ffdl' + if imgurl not in self.imgurls: + parsedUrl = urlparse.urlparse(imgurl) + + try: + if self.getConfig('no_image_processing'): + (data,ext,mime) = no_convert_image(imgurl, + fetch(imgurl)) + else: + try: + sizes = [ int(x) for x in self.getConfigList('image_max_size') ] + except Exception, e: + raise exceptions.FailedToDownload("Failed to parse image_max_size from personal.ini:%s\nException: %s"%(self.getConfigList('image_max_size'),e)) + grayscale = self.getConfig('grayscale_images') + imgtype = self.getConfig('convert_images_to') + if not imgtype: + imgtype = "jpg" + removetrans = self.getConfig('remove_transparency') + removetrans = removetrans or grayscale or imgtype=="jpg" + (data,ext,mime) = convert_image(imgurl, + fetch(imgurl), + sizes, + grayscale, + removetrans, + imgtype, + background="#"+self.getConfig('background_color')) + except Exception, e: + logger.info("Failed to load or convert image, skipping:\n%s\nException: %s"%(imgurl,e)) + return ("failedtoload","failedtoload") + + # explicit cover, make the first image. + if cover: + if len(self.imgtuples) > 0 and 'cover' in self.imgtuples[0]['newsrc']: + # remove existing cover, if there is one. + del self.imgurls[0] + del self.imgtuples[0] + self.imgurls.insert(0,imgurl) + newsrc = "images/cover.%s"%ext + self.cover=newsrc + self.imgtuples.insert(0,{'newsrc':newsrc,'mime':mime,'data':data}) + else: + self.imgurls.append(imgurl) + # First image, copy not link because calibre will replace with it's cover. + # Only if: No cover already AND + # make_firstimage_cover AND + # NOT never_make_cover AND + # either no coverexclusion OR coverexclusion doesn't match + if self.cover == None and \ + self.getConfig('make_firstimage_cover') and \ + not self.getConfig('never_make_cover') and \ + not (coverexclusion and re.search(coverexclusion,imgurl)): + newsrc = "images/cover.%s"%ext + self.cover=newsrc + self.imgtuples.append({'newsrc':newsrc,'mime':mime,'data':data}) + self.imgurls.append(imgurl) + + newsrc = "images/%s-%s.%s"%( + prefix, + self.imgurls.index(imgurl), + ext) + self.imgtuples.append({'newsrc':newsrc,'mime':mime,'data':data}) + + #logger.debug("\nimgurl:%s\nnewsrc:%s\nimage size:%d\n"%(imgurl,newsrc,len(data))) + else: + newsrc = self.imgtuples[self.imgurls.index(imgurl)]['newsrc'] + + #print("===============\n%s\nimg url:%s\n============"%(newsrc,self.imgurls[-1])) + + return (newsrc, imgurl) + + def getImgUrls(self): + retlist = [] + for i, url in enumerate(self.imgurls): + #parsedUrl = urlparse.urlparse(url) + retlist.append(self.imgtuples[i]) + return retlist + + def __str__(self): + return "Metadata: " +str(self.metadata) + +def commaGroups(s): + groups = [] + while s and s[-1].isdigit(): + groups.append(s[-3:]) + s = s[:-3] + return s + ','.join(reversed(groups)) + diff --git a/fanficdownloader/translit.py b/fanficdownloader/translit.py new file mode 100644 index 0000000..bf205a6 --- /dev/null +++ b/fanficdownloader/translit.py @@ -0,0 +1,57 @@ +#-*-coding:utf-8-*- +# Code taken from http://python.su/forum/viewtopic.php?pid=66946 +import unicodedata +def is_syllable(letter): + syllables = ("A", "E", "I", "O", "U", "a", "e", "i", "o", "u") + if letter in syllables: + return True + return False +def is_consonant(letter): + return not is_syllable(letter) +def romanize(letter): + try: + str(letter) + except UnicodeEncodeError: + pass + else: + return str(letter) + unid = unicodedata.name(letter) + exceptions = {"NUMERO SIGN": "No", "LEFT-POINTING DOUBLE ANGLE QUOTATION MARK": "\"", "RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK": "\"", "DASH": "-"} + for name_contains in exceptions: + if unid.find(name_contains)!=-1: + return exceptions[name_contains] + assert(unid.startswith("CYRILLIC"))# Not ready to romanize anything but cyrillics + transformation_pairs = {"CYRILLIC CAPITAL LETTER ": str.capitalize, "CYRILLIC SMALL LETTER ": str.lower} + func = str.lower + for name_contains in transformation_pairs: + if unid.find(name_contains)!=-1: + func = transformation_pairs[name_contains] + unid = unid.replace(name_contains, "") + cyrillic_exceptions = {"YERU": "y", "SHORT I": "y", "HARD SIGN": "\'", "SOFT SIGN": "\'", "BYELORUSSIAN-UKRAINIAN I": "i", "GHE WITH UPTURN": "g", "UKRAINIAN IE": "ie", "YU": "yu", "YA": "ya"} + for name_contains in cyrillic_exceptions: + if unid.find(name_contains)!=-1: + return cyrillic_exceptions[name_contains] + if all(map(is_syllable, unid)): + return func(unid) + else: + return func(filter(is_consonant, unid)) +def translit(text): + output = "" + for letter in text: + output += romanize(letter) + return output +#def main(): + #text = u"русск.: Любя, съешь щипцы, — вздохнёт мэр, — кайф жгуч." + #print translit(text) + #text = u"укр.: Гей, хлопці, не вспію - на ґанку ваша файна їжа знищується бурундучком." + #print translit(text) + #text = u"болг.: Ах, чудна българска земьо, полюшквай цъфтящи жита." + #print translit(text) + #text = u"серб.: Неуредне ноћне даме досађивале су Џеку К." + #print translit(text) + #russk.: Lyubya, s'iesh' shchiptsy, - vzdohniot mer, - kayf zhghuch. + #ukr.: Ghiey, hloptsi, nie vspiyu - na ganku vasha fayna yzha znishchuiet'sya burunduchkom. + #bolgh.: Ah, chudna b'lgharska ziem'o, polyushkvay ts'ftyashchi zhita. + #sierb.: Nieuriednie notshnie damie dosadjivalie su Dzhieku K. +if __name__=="__main__": + main() \ No newline at end of file diff --git a/fanficdownloader/writers/__init__.py b/fanficdownloader/writers/__init__.py new file mode 100644 index 0000000..7d9faf6 --- /dev/null +++ b/fanficdownloader/writers/__init__.py @@ -0,0 +1,38 @@ +# -*- 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. +# + +## This could (should?) use a dynamic loader like adapters, but for +## now, it's static, since there's so few of them. + +from ..exceptions import FailedToDownload + +from writer_html import HTMLWriter +from writer_txt import TextWriter +from writer_epub import EpubWriter +from writer_mobi import MobiWriter + +def getWriter(type,config,story): + if type == "html": + return HTMLWriter(config,story) + if type == "txt": + return TextWriter(config,story) + if type == "epub": + return EpubWriter(config,story) + if type == "mobi": + return MobiWriter(config,story) + + raise FailedToDownload("(%s) is not a supported download format."%type) diff --git a/fanficdownloader/writers/base_writer.py b/fanficdownloader/writers/base_writer.py new file mode 100644 index 0000000..c26d409 --- /dev/null +++ b/fanficdownloader/writers/base_writer.py @@ -0,0 +1,285 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import re +import os.path +import datetime +import string +import StringIO +import zipfile +from zipfile import ZipFile, ZIP_DEFLATED +import logging + +from ..configurable import Configurable +from ..htmlcleanup import removeEntities, removeAllEntities, stripHTML + +logger = logging.getLogger(__name__) + +class BaseStoryWriter(Configurable): + + @staticmethod + def getFormatName(): + return 'base' + + @staticmethod + def getFormatExt(): + return '.bse' + + def __init__(self, configuration, adapter): + Configurable.__init__(self, configuration) + + self.adapter = adapter + self.story = adapter.getStoryMetadataOnly() # only cache the metadata initially. + + # fall back labels. + self.titleLabels = { + 'category':'Category', + 'genre':'Genre', + 'language':'Language', + 'status':'Status', + 'series':'Series', + 'characters':'Characters', + 'ships':'Relationships', + 'datePublished':'Published', + 'dateUpdated':'Updated', + 'dateCreated':'Packaged', + 'rating':'Rating', + 'warnings':'Warnings', + 'numChapters':'Chapters', + 'numWords':'Words', + 'site':'Site', + 'storyId':'Story ID', + 'authorId':'Author ID', + 'extratags':'Extra Tags', + 'title':'Title', + 'storyUrl':'Story URL', + 'description':'Summary', + 'author':'Author', + 'authorUrl':'Author URL', + 'formatname':'File Format', + 'formatext':'File Extension', + 'siteabbrev':'Site Abbrev', + 'version':'FFDL Version' + } + self.story.setMetadata('formatname',self.getFormatName()) + self.story.setMetadata('formatext',self.getFormatExt()) + + def getMetadata(self,key, removeallentities=False): + return stripHTML(self.story.getMetadata(key, removeallentities)) + + def getOutputFileName(self): + if self.getConfig('zip_output'): + return self.getZipFileName() + else: + return self.getBaseFileName() + + def getBaseFileName(self): + return self.story.formatFileName(self.getConfig('output_filename'),self.getConfig('allow_unsafe_filename')) + + def getZipFileName(self): + return self.story.formatFileName(self.getConfig('zip_filename'),self.getConfig('allow_unsafe_filename')) + + def _write(self, out, text): + out.write(text.encode('utf8')) + + def writeTitlePage(self, out, START, ENTRY, END, WIDE_ENTRY=None, NO_TITLE_ENTRY=None): + """ + Write the title page, but only include entries that there's + metadata for. START, ENTRY and END are expected to already by + string.Template(). START and END are expected to use the same + names as Story.metadata, but ENTRY should use label and value. + """ + if self.getConfig("include_titlepage"): + + if self.hasConfig("titlepage_start"): + START = string.Template(self.getConfig("titlepage_start")) + + if self.hasConfig("titlepage_entry"): + ENTRY = string.Template(self.getConfig("titlepage_entry")) + + if self.hasConfig("titlepage_end"): + END = string.Template(self.getConfig("titlepage_end")) + + if self.hasConfig("titlepage_wide_entry"): + WIDE_ENTRY = string.Template(self.getConfig("titlepage_wide_entry")) + + if self.hasConfig("titlepage_no_title_entry"): + NO_TITLE_ENTRY = string.Template(self.getConfig("titlepage_no_title_entry")) + + self._write(out,START.substitute(self.story.getAllMetadata())) + + if WIDE_ENTRY==None: + WIDE_ENTRY=ENTRY + + titleEntriesList = self.getConfigList("titlepage_entries") + self.getConfigList("extra_titlepage_entries") + wideTitleEntriesList = self.getConfigList("wide_titlepage_entries") + + for entry in titleEntriesList: + if self.isValidMetaEntry(entry): + if self.story.getMetadata(entry): + if entry in wideTitleEntriesList: + TEMPLATE=WIDE_ENTRY + else: + TEMPLATE=ENTRY + + if self.hasConfig(entry+"_label"): + label=self.getConfig(entry+"_label") + elif entry in self.titleLabels: + logger.debug("Using fallback label for %s_label"%entry) + label=self.titleLabels[entry] + else: + label="%s"%entry.title() + logger.debug("No known label for %s, fallback to '%s'"%(entry,label)) + + # If the label for the title entry is empty, use the + # 'no title' option if there is one. + if label == "" and NO_TITLE_ENTRY: + TEMPLATE= NO_TITLE_ENTRY + + self._write(out,TEMPLATE.substitute({'label':label, + 'id':entry, + 'value':self.story.getMetadata(entry)})) + else: + self._write(out, entry) + + self._write(out,END.substitute(self.story.getAllMetadata())) + + def writeTOCPage(self, out, START, ENTRY, END): + """ + Write the Table of Contents page. START, ENTRY and END are expected to already by + string.Template(). START and END are expected to use the same + 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 self.hasConfig("tocpage_start"): + START = string.Template(self.getConfig("tocpage_start")) + + if self.hasConfig("tocpage_entry"): + ENTRY = string.Template(self.getConfig("tocpage_entry")) + + if self.hasConfig("tocpage_end"): + END = string.Template(self.getConfig("tocpage_end")) + + self._write(out,START.substitute(self.story.getAllMetadata())) + + for index, (url,title,html) in enumerate(self.story.getChapters(fortoc=True)): + if html: + self._write(out,ENTRY.substitute({'chapter':title, + 'number':index+1, + 'index':"%04d"%(index+1), + 'url':url})) + + self._write(out,END.substitute(self.story.getAllMetadata())) + + # if no outstream is given, write to file. + def writeStory(self,outstream=None, metaonly=False, outfilename=None, forceOverwrite=False): + + self.metaonly = metaonly + if outfilename == None: + outfilename=self.getOutputFileName() + + self.outfilename = outfilename + + # minor cheat, tucking css into metadata. + if self.getConfig("output_css"): + self.story.setMetadata("output_css", + self.getConfig("output_css"), + condremoveentities=False) + else: + self.story.setMetadata("output_css",'') + + if not outstream: + close=True + logger.info("Save directly to file: %s" % outfilename) + if self.getConfig('make_directories'): + path="" + outputdirs = os.path.dirname(outfilename).split('/') + for dir in outputdirs: + path+=dir+"/" + if not os.path.exists(path): + os.mkdir(path) ## os.makedirs() doesn't work in 2.5.2? + + ## Check for output file date vs updated date here + if not (self.getConfig('always_overwrite') or forceOverwrite): + if os.path.exists(outfilename): + ## date() truncs off time, which files have, but sites don't report. + lastupdated=self.story.getMetadataRaw('dateUpdated').date() + fileupdated=datetime.datetime.fromtimestamp(os.stat(outfilename)[8]).date() + if fileupdated > lastupdated: + logger.warn("File(%s) Updated(%s) more recently than Story(%s) - Skipping" % (outfilename,fileupdated,lastupdated)) + return + if not metaonly: + self.story = self.adapter.getStory() # get full story + # now, just + # before writing. + # Fetch before + # opening file. + outstream = open(outfilename,"wb") + else: + close=False + logger.debug("Save to stream") + + if not metaonly: + self.story = self.adapter.getStory() # get full story now, + # just before + # writing. Okay if + # double called with + # above, it will only + # fetch once. + if self.getConfig('zip_output'): + out = StringIO.StringIO() + self.zipout = ZipFile(outstream, 'w', compression=ZIP_DEFLATED) + self.writeStoryImpl(out) + self.zipout.writestr(self.getBaseFileName(),out.getvalue()) + # declares all the files created by Windows. otherwise, when + # it runs in appengine, windows unzips the files as 000 perms. + for zf in self.zipout.filelist: + zf.create_system = 0 + self.zipout.close() + out.close() + else: + self.writeStoryImpl(outstream) + + if close: + outstream.close() + + def writeFile(self, filename, data): + logger.debug("writeFile:%s"%filename) + + if self.getConfig('zip_output'): + outputdirs = os.path.dirname(self.getBaseFileName()) + if outputdirs: + filename=outputdirs+'/'+filename + self.zipout.writestr(filename,data) + else: + outputdirs = os.path.dirname(self.outfilename) + if outputdirs: + filename=outputdirs+'/'+filename + + dir = os.path.dirname(filename) + if not os.path.exists(dir): + os.mkdir(dir) ## os.makedirs() doesn't work in 2.5.2? + + outstream = open(filename,"wb") + outstream.write(data) + outstream.close() + + def writeStoryImpl(self, out): + "Must be overriden by sub classes." + pass + diff --git a/fanficdownloader/writers/writer_epub.py b/fanficdownloader/writers/writer_epub.py new file mode 100644 index 0000000..3b1b19d --- /dev/null +++ b/fanficdownloader/writers/writer_epub.py @@ -0,0 +1,690 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import logging +import string +import StringIO +import zipfile +from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED +import urllib +import re + +## XML isn't as forgiving as HTML, so rather than generate as strings, +## use DOM to generate the XML files. +from xml.dom.minidom import parse, parseString, getDOMImplementation + +from base_writer import * +from ..htmlcleanup import stripHTML + +logger = logging.getLogger(__name__) + +class EpubWriter(BaseStoryWriter): + + @staticmethod + def getFormatName(): + return 'epub' + + @staticmethod + def getFormatExt(): + return '.epub' + + def __init__(self, config, story): + BaseStoryWriter.__init__(self, config, story) + + self.EPUB_CSS = string.Template('''${output_css}''') + + self.EPUB_TITLE_PAGE_START = string.Template(''' + + + +${title} by ${author} + + + +

    ${title} by ${authorHTML}

    +
    +''') + + self.EPUB_TITLE_ENTRY = string.Template(''' +${label}: ${value}
    +''') + + self.EPUB_NO_TITLE_ENTRY = string.Template(''' +${value}
    +''') + + self.EPUB_TITLE_PAGE_END = string.Template(''' +
    + + + +''') + + self.EPUB_TABLE_TITLE_PAGE_START = string.Template(''' + + + +${title} by ${author} + + + +

    ${title} by ${authorHTML}

    + +''') + + self.EPUB_TABLE_TITLE_ENTRY = string.Template(''' + +''') + + self.EPUB_TABLE_TITLE_WIDE_ENTRY = string.Template(''' + +''') + + self.EPUB_TABLE_NO_TITLE_ENTRY = string.Template(''' + +''') + + self.EPUB_TABLE_TITLE_PAGE_END = string.Template(''' +
    ${label}:${value}
    ${label}: ${value}
    ${label}${value}
    + + + +''') + + self.EPUB_TOC_PAGE_START = string.Template(''' + + + +${title} by ${author} + + + +
    +

    Table of Contents

    +''') + + self.EPUB_TOC_ENTRY = string.Template(''' +${chapter}
    +''') + + self.EPUB_TOC_PAGE_END = string.Template(''' +
    + + +''') + + self.EPUB_CHAPTER_START = string.Template(''' + + + +${chapter} + + + +

    ${chapter}

    +''') + + self.EPUB_CHAPTER_END = string.Template(''' + + +''') + + self.EPUB_LOG_PAGE_START = string.Template(''' + + + +Update Log + + + +

    Update Log

    +''') + + self.EPUB_LOG_UPDATE_START = string.Template(''' +

    +''') + + self.EPUB_LOG_ENTRY = string.Template(''' +${label}: ${value} +''') + + self.EPUB_LOG_UPDATE_END = string.Template(''' +


    +''') + + self.EPUB_LOG_PAGE_END = string.Template(''' + + +''') + + self.EPUB_LOG_PAGE_END = string.Template(''' + + +''') + + self.EPUB_COVER = string.Template(''' +Cover
    +cover +
    +''') + + def writeLogPage(self, out): + """ + Write the log page, but only include entries that there's + metadata for. START, ENTRY and END are expected to already be + string.Template(). START and END are expected to use the same + names as Story.metadata, but ENTRY should use id, label and value. + """ + if self.hasConfig("logpage_start"): + START = string.Template(self.getConfig("logpage_start")) + else: + START = self.EPUB_LOG_PAGE_START + + if self.hasConfig("logpage_end"): + END = string.Template(self.getConfig("logpage_end")) + else: + END = self.EPUB_LOG_PAGE_END + + # if there's a self.story.logfile, there's an existing log + # to add to. + if self.story.logfile: + logger.debug("existing logfile found, appending") + logger.debug("existing data:%s"%self._getLastLogData(self.story.logfile)) + replace_string = "" # "" + self._write(out,self.story.logfile.replace(replace_string,self._makeLogEntry(self._getLastLogData(self.story.logfile))+replace_string)) + else: + # otherwise, write a new one. + self._write(out,START.substitute(self.story.getAllMetadata())) + self._write(out,self._makeLogEntry()) + self._write(out,END.substitute(self.story.getAllMetadata())) + + # self parsing instead of Soup because it should be simple and not + # worth the overhead. + def _getLastLogData(self,logfile): + """ + Make a dict() of the most recent(last) log entry for each piece of metadata. + Switch rindex to index to search from top instead of bottom. + """ + values = {} + for entry in self.getConfigList("logpage_entries") + self.getConfigList("extra_logpage_entries"): + try: + # 1975-04-15 + span = ''%entry + idx = logfile.rindex(span)+len(span) + values[entry] = logfile[idx:logfile.index('',idx)] + except Exception, e: + #print("e:%s"%e) + pass + + return values + + def _makeLogEntry(self, oldvalues={}): + if self.hasConfig("logpage_update_start"): + START = string.Template(self.getConfig("logpage_update_start")) + else: + START = self.EPUB_LOG_UPDATE_START + + if self.hasConfig("logpage_entry"): + ENTRY = string.Template(self.getConfig("logpage_entry")) + else: + ENTRY = self.EPUB_LOG_ENTRY + + if self.hasConfig("logpage_update_end"): + END = string.Template(self.getConfig("logpage_update_end")) + else: + END = self.EPUB_LOG_UPDATE_END + + retval = START.substitute(self.story.getAllMetadata()) + + for entry in self.getConfigList("logpage_entries") + self.getConfigList("extra_logpage_entries"): + if self.isValidMetaEntry(entry): + val = self.story.getMetadata(entry) + if val and ( entry not in oldvalues or val != oldvalues[entry] ): + if self.hasConfig(entry+"_label"): + label=self.getConfig(entry+"_label") + elif entry in self.titleLabels: + logger.debug("Using fallback label for %s_label"%entry) + label=self.titleLabels[entry] + else: + label="%s"%entry.title() + logger.debug("No known label for %s, fallback to '%s'"%(entry,label)) + + retval = retval + ENTRY.substitute({'id':entry, + 'label':label, + 'value':val}) + else: + # could be useful for introducing extra text, but + # mostly it makes it easy to tell when you get the + # keyword wrong. + retval = retval + entry + + retval = retval + END.substitute(self.story.getAllMetadata()) + + if self.getConfig('replace_hr'): + retval = retval.replace("
    ","
    * * *
    ") + + return retval + + def writeStoryImpl(self, out): + + ## Python 2.5 ZipFile is rather more primative than later + ## versions. It can operate on a file, or on a StringIO, but + ## not on an open stream. OTOH, I suspect we would have had + ## problems with closing and opening again to change the + ## compression type anyway. + zipio = StringIO.StringIO() + + ## mimetype must be first file and uncompressed. Python 2.5 + ## ZipFile can't change compression type file-by-file, so we + ## have to close and re-open + outputepub = ZipFile(zipio, 'w', compression=ZIP_STORED) + outputepub.debug=3 + outputepub.writestr('mimetype','application/epub+zip') + outputepub.close() + + ## Re-open file for content. + outputepub = ZipFile(zipio, 'a', compression=ZIP_DEFLATED) + outputepub.debug=3 + + ## Create META-INF/container.xml file. The only thing it does is + ## point to content.opf + containerdom = getDOMImplementation().createDocument(None, "container", None) + containertop = containerdom.documentElement + containertop.setAttribute("version","1.0") + containertop.setAttribute("xmlns","urn:oasis:names:tc:opendocument:xmlns:container") + rootfiles = containerdom.createElement("rootfiles") + containertop.appendChild(rootfiles) + rootfiles.appendChild(newTag(containerdom,"rootfile",{"full-path":"content.opf", + "media-type":"application/oebps-package+xml"})) + outputepub.writestr("META-INF/container.xml",containerdom.toxml(encoding='utf-8')) + containerdom.unlink() + del containerdom + + ## Epub has two metadata files with real data. We're putting + ## them in content.opf (pointed to by META-INF/container.xml) + ## and toc.ncx (pointed to by content.opf) + + ## content.opf contains metadata, a 'manifest' list of all + ## other included files, and another 'spine' list of the items in the + ## file + + uniqueid= 'fanficdownloader-uid:%s-u%s-s%s' % ( + self.getMetadata('site'), + self.story.getList('authorId')[0], + self.getMetadata('storyId')) + + contentdom = getDOMImplementation().createDocument(None, "package", None) + package = contentdom.documentElement + package.setAttribute("version","2.0") + package.setAttribute("xmlns","http://www.idpf.org/2007/opf") + package.setAttribute("unique-identifier","fanficdownloader-uid") + metadata=newTag(contentdom,"metadata", + attrs={"xmlns:dc":"http://purl.org/dc/elements/1.1/", + "xmlns:opf":"http://www.idpf.org/2007/opf"}) + package.appendChild(metadata) + + metadata.appendChild(newTag(contentdom,"dc:identifier", + text=uniqueid, + attrs={"id":"fanficdownloader-uid"})) + + if self.getMetadata('title'): + metadata.appendChild(newTag(contentdom,"dc:title",text=self.getMetadata('title'))) + + if self.getMetadata('author'): + if self.story.isList('author'): + for auth in self.story.getList('author'): + metadata.appendChild(newTag(contentdom,"dc:creator", + attrs={"opf:role":"aut"}, + text=auth)) + else: + metadata.appendChild(newTag(contentdom,"dc:creator", + attrs={"opf:role":"aut"}, + text=self.getMetadata('author'))) + + metadata.appendChild(newTag(contentdom,"dc:contributor",text="fanficdownloader [http://fanficdownloader.googlecode.com]",attrs={"opf:role":"bkp"})) + metadata.appendChild(newTag(contentdom,"dc:rights",text="")) + if self.story.getMetadata('langcode'): + metadata.appendChild(newTag(contentdom,"dc:language",text=self.story.getMetadata('langcode'))) + else: + metadata.appendChild(newTag(contentdom,"dc:language",text='en')) + + # published, created, updated, calibre + # Leave calling self.story.getMetadataRaw directly in case date format changes. + if self.story.getMetadataRaw('datePublished'): + metadata.appendChild(newTag(contentdom,"dc:date", + attrs={"opf:event":"publication"}, + text=self.story.getMetadataRaw('datePublished').strftime("%Y-%m-%d"))) + + if self.story.getMetadataRaw('dateCreated'): + metadata.appendChild(newTag(contentdom,"dc:date", + attrs={"opf:event":"creation"}, + text=self.story.getMetadataRaw('dateCreated').strftime("%Y-%m-%d"))) + + if self.story.getMetadataRaw('dateUpdated'): + metadata.appendChild(newTag(contentdom,"dc:date", + attrs={"opf:event":"modification"}, + text=self.story.getMetadataRaw('dateUpdated').strftime("%Y-%m-%d"))) + metadata.appendChild(newTag(contentdom,"meta", + attrs={"name":"calibre:timestamp", + "content":self.story.getMetadataRaw('dateUpdated').strftime("%Y-%m-%dT%H:%M:%S")})) + + if self.getMetadata('description'): + metadata.appendChild(newTag(contentdom,"dc:description",text= + self.getMetadata('description'))) + + for subject in self.story.getSubjectTags(): + metadata.appendChild(newTag(contentdom,"dc:subject",text=subject)) + + + if self.getMetadata('site'): + metadata.appendChild(newTag(contentdom,"dc:publisher", + text=self.getMetadata('site'))) + + if self.getMetadata('storyUrl'): + metadata.appendChild(newTag(contentdom,"dc:identifier", + attrs={"opf:scheme":"URL"}, + text=self.getMetadata('storyUrl'))) + metadata.appendChild(newTag(contentdom,"dc:source", + text=self.getMetadata('storyUrl'))) + + ## end of metadata, create manifest. + items = [] # list of (id, href, type, title) tuples(all strings) + itemrefs = [] # list of strings -- idrefs from .opfs' spines + items.append(("ncx","toc.ncx","application/x-dtbncx+xml",None)) ## we'll generate the toc.ncx file, + ## but it needs to be in the items manifest. + + guide = None + coverIO = None + + coverimgid = "image0000" + if not self.story.cover and self.story.oldcover: + logger.debug("writer_epub: no new cover, has old cover, write image.") + (oldcoverhtmlhref, + oldcoverhtmltype, + oldcoverhtmldata, + oldcoverimghref, + oldcoverimgtype, + oldcoverimgdata) = self.story.oldcover + outputepub.writestr(oldcoverhtmlhref,oldcoverhtmldata) + outputepub.writestr(oldcoverimghref,oldcoverimgdata) + + coverimgid = "image0" + items.append((coverimgid, + oldcoverimghref, + oldcoverimgtype, + None)) + items.append(("cover",oldcoverhtmlhref,oldcoverhtmltype,None)) + itemrefs.append("cover") + metadata.appendChild(newTag(contentdom,"meta",{"content":"image0", + "name":"cover"})) + guide = newTag(contentdom,"guide") + guide.appendChild(newTag(contentdom,"reference",attrs={"type":"cover", + "title":"Cover", + "href":oldcoverhtmlhref})) + + + + if self.getConfig('include_images'): + imgcount=0 + for imgmap in self.story.getImgUrls(): + imgfile = "OEBPS/"+imgmap['newsrc'] + outputepub.writestr(imgfile,imgmap['data']) + items.append(("image%04d"%imgcount, + imgfile, + imgmap['mime'], + None)) + imgcount+=1 + if 'cover' in imgfile: + # make sure coverimgid is set to the cover, not + # just the first image. + coverimgid = items[-1][0] + + + items.append(("style","OEBPS/stylesheet.css","text/css",None)) + + if self.story.cover: + # Note that the id of the cover xhmtl *must* be 'cover' + # for it to work on Nook. + items.append(("cover","OEBPS/cover.xhtml","application/xhtml+xml",None)) + itemrefs.append("cover") + # + # + metadata.appendChild(newTag(contentdom,"meta",{"content":coverimgid, + "name":"cover"})) + # cover stuff for later: + # at end of : + # + # + # + guide = newTag(contentdom,"guide") + guide.appendChild(newTag(contentdom,"reference",attrs={"type":"cover", + "title":"Cover", + "href":"OEBPS/cover.xhtml"})) + + if self.hasConfig("cover_content"): + COVER = string.Template(self.getConfig("cover_content")) + else: + COVER = self.EPUB_COVER + coverIO = StringIO.StringIO() + coverIO.write(COVER.substitute(dict(self.story.getAllMetadata().items()+{'coverimg':self.story.cover}.items()))) + + 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 : + items.append(("toc_page","OEBPS/toc_page.xhtml","application/xhtml+xml","Table of Contents")) + itemrefs.append("toc_page") + + dologpage = ( self.getConfig("include_logpage") == "smart" and \ + (self.story.logfile or self.story.getMetadataRaw("status") == "In-Progress") ) \ + or self.getConfig("include_logpage") == "true" + + if dologpage: + items.append(("log_page","OEBPS/log_page.xhtml","application/xhtml+xml","Update Log")) + itemrefs.append("log_page") + + for index, (url,title,html) in enumerate(self.story.getChapters(fortoc=True)): + if html: + i=index+1 + items.append(("file%04d"%i, + "OEBPS/file%04d.xhtml"%i, + "application/xhtml+xml", + title)) + itemrefs.append("file%04d"%i) + + manifest = contentdom.createElement("manifest") + package.appendChild(manifest) + for item in items: + (id,href,type,title)=item + manifest.appendChild(newTag(contentdom,"item", + attrs={'id':id, + 'href':href, + 'media-type':type})) + + spine = newTag(contentdom,"spine",attrs={"toc":"ncx"}) + package.appendChild(spine) + for itemref in itemrefs: + spine.appendChild(newTag(contentdom,"itemref", + attrs={"idref":itemref, + "linear":"yes"})) + # guide only exists if there's a cover. + if guide: + package.appendChild(guide) + + # write content.opf to zip. + contentxml = contentdom.toxml(encoding='utf-8') + + # tweak for brain damaged Nook STR. Nook insists on name before content. + contentxml = contentxml.replace(''%coverimgid, + ''%coverimgid) + outputepub.writestr("content.opf",contentxml) + + contentdom.unlink() + del contentdom + + ## create toc.ncx file + tocncxdom = getDOMImplementation().createDocument(None, "ncx", None) + ncx = tocncxdom.documentElement + ncx.setAttribute("version","2005-1") + ncx.setAttribute("xmlns","http://www.daisy.org/z3986/2005/ncx/") + head = tocncxdom.createElement("head") + ncx.appendChild(head) + head.appendChild(newTag(tocncxdom,"meta", + attrs={"name":"dtb:uid", "content":uniqueid})) + head.appendChild(newTag(tocncxdom,"meta", + attrs={"name":"dtb:depth", "content":"1"})) + head.appendChild(newTag(tocncxdom,"meta", + attrs={"name":"dtb:totalPageCount", "content":"0"})) + head.appendChild(newTag(tocncxdom,"meta", + attrs={"name":"dtb:maxPageNumber", "content":"0"})) + + docTitle = tocncxdom.createElement("docTitle") + docTitle.appendChild(newTag(tocncxdom,"text",text=self.getMetadata('title'))) + ncx.appendChild(docTitle) + + tocnavMap = tocncxdom.createElement("navMap") + ncx.appendChild(tocnavMap) + + # + # + # + # + # + # + index=0 + for item in items: + (id,href,type,title)=item + # only items to be skipped, cover.xhtml, images, toc.ncx, stylesheet.css, should have no title. + if title : + navPoint = newTag(tocncxdom,"navPoint", + attrs={'id':id, + 'playOrder':str(index)}) + tocnavMap.appendChild(navPoint) + navLabel = newTag(tocncxdom,"navLabel") + navPoint.appendChild(navLabel) + ## the xml library will re-escape as needed. + navLabel.appendChild(newTag(tocncxdom,"text",text=stripHTML(title))) + navPoint.appendChild(newTag(tocncxdom,"content",attrs={"src":href})) + index=index+1 + + # write toc.ncx to zip file + outputepub.writestr("toc.ncx",tocncxdom.toxml(encoding='utf-8')) + tocncxdom.unlink() + del tocncxdom + + # write stylesheet.css file. + outputepub.writestr("OEBPS/stylesheet.css",self.EPUB_CSS.substitute(self.story.getAllMetadata())) + + # write title page. + if self.getConfig("titlepage_use_table"): + TITLE_PAGE_START = self.EPUB_TABLE_TITLE_PAGE_START + TITLE_ENTRY = self.EPUB_TABLE_TITLE_ENTRY + WIDE_TITLE_ENTRY = self.EPUB_TABLE_TITLE_WIDE_ENTRY + NO_TITLE_ENTRY = self.EPUB_TABLE_NO_TITLE_ENTRY + TITLE_PAGE_END = self.EPUB_TABLE_TITLE_PAGE_END + else: + TITLE_PAGE_START = self.EPUB_TITLE_PAGE_START + TITLE_ENTRY = self.EPUB_TITLE_ENTRY + WIDE_TITLE_ENTRY = self.EPUB_TITLE_ENTRY # same, only wide in tables. + NO_TITLE_ENTRY = self.EPUB_NO_TITLE_ENTRY + TITLE_PAGE_END = self.EPUB_TITLE_PAGE_END + + if coverIO: + outputepub.writestr("OEBPS/cover.xhtml",coverIO.getvalue()) + coverIO.close() + + titlepageIO = StringIO.StringIO() + self.writeTitlePage(out=titlepageIO, + START=TITLE_PAGE_START, + ENTRY=TITLE_ENTRY, + WIDE_ENTRY=WIDE_TITLE_ENTRY, + END=TITLE_PAGE_END, + NO_TITLE_ENTRY=NO_TITLE_ENTRY) + if titlepageIO.getvalue(): # will be false if no title page. + outputepub.writestr("OEBPS/title_page.xhtml",titlepageIO.getvalue()) + titlepageIO.close() + + # write toc page. + tocpageIO = StringIO.StringIO() + self.writeTOCPage(tocpageIO, + self.EPUB_TOC_PAGE_START, + self.EPUB_TOC_ENTRY, + self.EPUB_TOC_PAGE_END) + if tocpageIO.getvalue(): # will be false if no toc page. + outputepub.writestr("OEBPS/toc_page.xhtml",tocpageIO.getvalue()) + tocpageIO.close() + + if dologpage: + # write log page. + logpageIO = StringIO.StringIO() + self.writeLogPage(logpageIO) + outputepub.writestr("OEBPS/log_page.xhtml",logpageIO.getvalue()) + logpageIO.close() + + if self.hasConfig('chapter_start'): + CHAPTER_START = string.Template(self.getConfig("chapter_start")) + else: + CHAPTER_START = self.EPUB_CHAPTER_START + + if self.hasConfig('chapter_end'): + CHAPTER_END = string.Template(self.getConfig("chapter_end")) + else: + CHAPTER_END = self.EPUB_CHAPTER_END + + for index, (url,title,html) in enumerate(self.story.getChapters()): + if html: + logger.debug('Writing chapter text for: %s' % title) + vals={'url':url, 'chapter':title, 'index':"%04d"%(index+1), 'number':index+1} + fullhtml = CHAPTER_START.substitute(vals) + html + CHAPTER_END.substitute(vals) + # ffnet(& maybe others) gives the whole chapter text + # as one line. This causes problems for nook(at + # least) when the chapter size starts getting big + # (200k+) + #fullhtml = fullhtml.replace('

    ','

    \n').replace('
    ','
    \n') + # The replaces above added tons of extra newlines + # during *each* epub update. The regexp version adds + # only one and removes any extra. + fullhtml = re.sub(r'(

    |
    )\n*',r'\1\n',fullhtml) + + outputepub.writestr("OEBPS/file%04d.xhtml"%(index+1),fullhtml.encode('utf-8')) + del fullhtml + + if self.story.calibrebookmark: + outputepub.writestr("META-INF/calibre_bookmarks.txt",self.story.calibrebookmark) + + # declares all the files created by Windows. otherwise, when + # it runs in appengine, windows unzips the files as 000 perms. + for zf in outputepub.filelist: + zf.create_system = 0 + outputepub.close() + out.write(zipio.getvalue()) + zipio.close() + +## Utility method for creating new tags. +def newTag(dom,name,attrs=None,text=None): + tag = dom.createElement(name) + if( attrs is not None ): + for attr in attrs.keys(): + tag.setAttribute(attr,attrs[attr]) + if( text is not None ): + tag.appendChild(dom.createTextNode(text)) + return tag + diff --git a/fanficdownloader/writers/writer_html.py b/fanficdownloader/writers/writer_html.py new file mode 100644 index 0000000..41b0175 --- /dev/null +++ b/fanficdownloader/writers/writer_html.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import logging +import string + +from base_writer import * + +class HTMLWriter(BaseStoryWriter): + + @staticmethod + def getFormatName(): + return 'html' + + @staticmethod + def getFormatExt(): + return '.html' + + def __init__(self, config, story): + BaseStoryWriter.__init__(self, config, story) + + self.HTML_FILE_START = string.Template(''' + + + +${title} by ${author} + + + +

    ${title} by ${authorHTML}

    +''') + + self.HTML_COVER = string.Template(''' +cover +''') + + self.HTML_TITLE_PAGE_START = string.Template(''' + +''') + + self.HTML_TITLE_ENTRY = string.Template(''' + +''') + + self.HTML_TITLE_PAGE_END = string.Template(''' +
    ${label}:${value}
    +''') + + self.HTML_TOC_PAGE_START = string.Template(''' +

    Table of Contents

    +

    +''') + + self.HTML_TOC_ENTRY = string.Template(''' +${chapter}
    +''') + + self.HTML_TOC_PAGE_END = string.Template(''' +

    +''') + + self.HTML_CHAPTER_START = string.Template(''' +

    ${chapter}

    +''') + + self.HTML_CHAPTER_END = string.Template('') + + self.HTML_FILE_END = string.Template(''' + +''') + + + def writeStoryImpl(self, out): + + if self.hasConfig("cover_content"): + COVER = string.Template(self.getConfig("cover_content")) + else: + COVER = self.HTML_COVER + + if self.hasConfig('file_start'): + FILE_START = string.Template(self.getConfig("file_start")) + else: + FILE_START = self.HTML_FILE_START + + if self.hasConfig('file_end'): + FILE_END = string.Template(self.getConfig("file_end")) + else: + FILE_END = self.HTML_FILE_END + + self._write(out,FILE_START.substitute(self.story.getAllMetadata())) + + if self.getConfig('include_images') and self.story.cover: + self._write(out,COVER.substitute(dict(self.story.getAllMetadata().items()+{'coverimg':self.story.cover}.items()))) + + self.writeTitlePage(out, + self.HTML_TITLE_PAGE_START, + self.HTML_TITLE_ENTRY, + self.HTML_TITLE_PAGE_END) + + self.writeTOCPage(out, + self.HTML_TOC_PAGE_START, + self.HTML_TOC_ENTRY, + self.HTML_TOC_PAGE_END) + + if self.hasConfig('chapter_start'): + CHAPTER_START = string.Template(self.getConfig("chapter_start")) + else: + CHAPTER_START = self.HTML_CHAPTER_START + + if self.hasConfig('chapter_end'): + CHAPTER_END = string.Template(self.getConfig("chapter_end")) + else: + CHAPTER_END = self.HTML_CHAPTER_END + + for index, (url,title,html) in enumerate(self.story.getChapters()): + if html: + logging.debug('Writing chapter text for: %s' % title) + vals={'url':url, 'chapter':title, 'index':"%04d"%(index+1), 'number':index+1} + self._write(out,CHAPTER_START.substitute(vals)) + self._write(out,html) + self._write(out,CHAPTER_END.substitute(vals)) + + self._write(out,FILE_END.substitute(self.story.getAllMetadata())) + + if self.getConfig('include_images'): + for imgmap in self.story.getImgUrls(): + self.writeFile(imgmap['newsrc'],imgmap['data']) + diff --git a/fanficdownloader/writers/writer_mobi.py b/fanficdownloader/writers/writer_mobi.py new file mode 100644 index 0000000..4396ab0 --- /dev/null +++ b/fanficdownloader/writers/writer_mobi.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import logging +import string +import StringIO + +from base_writer import * +from ..htmlcleanup import stripHTML +from ..mobi import Converter +from ..exceptions import FailedToWriteOutput + +logger = logging.getLogger(__name__) + +class MobiWriter(BaseStoryWriter): + + @staticmethod + def getFormatName(): + return 'mobi' + + @staticmethod + def getFormatExt(): + return '.mobi' + + def __init__(self, config, story): + BaseStoryWriter.__init__(self, config, story) + + self.MOBI_TITLE_PAGE_START = string.Template(''' + + + +${title} by ${author} + + +

    ${title} by ${authorHTML}

    +
    +''') + + self.MOBI_TITLE_ENTRY = string.Template(''' +${label}: ${value}
    +''') + + self.MOBI_NO_TITLE_ENTRY = string.Template(''' +${value}
    +''') + + self.MOBI_TITLE_PAGE_END = string.Template(''' +
    + + + +''') + + self.MOBI_TABLE_TITLE_PAGE_START = string.Template(''' + + + +${title} by ${author} + + +

    ${title} by ${authorHTML}

    + +''') + + self.MOBI_TABLE_TITLE_ENTRY = string.Template(''' + +''') + + self.MOBI_TABLE_TITLE_WIDE_ENTRY = string.Template(''' + +''') + + self.MOBI_TABLE_NO_TITLE_WIDE_ENTRY = string.Template(''' + +''') + + self.MOBI_TABLE_TITLE_PAGE_END = string.Template(''' +
    ${label}:${value}
    ${label}: ${value}
    ${value}
    + + + +''') + + self.MOBI_CHAPTER_START = string.Template(''' + + + +${chapter} + + +

    ${chapter}

    +''') + + self.MOBI_CHAPTER_END = string.Template(''' + + +''') + + def writeStoryImpl(self, out): + + files = [] + + # write title page. + if self.getConfig("titlepage_use_table"): + TITLE_PAGE_START = self.MOBI_TABLE_TITLE_PAGE_START + TITLE_ENTRY = self.MOBI_TABLE_TITLE_ENTRY + WIDE_TITLE_ENTRY = self.MOBI_TABLE_TITLE_WIDE_ENTRY + NO_TITLE_ENTRY = self.MOBI_TABLE_NO_TITLE_ENTRY + TITLE_PAGE_END = self.MOBI_TABLE_TITLE_PAGE_END + else: + TITLE_PAGE_START = self.MOBI_TITLE_PAGE_START + TITLE_ENTRY = self.MOBI_TITLE_ENTRY + WIDE_TITLE_ENTRY = self.MOBI_TITLE_ENTRY # same, only wide in tables. + NO_TITLE_ENTRY = self.MOBI_NO_TITLE_ENTRY + TITLE_PAGE_END = self.MOBI_TITLE_PAGE_END + + titlepageIO = StringIO.StringIO() + self.writeTitlePage(out=titlepageIO, + START=TITLE_PAGE_START, + ENTRY=TITLE_ENTRY, + WIDE_ENTRY=WIDE_TITLE_ENTRY, + END=TITLE_PAGE_END, + NO_TITLE_ENTRY=NO_TITLE_ENTRY) + if titlepageIO.getvalue(): # will be false if no title page. + files.append(titlepageIO.getvalue()) + titlepageIO.close() + + ## MOBI always has a TOC injected by mobi.py because there's + ## no meta-data TOC. + # # write toc page. + # tocpageIO = StringIO.StringIO() + # self.writeTOCPage(tocpageIO, + # self.MOBI_TOC_PAGE_START, + # self.MOBI_TOC_ENTRY, + # self.MOBI_TOC_PAGE_END) + # if tocpageIO.getvalue(): # will be false if no toc page. + # files.append(tocpageIO.getvalue()) + # tocpageIO.close() + + if self.hasConfig('chapter_start'): + CHAPTER_START = string.Template(self.getConfig("chapter_start")) + else: + CHAPTER_START = self.MOBI_CHAPTER_START + + if self.hasConfig('chapter_end'): + CHAPTER_END = string.Template(self.getConfig("chapter_end")) + else: + CHAPTER_END = self.MOBI_CHAPTER_END + + for index, (url,title,html) in enumerate(self.story.getChapters()): + if html: + logger.debug('Writing chapter text for: %s' % title) + vals={'url':url, 'chapter':title, 'index':"%04d"%(index+1), 'number':index+1} + fullhtml = CHAPTER_START.substitute(vals) + html + CHAPTER_END.substitute(vals) + # ffnet(& maybe others) gives the whole chapter text + # as one line. This causes problems for nook(at + # least) when the chapter size starts getting big + # (200k+) + fullhtml = fullhtml.replace('

    ','

    \n').replace('
    ','
    \n') + files.append(fullhtml.encode('utf-8')) + del fullhtml + + c = Converter(title=self.getMetadata('title'), + author=self.getMetadata('author'), + publisher=self.getMetadata('site')) + mobidata = c.ConvertStrings(files) + if len(mobidata) < 1: + raise FailedToWriteOutput("Zero length mobi output") + out.write(mobidata) + + del files + del mobidata + +## Utility method for creating new tags. +def newTag(dom,name,attrs=None,text=None): + tag = dom.createElement(name) + if( attrs is not None ): + for attr in attrs.keys(): + tag.setAttribute(attr,attrs[attr]) + if( text is not None ): + tag.appendChild(dom.createTextNode(text)) + return tag + diff --git a/fanficdownloader/writers/writer_txt.py b/fanficdownloader/writers/writer_txt.py new file mode 100644 index 0000000..8705a41 --- /dev/null +++ b/fanficdownloader/writers/writer_txt.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- + +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import logging +import string +from textwrap import wrap + +from base_writer import * + +from ..html2text import html2text + +## In BaseStoryWriter, we define _write to encode objects +## back into for true output. But txt needs to write the +## title page and TOC to a buffer first to wordwrap. And StringIO +## gets pissy about unicode bytes in its buflist. This decodes the +## unicode containing object passed in back to a +## object so they join up properly. Could override _write to not +## encode and do out.write(whatever.encode('utf8') instead. Honestly +## not sure which is uglier. +class KludgeStringIO(): + def __init__(self, buf = ''): + self.buflist=[] + def write(self,s): + try: + s=s.decode('utf-8') + except: + pass + self.buflist.append(s) + def getvalue(self): + return u''.join(self.buflist) + def close(self): + pass + +class TextWriter(BaseStoryWriter): + + @staticmethod + def getFormatName(): + return 'txt' + + @staticmethod + def getFormatExt(): + return '.txt' + + def __init__(self, config, story): + + BaseStoryWriter.__init__(self, config, story) + + self.TEXT_FILE_START = string.Template(u''' + + +${title} + +by ${author} + + +''') + + self.TEXT_TITLE_PAGE_START = string.Template(u''' +''') + + self.TEXT_TITLE_ENTRY = string.Template(u'''${label}: ${value} +''') + + self.TEXT_TITLE_PAGE_END = string.Template(u''' + + +''') + + self.TEXT_TOC_PAGE_START = string.Template(u''' + +TABLE OF CONTENTS + +''') + + self.TEXT_TOC_ENTRY = string.Template(u''' +${chapter} +''') + + self.TEXT_TOC_PAGE_END = string.Template(u''' +''') + + self.TEXT_CHAPTER_START = string.Template(u''' + +\t${chapter} + +''') + self.TEXT_CHAPTER_END = string.Template(u'') + + self.TEXT_FILE_END = string.Template(u''' + +End file. +''') + + def writeStoryImpl(self, out): + + self.wrap_width = self.getConfig('wrap_width') + if self.wrap_width == '' or self.wrap_width == '0': + self.wrap_width = None + else: + self.wrap_width = int(self.wrap_width) + + wrapout = KludgeStringIO() + + if self.hasConfig("file_start"): + FILE_START = string.Template(self.getConfig("file_start")) + else: + FILE_START = self.TEXT_FILE_START + + if self.hasConfig("file_end"): + FILE_END = string.Template(self.getConfig("file_end")) + else: + FILE_END = self.TEXT_FILE_END + + wrapout.write(FILE_START.substitute(self.story.getAllMetadata())) + + self.writeTitlePage(wrapout, + self.TEXT_TITLE_PAGE_START, + self.TEXT_TITLE_ENTRY, + self.TEXT_TITLE_PAGE_END) + towrap = wrapout.getvalue() + + self.writeTOCPage(wrapout, + self.TEXT_TOC_PAGE_START, + self.TEXT_TOC_ENTRY, + self.TEXT_TOC_PAGE_END) + + towrap = wrapout.getvalue() + wrapout.close() + towrap = removeAllEntities(towrap) + + self._write(out,self.lineends(self.wraplines(towrap))) + + if self.hasConfig('chapter_start'): + CHAPTER_START = string.Template(self.getConfig("chapter_start")) + else: + CHAPTER_START = self.TEXT_CHAPTER_START + + if self.hasConfig('chapter_end'): + CHAPTER_END = string.Template(self.getConfig("chapter_end")) + else: + CHAPTER_END = self.TEXT_CHAPTER_END + + for index, (url, title,html) in enumerate(self.story.getChapters()): + if html: + logging.debug('Writing chapter text for: %s' % title) + vals={'url':url, 'chapter':title, 'index':"%04d"%(index+1), 'number':index+1} + self._write(out,self.lineends(self.wraplines(removeAllEntities(CHAPTER_START.substitute(vals))))) + self._write(out,self.lineends(html2text(html,wrap_width=self.wrap_width))) + self._write(out,self.lineends(self.wraplines(removeAllEntities(CHAPTER_END.substitute(vals))))) + + self._write(out,self.lineends(self.wraplines(FILE_END.substitute(self.story.getAllMetadata())))) + + def wraplines(self, text): + + if not self.wrap_width: + return text + + result='' + for para in text.split("\n"): + first=True + for line in wrap(para, self.wrap_width): + if first: + first=False + else: + result += u"\n" + result += line + result += u"\n" + return result + + ## The appengine will return unix line endings. + def lineends(self, txt): + txt = txt.replace('\r','') + if self.getConfig("windows_eol"): + txt = txt.replace('\n',u'\r\n') + return txt + diff --git a/ffstorage.py b/ffstorage.py new file mode 100644 index 0000000..bad9b4a --- /dev/null +++ b/ffstorage.py @@ -0,0 +1,63 @@ +# Copyright 2011 Fanficdownloader team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import pickle, copy +from google.appengine.ext import db + +class ObjectProperty(db.Property): + data_type = db.Blob + + def get_value_for_datastore(self, model_instance): + value = self.__get__(model_instance, model_instance.__class__) + pickled_val = pickle.dumps(value,protocol=pickle.HIGHEST_PROTOCOL) + if value is not None: return db.Blob(pickled_val) + + def make_value_from_datastore(self, value): + if value is not None: return pickle.loads(value) + + def default_value(self): + return copy.copy(self.default) + +class DownloadMeta(db.Model): + user = db.UserProperty() + url = db.StringProperty() + name = db.StringProperty() + title = db.StringProperty() + author = db.StringProperty() + format = db.StringProperty() + failure = db.TextProperty() + completed = db.BooleanProperty(default=False) + date = db.DateTimeProperty(auto_now_add=True) + version = db.StringProperty() + # data_chunks is implicit from DownloadData def. + +class DownloadData(db.Model): + download = db.ReferenceProperty(DownloadMeta, + collection_name='data_chunks') + blob = db.BlobProperty() + index = db.IntegerProperty() + +class UserConfig(db.Model): + user = db.UserProperty() + config = db.BlobProperty() + +class SavedMeta(db.Model): + url = db.StringProperty() + title = db.StringProperty() + author = db.StringProperty() + date = db.DateTimeProperty(auto_now_add=True) + count = db.IntegerProperty() + meta = ObjectProperty() + diff --git a/html5lib/LICENSE b/html5lib/LICENSE new file mode 100644 index 0000000..c87fa7a --- /dev/null +++ b/html5lib/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2006-2013 James Graham and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/html5lib/__init__.py b/html5lib/__init__.py new file mode 100644 index 0000000..19a4b7d --- /dev/null +++ b/html5lib/__init__.py @@ -0,0 +1,23 @@ +""" +HTML parsing library based on the WHATWG "HTML5" +specification. The parser is designed to be compatible with existing +HTML found in the wild and implements well-defined error recovery that +is largely compatible with modern desktop web browsers. + +Example usage: + +import html5lib +f = open("my_document.html") +tree = html5lib.parse(f) +""" + +from __future__ import absolute_import, division, unicode_literals + +from .html5parser import HTMLParser, parse, parseFragment +from .treebuilders import getTreeBuilder +from .treewalkers import getTreeWalker +from .serializer import serialize + +__all__ = ["HTMLParser", "parse", "parseFragment", "getTreeBuilder", + "getTreeWalker", "serialize"] +__version__ = "0.999" diff --git a/html5lib/constants.py b/html5lib/constants.py new file mode 100644 index 0000000..e708984 --- /dev/null +++ b/html5lib/constants.py @@ -0,0 +1,3104 @@ +from __future__ import absolute_import, division, unicode_literals + +import string +import gettext +_ = gettext.gettext + +EOF = None + +E = { + "null-character": + _("Null character in input stream, replaced with U+FFFD."), + "invalid-codepoint": + _("Invalid codepoint in stream."), + "incorrectly-placed-solidus": + _("Solidus (/) incorrectly placed in tag."), + "incorrect-cr-newline-entity": + _("Incorrect CR newline entity, replaced with LF."), + "illegal-windows-1252-entity": + _("Entity used with illegal number (windows-1252 reference)."), + "cant-convert-numeric-entity": + _("Numeric entity couldn't be converted to character " + "(codepoint U+%(charAsInt)08x)."), + "illegal-codepoint-for-numeric-entity": + _("Numeric entity represents an illegal codepoint: " + "U+%(charAsInt)08x."), + "numeric-entity-without-semicolon": + _("Numeric entity didn't end with ';'."), + "expected-numeric-entity-but-got-eof": + _("Numeric entity expected. Got end of file instead."), + "expected-numeric-entity": + _("Numeric entity expected but none found."), + "named-entity-without-semicolon": + _("Named entity didn't end with ';'."), + "expected-named-entity": + _("Named entity expected. Got none."), + "attributes-in-end-tag": + _("End tag contains unexpected attributes."), + 'self-closing-flag-on-end-tag': + _("End tag contains unexpected self-closing flag."), + "expected-tag-name-but-got-right-bracket": + _("Expected tag name. Got '>' instead."), + "expected-tag-name-but-got-question-mark": + _("Expected tag name. Got '?' instead. (HTML doesn't " + "support processing instructions.)"), + "expected-tag-name": + _("Expected tag name. Got something else instead"), + "expected-closing-tag-but-got-right-bracket": + _("Expected closing tag. Got '>' instead. Ignoring ''."), + "expected-closing-tag-but-got-eof": + _("Expected closing tag. Unexpected end of file."), + "expected-closing-tag-but-got-char": + _("Expected closing tag. Unexpected character '%(data)s' found."), + "eof-in-tag-name": + _("Unexpected end of file in the tag name."), + "expected-attribute-name-but-got-eof": + _("Unexpected end of file. Expected attribute name instead."), + "eof-in-attribute-name": + _("Unexpected end of file in attribute name."), + "invalid-character-in-attribute-name": + _("Invalid character in attribute name"), + "duplicate-attribute": + _("Dropped duplicate attribute on tag."), + "expected-end-of-tag-name-but-got-eof": + _("Unexpected end of file. Expected = or end of tag."), + "expected-attribute-value-but-got-eof": + _("Unexpected end of file. Expected attribute value."), + "expected-attribute-value-but-got-right-bracket": + _("Expected attribute value. Got '>' instead."), + 'equals-in-unquoted-attribute-value': + _("Unexpected = in unquoted attribute"), + 'unexpected-character-in-unquoted-attribute-value': + _("Unexpected character in unquoted attribute"), + "invalid-character-after-attribute-name": + _("Unexpected character after attribute name."), + "unexpected-character-after-attribute-value": + _("Unexpected character after attribute value."), + "eof-in-attribute-value-double-quote": + _("Unexpected end of file in attribute value (\")."), + "eof-in-attribute-value-single-quote": + _("Unexpected end of file in attribute value (')."), + "eof-in-attribute-value-no-quotes": + _("Unexpected end of file in attribute value."), + "unexpected-EOF-after-solidus-in-tag": + _("Unexpected end of file in tag. Expected >"), + "unexpected-character-after-solidus-in-tag": + _("Unexpected character after / in tag. Expected >"), + "expected-dashes-or-doctype": + _("Expected '--' or 'DOCTYPE'. Not found."), + "unexpected-bang-after-double-dash-in-comment": + _("Unexpected ! after -- in comment"), + "unexpected-space-after-double-dash-in-comment": + _("Unexpected space after -- in comment"), + "incorrect-comment": + _("Incorrect comment."), + "eof-in-comment": + _("Unexpected end of file in comment."), + "eof-in-comment-end-dash": + _("Unexpected end of file in comment (-)"), + "unexpected-dash-after-double-dash-in-comment": + _("Unexpected '-' after '--' found in comment."), + "eof-in-comment-double-dash": + _("Unexpected end of file in comment (--)."), + "eof-in-comment-end-space-state": + _("Unexpected end of file in comment."), + "eof-in-comment-end-bang-state": + _("Unexpected end of file in comment."), + "unexpected-char-in-comment": + _("Unexpected character in comment found."), + "need-space-after-doctype": + _("No space after literal string 'DOCTYPE'."), + "expected-doctype-name-but-got-right-bracket": + _("Unexpected > character. Expected DOCTYPE name."), + "expected-doctype-name-but-got-eof": + _("Unexpected end of file. Expected DOCTYPE name."), + "eof-in-doctype-name": + _("Unexpected end of file in DOCTYPE name."), + "eof-in-doctype": + _("Unexpected end of file in DOCTYPE."), + "expected-space-or-right-bracket-in-doctype": + _("Expected space or '>'. Got '%(data)s'"), + "unexpected-end-of-doctype": + _("Unexpected end of DOCTYPE."), + "unexpected-char-in-doctype": + _("Unexpected character in DOCTYPE."), + "eof-in-innerhtml": + _("XXX innerHTML EOF"), + "unexpected-doctype": + _("Unexpected DOCTYPE. Ignored."), + "non-html-root": + _("html needs to be the first start tag."), + "expected-doctype-but-got-eof": + _("Unexpected End of file. Expected DOCTYPE."), + "unknown-doctype": + _("Erroneous DOCTYPE."), + "expected-doctype-but-got-chars": + _("Unexpected non-space characters. Expected DOCTYPE."), + "expected-doctype-but-got-start-tag": + _("Unexpected start tag (%(name)s). Expected DOCTYPE."), + "expected-doctype-but-got-end-tag": + _("Unexpected end tag (%(name)s). Expected DOCTYPE."), + "end-tag-after-implied-root": + _("Unexpected end tag (%(name)s) after the (implied) root element."), + "expected-named-closing-tag-but-got-eof": + _("Unexpected end of file. Expected end tag (%(name)s)."), + "two-heads-are-not-better-than-one": + _("Unexpected start tag head in existing head. Ignored."), + "unexpected-end-tag": + _("Unexpected end tag (%(name)s). Ignored."), + "unexpected-start-tag-out-of-my-head": + _("Unexpected start tag (%(name)s) that can be in head. Moved."), + "unexpected-start-tag": + _("Unexpected start tag (%(name)s)."), + "missing-end-tag": + _("Missing end tag (%(name)s)."), + "missing-end-tags": + _("Missing end tags (%(name)s)."), + "unexpected-start-tag-implies-end-tag": + _("Unexpected start tag (%(startName)s) " + "implies end tag (%(endName)s)."), + "unexpected-start-tag-treated-as": + _("Unexpected start tag (%(originalName)s). Treated as %(newName)s."), + "deprecated-tag": + _("Unexpected start tag %(name)s. Don't use it!"), + "unexpected-start-tag-ignored": + _("Unexpected start tag %(name)s. Ignored."), + "expected-one-end-tag-but-got-another": + _("Unexpected end tag (%(gotName)s). " + "Missing end tag (%(expectedName)s)."), + "end-tag-too-early": + _("End tag (%(name)s) seen too early. Expected other end tag."), + "end-tag-too-early-named": + _("Unexpected end tag (%(gotName)s). Expected end tag (%(expectedName)s)."), + "end-tag-too-early-ignored": + _("End tag (%(name)s) seen too early. Ignored."), + "adoption-agency-1.1": + _("End tag (%(name)s) violates step 1, " + "paragraph 1 of the adoption agency algorithm."), + "adoption-agency-1.2": + _("End tag (%(name)s) violates step 1, " + "paragraph 2 of the adoption agency algorithm."), + "adoption-agency-1.3": + _("End tag (%(name)s) violates step 1, " + "paragraph 3 of the adoption agency algorithm."), + "adoption-agency-4.4": + _("End tag (%(name)s) violates step 4, " + "paragraph 4 of the adoption agency algorithm."), + "unexpected-end-tag-treated-as": + _("Unexpected end tag (%(originalName)s). Treated as %(newName)s."), + "no-end-tag": + _("This element (%(name)s) has no end tag."), + "unexpected-implied-end-tag-in-table": + _("Unexpected implied end tag (%(name)s) in the table phase."), + "unexpected-implied-end-tag-in-table-body": + _("Unexpected implied end tag (%(name)s) in the table body phase."), + "unexpected-char-implies-table-voodoo": + _("Unexpected non-space characters in " + "table context caused voodoo mode."), + "unexpected-hidden-input-in-table": + _("Unexpected input with type hidden in table context."), + "unexpected-form-in-table": + _("Unexpected form in table context."), + "unexpected-start-tag-implies-table-voodoo": + _("Unexpected start tag (%(name)s) in " + "table context caused voodoo mode."), + "unexpected-end-tag-implies-table-voodoo": + _("Unexpected end tag (%(name)s) in " + "table context caused voodoo mode."), + "unexpected-cell-in-table-body": + _("Unexpected table cell start tag (%(name)s) " + "in the table body phase."), + "unexpected-cell-end-tag": + _("Got table cell end tag (%(name)s) " + "while required end tags are missing."), + "unexpected-end-tag-in-table-body": + _("Unexpected end tag (%(name)s) in the table body phase. Ignored."), + "unexpected-implied-end-tag-in-table-row": + _("Unexpected implied end tag (%(name)s) in the table row phase."), + "unexpected-end-tag-in-table-row": + _("Unexpected end tag (%(name)s) in the table row phase. Ignored."), + "unexpected-select-in-select": + _("Unexpected select start tag in the select phase " + "treated as select end tag."), + "unexpected-input-in-select": + _("Unexpected input start tag in the select phase."), + "unexpected-start-tag-in-select": + _("Unexpected start tag token (%(name)s in the select phase. " + "Ignored."), + "unexpected-end-tag-in-select": + _("Unexpected end tag (%(name)s) in the select phase. Ignored."), + "unexpected-table-element-start-tag-in-select-in-table": + _("Unexpected table element start tag (%(name)s) in the select in table phase."), + "unexpected-table-element-end-tag-in-select-in-table": + _("Unexpected table element end tag (%(name)s) in the select in table phase."), + "unexpected-char-after-body": + _("Unexpected non-space characters in the after body phase."), + "unexpected-start-tag-after-body": + _("Unexpected start tag token (%(name)s)" + " in the after body phase."), + "unexpected-end-tag-after-body": + _("Unexpected end tag token (%(name)s)" + " in the after body phase."), + "unexpected-char-in-frameset": + _("Unexpected characters in the frameset phase. Characters ignored."), + "unexpected-start-tag-in-frameset": + _("Unexpected start tag token (%(name)s)" + " in the frameset phase. Ignored."), + "unexpected-frameset-in-frameset-innerhtml": + _("Unexpected end tag token (frameset) " + "in the frameset phase (innerHTML)."), + "unexpected-end-tag-in-frameset": + _("Unexpected end tag token (%(name)s)" + " in the frameset phase. Ignored."), + "unexpected-char-after-frameset": + _("Unexpected non-space characters in the " + "after frameset phase. Ignored."), + "unexpected-start-tag-after-frameset": + _("Unexpected start tag (%(name)s)" + " in the after frameset phase. Ignored."), + "unexpected-end-tag-after-frameset": + _("Unexpected end tag (%(name)s)" + " in the after frameset phase. Ignored."), + "unexpected-end-tag-after-body-innerhtml": + _("Unexpected end tag after body(innerHtml)"), + "expected-eof-but-got-char": + _("Unexpected non-space characters. Expected end of file."), + "expected-eof-but-got-start-tag": + _("Unexpected start tag (%(name)s)" + ". Expected end of file."), + "expected-eof-but-got-end-tag": + _("Unexpected end tag (%(name)s)" + ". Expected end of file."), + "eof-in-table": + _("Unexpected end of file. Expected table content."), + "eof-in-select": + _("Unexpected end of file. Expected select content."), + "eof-in-frameset": + _("Unexpected end of file. Expected frameset content."), + "eof-in-script-in-script": + _("Unexpected end of file. Expected script content."), + "eof-in-foreign-lands": + _("Unexpected end of file. Expected foreign content"), + "non-void-element-with-trailing-solidus": + _("Trailing solidus not allowed on element %(name)s"), + "unexpected-html-element-in-foreign-content": + _("Element %(name)s not allowed in a non-html context"), + "unexpected-end-tag-before-html": + _("Unexpected end tag (%(name)s) before html."), + "XXX-undefined-error": + _("Undefined error (this sucks and should be fixed)"), +} + +namespaces = { + "html": "http://www.w3.org/1999/xhtml", + "mathml": "http://www.w3.org/1998/Math/MathML", + "svg": "http://www.w3.org/2000/svg", + "xlink": "http://www.w3.org/1999/xlink", + "xml": "http://www.w3.org/XML/1998/namespace", + "xmlns": "http://www.w3.org/2000/xmlns/" +} + +scopingElements = frozenset(( + (namespaces["html"], "applet"), + (namespaces["html"], "caption"), + (namespaces["html"], "html"), + (namespaces["html"], "marquee"), + (namespaces["html"], "object"), + (namespaces["html"], "table"), + (namespaces["html"], "td"), + (namespaces["html"], "th"), + (namespaces["mathml"], "mi"), + (namespaces["mathml"], "mo"), + (namespaces["mathml"], "mn"), + (namespaces["mathml"], "ms"), + (namespaces["mathml"], "mtext"), + (namespaces["mathml"], "annotation-xml"), + (namespaces["svg"], "foreignObject"), + (namespaces["svg"], "desc"), + (namespaces["svg"], "title"), +)) + +formattingElements = frozenset(( + (namespaces["html"], "a"), + (namespaces["html"], "b"), + (namespaces["html"], "big"), + (namespaces["html"], "code"), + (namespaces["html"], "em"), + (namespaces["html"], "font"), + (namespaces["html"], "i"), + (namespaces["html"], "nobr"), + (namespaces["html"], "s"), + (namespaces["html"], "small"), + (namespaces["html"], "strike"), + (namespaces["html"], "strong"), + (namespaces["html"], "tt"), + (namespaces["html"], "u") +)) + +specialElements = frozenset(( + (namespaces["html"], "address"), + (namespaces["html"], "applet"), + (namespaces["html"], "area"), + (namespaces["html"], "article"), + (namespaces["html"], "aside"), + (namespaces["html"], "base"), + (namespaces["html"], "basefont"), + (namespaces["html"], "bgsound"), + (namespaces["html"], "blockquote"), + (namespaces["html"], "body"), + (namespaces["html"], "br"), + (namespaces["html"], "button"), + (namespaces["html"], "caption"), + (namespaces["html"], "center"), + (namespaces["html"], "col"), + (namespaces["html"], "colgroup"), + (namespaces["html"], "command"), + (namespaces["html"], "dd"), + (namespaces["html"], "details"), + (namespaces["html"], "dir"), + (namespaces["html"], "div"), + (namespaces["html"], "dl"), + (namespaces["html"], "dt"), + (namespaces["html"], "embed"), + (namespaces["html"], "fieldset"), + (namespaces["html"], "figure"), + (namespaces["html"], "footer"), + (namespaces["html"], "form"), + (namespaces["html"], "frame"), + (namespaces["html"], "frameset"), + (namespaces["html"], "h1"), + (namespaces["html"], "h2"), + (namespaces["html"], "h3"), + (namespaces["html"], "h4"), + (namespaces["html"], "h5"), + (namespaces["html"], "h6"), + (namespaces["html"], "head"), + (namespaces["html"], "header"), + (namespaces["html"], "hr"), + (namespaces["html"], "html"), + (namespaces["html"], "iframe"), + # Note that image is commented out in the spec as "this isn't an + # element that can end up on the stack, so it doesn't matter," + (namespaces["html"], "image"), + (namespaces["html"], "img"), + (namespaces["html"], "input"), + (namespaces["html"], "isindex"), + (namespaces["html"], "li"), + (namespaces["html"], "link"), + (namespaces["html"], "listing"), + (namespaces["html"], "marquee"), + (namespaces["html"], "menu"), + (namespaces["html"], "meta"), + (namespaces["html"], "nav"), + (namespaces["html"], "noembed"), + (namespaces["html"], "noframes"), + (namespaces["html"], "noscript"), + (namespaces["html"], "object"), + (namespaces["html"], "ol"), + (namespaces["html"], "p"), + (namespaces["html"], "param"), + (namespaces["html"], "plaintext"), + (namespaces["html"], "pre"), + (namespaces["html"], "script"), + (namespaces["html"], "section"), + (namespaces["html"], "select"), + (namespaces["html"], "style"), + (namespaces["html"], "table"), + (namespaces["html"], "tbody"), + (namespaces["html"], "td"), + (namespaces["html"], "textarea"), + (namespaces["html"], "tfoot"), + (namespaces["html"], "th"), + (namespaces["html"], "thead"), + (namespaces["html"], "title"), + (namespaces["html"], "tr"), + (namespaces["html"], "ul"), + (namespaces["html"], "wbr"), + (namespaces["html"], "xmp"), + (namespaces["svg"], "foreignObject") +)) + +htmlIntegrationPointElements = frozenset(( + (namespaces["mathml"], "annotaion-xml"), + (namespaces["svg"], "foreignObject"), + (namespaces["svg"], "desc"), + (namespaces["svg"], "title") +)) + +mathmlTextIntegrationPointElements = frozenset(( + (namespaces["mathml"], "mi"), + (namespaces["mathml"], "mo"), + (namespaces["mathml"], "mn"), + (namespaces["mathml"], "ms"), + (namespaces["mathml"], "mtext") +)) + +adjustForeignAttributes = { + "xlink:actuate": ("xlink", "actuate", namespaces["xlink"]), + "xlink:arcrole": ("xlink", "arcrole", namespaces["xlink"]), + "xlink:href": ("xlink", "href", namespaces["xlink"]), + "xlink:role": ("xlink", "role", namespaces["xlink"]), + "xlink:show": ("xlink", "show", namespaces["xlink"]), + "xlink:title": ("xlink", "title", namespaces["xlink"]), + "xlink:type": ("xlink", "type", namespaces["xlink"]), + "xml:base": ("xml", "base", namespaces["xml"]), + "xml:lang": ("xml", "lang", namespaces["xml"]), + "xml:space": ("xml", "space", namespaces["xml"]), + "xmlns": (None, "xmlns", namespaces["xmlns"]), + "xmlns:xlink": ("xmlns", "xlink", namespaces["xmlns"]) +} + +unadjustForeignAttributes = dict([((ns, local), qname) for qname, (prefix, local, ns) in + adjustForeignAttributes.items()]) + +spaceCharacters = frozenset(( + "\t", + "\n", + "\u000C", + " ", + "\r" +)) + +tableInsertModeElements = frozenset(( + "table", + "tbody", + "tfoot", + "thead", + "tr" +)) + +asciiLowercase = frozenset(string.ascii_lowercase) +asciiUppercase = frozenset(string.ascii_uppercase) +asciiLetters = frozenset(string.ascii_letters) +digits = frozenset(string.digits) +hexDigits = frozenset(string.hexdigits) + +asciiUpper2Lower = dict([(ord(c), ord(c.lower())) + for c in string.ascii_uppercase]) + +# Heading elements need to be ordered +headingElements = ( + "h1", + "h2", + "h3", + "h4", + "h5", + "h6" +) + +voidElements = frozenset(( + "base", + "command", + "event-source", + "link", + "meta", + "hr", + "br", + "img", + "embed", + "param", + "area", + "col", + "input", + "source", + "track" +)) + +cdataElements = frozenset(('title', 'textarea')) + +rcdataElements = frozenset(( + 'style', + 'script', + 'xmp', + 'iframe', + 'noembed', + 'noframes', + 'noscript' +)) + +booleanAttributes = { + "": frozenset(("irrelevant",)), + "style": frozenset(("scoped",)), + "img": frozenset(("ismap",)), + "audio": frozenset(("autoplay", "controls")), + "video": frozenset(("autoplay", "controls")), + "script": frozenset(("defer", "async")), + "details": frozenset(("open",)), + "datagrid": frozenset(("multiple", "disabled")), + "command": frozenset(("hidden", "disabled", "checked", "default")), + "hr": frozenset(("noshade")), + "menu": frozenset(("autosubmit",)), + "fieldset": frozenset(("disabled", "readonly")), + "option": frozenset(("disabled", "readonly", "selected")), + "optgroup": frozenset(("disabled", "readonly")), + "button": frozenset(("disabled", "autofocus")), + "input": frozenset(("disabled", "readonly", "required", "autofocus", "checked", "ismap")), + "select": frozenset(("disabled", "readonly", "autofocus", "multiple")), + "output": frozenset(("disabled", "readonly")), +} + +# entitiesWindows1252 has to be _ordered_ and needs to have an index. It +# therefore can't be a frozenset. +entitiesWindows1252 = ( + 8364, # 0x80 0x20AC EURO SIGN + 65533, # 0x81 UNDEFINED + 8218, # 0x82 0x201A SINGLE LOW-9 QUOTATION MARK + 402, # 0x83 0x0192 LATIN SMALL LETTER F WITH HOOK + 8222, # 0x84 0x201E DOUBLE LOW-9 QUOTATION MARK + 8230, # 0x85 0x2026 HORIZONTAL ELLIPSIS + 8224, # 0x86 0x2020 DAGGER + 8225, # 0x87 0x2021 DOUBLE DAGGER + 710, # 0x88 0x02C6 MODIFIER LETTER CIRCUMFLEX ACCENT + 8240, # 0x89 0x2030 PER MILLE SIGN + 352, # 0x8A 0x0160 LATIN CAPITAL LETTER S WITH CARON + 8249, # 0x8B 0x2039 SINGLE LEFT-POINTING ANGLE QUOTATION MARK + 338, # 0x8C 0x0152 LATIN CAPITAL LIGATURE OE + 65533, # 0x8D UNDEFINED + 381, # 0x8E 0x017D LATIN CAPITAL LETTER Z WITH CARON + 65533, # 0x8F UNDEFINED + 65533, # 0x90 UNDEFINED + 8216, # 0x91 0x2018 LEFT SINGLE QUOTATION MARK + 8217, # 0x92 0x2019 RIGHT SINGLE QUOTATION MARK + 8220, # 0x93 0x201C LEFT DOUBLE QUOTATION MARK + 8221, # 0x94 0x201D RIGHT DOUBLE QUOTATION MARK + 8226, # 0x95 0x2022 BULLET + 8211, # 0x96 0x2013 EN DASH + 8212, # 0x97 0x2014 EM DASH + 732, # 0x98 0x02DC SMALL TILDE + 8482, # 0x99 0x2122 TRADE MARK SIGN + 353, # 0x9A 0x0161 LATIN SMALL LETTER S WITH CARON + 8250, # 0x9B 0x203A SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + 339, # 0x9C 0x0153 LATIN SMALL LIGATURE OE + 65533, # 0x9D UNDEFINED + 382, # 0x9E 0x017E LATIN SMALL LETTER Z WITH CARON + 376 # 0x9F 0x0178 LATIN CAPITAL LETTER Y WITH DIAERESIS +) + +xmlEntities = frozenset(('lt;', 'gt;', 'amp;', 'apos;', 'quot;')) + +entities = { + "AElig": "\xc6", + "AElig;": "\xc6", + "AMP": "&", + "AMP;": "&", + "Aacute": "\xc1", + "Aacute;": "\xc1", + "Abreve;": "\u0102", + "Acirc": "\xc2", + "Acirc;": "\xc2", + "Acy;": "\u0410", + "Afr;": "\U0001d504", + "Agrave": "\xc0", + "Agrave;": "\xc0", + "Alpha;": "\u0391", + "Amacr;": "\u0100", + "And;": "\u2a53", + "Aogon;": "\u0104", + "Aopf;": "\U0001d538", + "ApplyFunction;": "\u2061", + "Aring": "\xc5", + "Aring;": "\xc5", + "Ascr;": "\U0001d49c", + "Assign;": "\u2254", + "Atilde": "\xc3", + "Atilde;": "\xc3", + "Auml": "\xc4", + "Auml;": "\xc4", + "Backslash;": "\u2216", + "Barv;": "\u2ae7", + "Barwed;": "\u2306", + "Bcy;": "\u0411", + "Because;": "\u2235", + "Bernoullis;": "\u212c", + "Beta;": "\u0392", + "Bfr;": "\U0001d505", + "Bopf;": "\U0001d539", + "Breve;": "\u02d8", + "Bscr;": "\u212c", + "Bumpeq;": "\u224e", + "CHcy;": "\u0427", + "COPY": "\xa9", + "COPY;": "\xa9", + "Cacute;": "\u0106", + "Cap;": "\u22d2", + "CapitalDifferentialD;": "\u2145", + "Cayleys;": "\u212d", + "Ccaron;": "\u010c", + "Ccedil": "\xc7", + "Ccedil;": "\xc7", + "Ccirc;": "\u0108", + "Cconint;": "\u2230", + "Cdot;": "\u010a", + "Cedilla;": "\xb8", + "CenterDot;": "\xb7", + "Cfr;": "\u212d", + "Chi;": "\u03a7", + "CircleDot;": "\u2299", + "CircleMinus;": "\u2296", + "CirclePlus;": "\u2295", + "CircleTimes;": "\u2297", + "ClockwiseContourIntegral;": "\u2232", + "CloseCurlyDoubleQuote;": "\u201d", + "CloseCurlyQuote;": "\u2019", + "Colon;": "\u2237", + "Colone;": "\u2a74", + "Congruent;": "\u2261", + "Conint;": "\u222f", + "ContourIntegral;": "\u222e", + "Copf;": "\u2102", + "Coproduct;": "\u2210", + "CounterClockwiseContourIntegral;": "\u2233", + "Cross;": "\u2a2f", + "Cscr;": "\U0001d49e", + "Cup;": "\u22d3", + "CupCap;": "\u224d", + "DD;": "\u2145", + "DDotrahd;": "\u2911", + "DJcy;": "\u0402", + "DScy;": "\u0405", + "DZcy;": "\u040f", + "Dagger;": "\u2021", + "Darr;": "\u21a1", + "Dashv;": "\u2ae4", + "Dcaron;": "\u010e", + "Dcy;": "\u0414", + "Del;": "\u2207", + "Delta;": "\u0394", + "Dfr;": "\U0001d507", + "DiacriticalAcute;": "\xb4", + "DiacriticalDot;": "\u02d9", + "DiacriticalDoubleAcute;": "\u02dd", + "DiacriticalGrave;": "`", + "DiacriticalTilde;": "\u02dc", + "Diamond;": "\u22c4", + "DifferentialD;": "\u2146", + "Dopf;": "\U0001d53b", + "Dot;": "\xa8", + "DotDot;": "\u20dc", + "DotEqual;": "\u2250", + "DoubleContourIntegral;": "\u222f", + "DoubleDot;": "\xa8", + "DoubleDownArrow;": "\u21d3", + "DoubleLeftArrow;": "\u21d0", + "DoubleLeftRightArrow;": "\u21d4", + "DoubleLeftTee;": "\u2ae4", + "DoubleLongLeftArrow;": "\u27f8", + "DoubleLongLeftRightArrow;": "\u27fa", + "DoubleLongRightArrow;": "\u27f9", + "DoubleRightArrow;": "\u21d2", + "DoubleRightTee;": "\u22a8", + "DoubleUpArrow;": "\u21d1", + "DoubleUpDownArrow;": "\u21d5", + "DoubleVerticalBar;": "\u2225", + "DownArrow;": "\u2193", + "DownArrowBar;": "\u2913", + "DownArrowUpArrow;": "\u21f5", + "DownBreve;": "\u0311", + "DownLeftRightVector;": "\u2950", + "DownLeftTeeVector;": "\u295e", + "DownLeftVector;": "\u21bd", + "DownLeftVectorBar;": "\u2956", + "DownRightTeeVector;": "\u295f", + "DownRightVector;": "\u21c1", + "DownRightVectorBar;": "\u2957", + "DownTee;": "\u22a4", + "DownTeeArrow;": "\u21a7", + "Downarrow;": "\u21d3", + "Dscr;": "\U0001d49f", + "Dstrok;": "\u0110", + "ENG;": "\u014a", + "ETH": "\xd0", + "ETH;": "\xd0", + "Eacute": "\xc9", + "Eacute;": "\xc9", + "Ecaron;": "\u011a", + "Ecirc": "\xca", + "Ecirc;": "\xca", + "Ecy;": "\u042d", + "Edot;": "\u0116", + "Efr;": "\U0001d508", + "Egrave": "\xc8", + "Egrave;": "\xc8", + "Element;": "\u2208", + "Emacr;": "\u0112", + "EmptySmallSquare;": "\u25fb", + "EmptyVerySmallSquare;": "\u25ab", + "Eogon;": "\u0118", + "Eopf;": "\U0001d53c", + "Epsilon;": "\u0395", + "Equal;": "\u2a75", + "EqualTilde;": "\u2242", + "Equilibrium;": "\u21cc", + "Escr;": "\u2130", + "Esim;": "\u2a73", + "Eta;": "\u0397", + "Euml": "\xcb", + "Euml;": "\xcb", + "Exists;": "\u2203", + "ExponentialE;": "\u2147", + "Fcy;": "\u0424", + "Ffr;": "\U0001d509", + "FilledSmallSquare;": "\u25fc", + "FilledVerySmallSquare;": "\u25aa", + "Fopf;": "\U0001d53d", + "ForAll;": "\u2200", + "Fouriertrf;": "\u2131", + "Fscr;": "\u2131", + "GJcy;": "\u0403", + "GT": ">", + "GT;": ">", + "Gamma;": "\u0393", + "Gammad;": "\u03dc", + "Gbreve;": "\u011e", + "Gcedil;": "\u0122", + "Gcirc;": "\u011c", + "Gcy;": "\u0413", + "Gdot;": "\u0120", + "Gfr;": "\U0001d50a", + "Gg;": "\u22d9", + "Gopf;": "\U0001d53e", + "GreaterEqual;": "\u2265", + "GreaterEqualLess;": "\u22db", + "GreaterFullEqual;": "\u2267", + "GreaterGreater;": "\u2aa2", + "GreaterLess;": "\u2277", + "GreaterSlantEqual;": "\u2a7e", + "GreaterTilde;": "\u2273", + "Gscr;": "\U0001d4a2", + "Gt;": "\u226b", + "HARDcy;": "\u042a", + "Hacek;": "\u02c7", + "Hat;": "^", + "Hcirc;": "\u0124", + "Hfr;": "\u210c", + "HilbertSpace;": "\u210b", + "Hopf;": "\u210d", + "HorizontalLine;": "\u2500", + "Hscr;": "\u210b", + "Hstrok;": "\u0126", + "HumpDownHump;": "\u224e", + "HumpEqual;": "\u224f", + "IEcy;": "\u0415", + "IJlig;": "\u0132", + "IOcy;": "\u0401", + "Iacute": "\xcd", + "Iacute;": "\xcd", + "Icirc": "\xce", + "Icirc;": "\xce", + "Icy;": "\u0418", + "Idot;": "\u0130", + "Ifr;": "\u2111", + "Igrave": "\xcc", + "Igrave;": "\xcc", + "Im;": "\u2111", + "Imacr;": "\u012a", + "ImaginaryI;": "\u2148", + "Implies;": "\u21d2", + "Int;": "\u222c", + "Integral;": "\u222b", + "Intersection;": "\u22c2", + "InvisibleComma;": "\u2063", + "InvisibleTimes;": "\u2062", + "Iogon;": "\u012e", + "Iopf;": "\U0001d540", + "Iota;": "\u0399", + "Iscr;": "\u2110", + "Itilde;": "\u0128", + "Iukcy;": "\u0406", + "Iuml": "\xcf", + "Iuml;": "\xcf", + "Jcirc;": "\u0134", + "Jcy;": "\u0419", + "Jfr;": "\U0001d50d", + "Jopf;": "\U0001d541", + "Jscr;": "\U0001d4a5", + "Jsercy;": "\u0408", + "Jukcy;": "\u0404", + "KHcy;": "\u0425", + "KJcy;": "\u040c", + "Kappa;": "\u039a", + "Kcedil;": "\u0136", + "Kcy;": "\u041a", + "Kfr;": "\U0001d50e", + "Kopf;": "\U0001d542", + "Kscr;": "\U0001d4a6", + "LJcy;": "\u0409", + "LT": "<", + "LT;": "<", + "Lacute;": "\u0139", + "Lambda;": "\u039b", + "Lang;": "\u27ea", + "Laplacetrf;": "\u2112", + "Larr;": "\u219e", + "Lcaron;": "\u013d", + "Lcedil;": "\u013b", + "Lcy;": "\u041b", + "LeftAngleBracket;": "\u27e8", + "LeftArrow;": "\u2190", + "LeftArrowBar;": "\u21e4", + "LeftArrowRightArrow;": "\u21c6", + "LeftCeiling;": "\u2308", + "LeftDoubleBracket;": "\u27e6", + "LeftDownTeeVector;": "\u2961", + "LeftDownVector;": "\u21c3", + "LeftDownVectorBar;": "\u2959", + "LeftFloor;": "\u230a", + "LeftRightArrow;": "\u2194", + "LeftRightVector;": "\u294e", + "LeftTee;": "\u22a3", + "LeftTeeArrow;": "\u21a4", + "LeftTeeVector;": "\u295a", + "LeftTriangle;": "\u22b2", + "LeftTriangleBar;": "\u29cf", + "LeftTriangleEqual;": "\u22b4", + "LeftUpDownVector;": "\u2951", + "LeftUpTeeVector;": "\u2960", + "LeftUpVector;": "\u21bf", + "LeftUpVectorBar;": "\u2958", + "LeftVector;": "\u21bc", + "LeftVectorBar;": "\u2952", + "Leftarrow;": "\u21d0", + "Leftrightarrow;": "\u21d4", + "LessEqualGreater;": "\u22da", + "LessFullEqual;": "\u2266", + "LessGreater;": "\u2276", + "LessLess;": "\u2aa1", + "LessSlantEqual;": "\u2a7d", + "LessTilde;": "\u2272", + "Lfr;": "\U0001d50f", + "Ll;": "\u22d8", + "Lleftarrow;": "\u21da", + "Lmidot;": "\u013f", + "LongLeftArrow;": "\u27f5", + "LongLeftRightArrow;": "\u27f7", + "LongRightArrow;": "\u27f6", + "Longleftarrow;": "\u27f8", + "Longleftrightarrow;": "\u27fa", + "Longrightarrow;": "\u27f9", + "Lopf;": "\U0001d543", + "LowerLeftArrow;": "\u2199", + "LowerRightArrow;": "\u2198", + "Lscr;": "\u2112", + "Lsh;": "\u21b0", + "Lstrok;": "\u0141", + "Lt;": "\u226a", + "Map;": "\u2905", + "Mcy;": "\u041c", + "MediumSpace;": "\u205f", + "Mellintrf;": "\u2133", + "Mfr;": "\U0001d510", + "MinusPlus;": "\u2213", + "Mopf;": "\U0001d544", + "Mscr;": "\u2133", + "Mu;": "\u039c", + "NJcy;": "\u040a", + "Nacute;": "\u0143", + "Ncaron;": "\u0147", + "Ncedil;": "\u0145", + "Ncy;": "\u041d", + "NegativeMediumSpace;": "\u200b", + "NegativeThickSpace;": "\u200b", + "NegativeThinSpace;": "\u200b", + "NegativeVeryThinSpace;": "\u200b", + "NestedGreaterGreater;": "\u226b", + "NestedLessLess;": "\u226a", + "NewLine;": "\n", + "Nfr;": "\U0001d511", + "NoBreak;": "\u2060", + "NonBreakingSpace;": "\xa0", + "Nopf;": "\u2115", + "Not;": "\u2aec", + "NotCongruent;": "\u2262", + "NotCupCap;": "\u226d", + "NotDoubleVerticalBar;": "\u2226", + "NotElement;": "\u2209", + "NotEqual;": "\u2260", + "NotEqualTilde;": "\u2242\u0338", + "NotExists;": "\u2204", + "NotGreater;": "\u226f", + "NotGreaterEqual;": "\u2271", + "NotGreaterFullEqual;": "\u2267\u0338", + "NotGreaterGreater;": "\u226b\u0338", + "NotGreaterLess;": "\u2279", + "NotGreaterSlantEqual;": "\u2a7e\u0338", + "NotGreaterTilde;": "\u2275", + "NotHumpDownHump;": "\u224e\u0338", + "NotHumpEqual;": "\u224f\u0338", + "NotLeftTriangle;": "\u22ea", + "NotLeftTriangleBar;": "\u29cf\u0338", + "NotLeftTriangleEqual;": "\u22ec", + "NotLess;": "\u226e", + "NotLessEqual;": "\u2270", + "NotLessGreater;": "\u2278", + "NotLessLess;": "\u226a\u0338", + "NotLessSlantEqual;": "\u2a7d\u0338", + "NotLessTilde;": "\u2274", + "NotNestedGreaterGreater;": "\u2aa2\u0338", + "NotNestedLessLess;": "\u2aa1\u0338", + "NotPrecedes;": "\u2280", + "NotPrecedesEqual;": "\u2aaf\u0338", + "NotPrecedesSlantEqual;": "\u22e0", + "NotReverseElement;": "\u220c", + "NotRightTriangle;": "\u22eb", + "NotRightTriangleBar;": "\u29d0\u0338", + "NotRightTriangleEqual;": "\u22ed", + "NotSquareSubset;": "\u228f\u0338", + "NotSquareSubsetEqual;": "\u22e2", + "NotSquareSuperset;": "\u2290\u0338", + "NotSquareSupersetEqual;": "\u22e3", + "NotSubset;": "\u2282\u20d2", + "NotSubsetEqual;": "\u2288", + "NotSucceeds;": "\u2281", + "NotSucceedsEqual;": "\u2ab0\u0338", + "NotSucceedsSlantEqual;": "\u22e1", + "NotSucceedsTilde;": "\u227f\u0338", + "NotSuperset;": "\u2283\u20d2", + "NotSupersetEqual;": "\u2289", + "NotTilde;": "\u2241", + "NotTildeEqual;": "\u2244", + "NotTildeFullEqual;": "\u2247", + "NotTildeTilde;": "\u2249", + "NotVerticalBar;": "\u2224", + "Nscr;": "\U0001d4a9", + "Ntilde": "\xd1", + "Ntilde;": "\xd1", + "Nu;": "\u039d", + "OElig;": "\u0152", + "Oacute": "\xd3", + "Oacute;": "\xd3", + "Ocirc": "\xd4", + "Ocirc;": "\xd4", + "Ocy;": "\u041e", + "Odblac;": "\u0150", + "Ofr;": "\U0001d512", + "Ograve": "\xd2", + "Ograve;": "\xd2", + "Omacr;": "\u014c", + "Omega;": "\u03a9", + "Omicron;": "\u039f", + "Oopf;": "\U0001d546", + "OpenCurlyDoubleQuote;": "\u201c", + "OpenCurlyQuote;": "\u2018", + "Or;": "\u2a54", + "Oscr;": "\U0001d4aa", + "Oslash": "\xd8", + "Oslash;": "\xd8", + "Otilde": "\xd5", + "Otilde;": "\xd5", + "Otimes;": "\u2a37", + "Ouml": "\xd6", + "Ouml;": "\xd6", + "OverBar;": "\u203e", + "OverBrace;": "\u23de", + "OverBracket;": "\u23b4", + "OverParenthesis;": "\u23dc", + "PartialD;": "\u2202", + "Pcy;": "\u041f", + "Pfr;": "\U0001d513", + "Phi;": "\u03a6", + "Pi;": "\u03a0", + "PlusMinus;": "\xb1", + "Poincareplane;": "\u210c", + "Popf;": "\u2119", + "Pr;": "\u2abb", + "Precedes;": "\u227a", + "PrecedesEqual;": "\u2aaf", + "PrecedesSlantEqual;": "\u227c", + "PrecedesTilde;": "\u227e", + "Prime;": "\u2033", + "Product;": "\u220f", + "Proportion;": "\u2237", + "Proportional;": "\u221d", + "Pscr;": "\U0001d4ab", + "Psi;": "\u03a8", + "QUOT": "\"", + "QUOT;": "\"", + "Qfr;": "\U0001d514", + "Qopf;": "\u211a", + "Qscr;": "\U0001d4ac", + "RBarr;": "\u2910", + "REG": "\xae", + "REG;": "\xae", + "Racute;": "\u0154", + "Rang;": "\u27eb", + "Rarr;": "\u21a0", + "Rarrtl;": "\u2916", + "Rcaron;": "\u0158", + "Rcedil;": "\u0156", + "Rcy;": "\u0420", + "Re;": "\u211c", + "ReverseElement;": "\u220b", + "ReverseEquilibrium;": "\u21cb", + "ReverseUpEquilibrium;": "\u296f", + "Rfr;": "\u211c", + "Rho;": "\u03a1", + "RightAngleBracket;": "\u27e9", + "RightArrow;": "\u2192", + "RightArrowBar;": "\u21e5", + "RightArrowLeftArrow;": "\u21c4", + "RightCeiling;": "\u2309", + "RightDoubleBracket;": "\u27e7", + "RightDownTeeVector;": "\u295d", + "RightDownVector;": "\u21c2", + "RightDownVectorBar;": "\u2955", + "RightFloor;": "\u230b", + "RightTee;": "\u22a2", + "RightTeeArrow;": "\u21a6", + "RightTeeVector;": "\u295b", + "RightTriangle;": "\u22b3", + "RightTriangleBar;": "\u29d0", + "RightTriangleEqual;": "\u22b5", + "RightUpDownVector;": "\u294f", + "RightUpTeeVector;": "\u295c", + "RightUpVector;": "\u21be", + "RightUpVectorBar;": "\u2954", + "RightVector;": "\u21c0", + "RightVectorBar;": "\u2953", + "Rightarrow;": "\u21d2", + "Ropf;": "\u211d", + "RoundImplies;": "\u2970", + "Rrightarrow;": "\u21db", + "Rscr;": "\u211b", + "Rsh;": "\u21b1", + "RuleDelayed;": "\u29f4", + "SHCHcy;": "\u0429", + "SHcy;": "\u0428", + "SOFTcy;": "\u042c", + "Sacute;": "\u015a", + "Sc;": "\u2abc", + "Scaron;": "\u0160", + "Scedil;": "\u015e", + "Scirc;": "\u015c", + "Scy;": "\u0421", + "Sfr;": "\U0001d516", + "ShortDownArrow;": "\u2193", + "ShortLeftArrow;": "\u2190", + "ShortRightArrow;": "\u2192", + "ShortUpArrow;": "\u2191", + "Sigma;": "\u03a3", + "SmallCircle;": "\u2218", + "Sopf;": "\U0001d54a", + "Sqrt;": "\u221a", + "Square;": "\u25a1", + "SquareIntersection;": "\u2293", + "SquareSubset;": "\u228f", + "SquareSubsetEqual;": "\u2291", + "SquareSuperset;": "\u2290", + "SquareSupersetEqual;": "\u2292", + "SquareUnion;": "\u2294", + "Sscr;": "\U0001d4ae", + "Star;": "\u22c6", + "Sub;": "\u22d0", + "Subset;": "\u22d0", + "SubsetEqual;": "\u2286", + "Succeeds;": "\u227b", + "SucceedsEqual;": "\u2ab0", + "SucceedsSlantEqual;": "\u227d", + "SucceedsTilde;": "\u227f", + "SuchThat;": "\u220b", + "Sum;": "\u2211", + "Sup;": "\u22d1", + "Superset;": "\u2283", + "SupersetEqual;": "\u2287", + "Supset;": "\u22d1", + "THORN": "\xde", + "THORN;": "\xde", + "TRADE;": "\u2122", + "TSHcy;": "\u040b", + "TScy;": "\u0426", + "Tab;": "\t", + "Tau;": "\u03a4", + "Tcaron;": "\u0164", + "Tcedil;": "\u0162", + "Tcy;": "\u0422", + "Tfr;": "\U0001d517", + "Therefore;": "\u2234", + "Theta;": "\u0398", + "ThickSpace;": "\u205f\u200a", + "ThinSpace;": "\u2009", + "Tilde;": "\u223c", + "TildeEqual;": "\u2243", + "TildeFullEqual;": "\u2245", + "TildeTilde;": "\u2248", + "Topf;": "\U0001d54b", + "TripleDot;": "\u20db", + "Tscr;": "\U0001d4af", + "Tstrok;": "\u0166", + "Uacute": "\xda", + "Uacute;": "\xda", + "Uarr;": "\u219f", + "Uarrocir;": "\u2949", + "Ubrcy;": "\u040e", + "Ubreve;": "\u016c", + "Ucirc": "\xdb", + "Ucirc;": "\xdb", + "Ucy;": "\u0423", + "Udblac;": "\u0170", + "Ufr;": "\U0001d518", + "Ugrave": "\xd9", + "Ugrave;": "\xd9", + "Umacr;": "\u016a", + "UnderBar;": "_", + "UnderBrace;": "\u23df", + "UnderBracket;": "\u23b5", + "UnderParenthesis;": "\u23dd", + "Union;": "\u22c3", + "UnionPlus;": "\u228e", + "Uogon;": "\u0172", + "Uopf;": "\U0001d54c", + "UpArrow;": "\u2191", + "UpArrowBar;": "\u2912", + "UpArrowDownArrow;": "\u21c5", + "UpDownArrow;": "\u2195", + "UpEquilibrium;": "\u296e", + "UpTee;": "\u22a5", + "UpTeeArrow;": "\u21a5", + "Uparrow;": "\u21d1", + "Updownarrow;": "\u21d5", + "UpperLeftArrow;": "\u2196", + "UpperRightArrow;": "\u2197", + "Upsi;": "\u03d2", + "Upsilon;": "\u03a5", + "Uring;": "\u016e", + "Uscr;": "\U0001d4b0", + "Utilde;": "\u0168", + "Uuml": "\xdc", + "Uuml;": "\xdc", + "VDash;": "\u22ab", + "Vbar;": "\u2aeb", + "Vcy;": "\u0412", + "Vdash;": "\u22a9", + "Vdashl;": "\u2ae6", + "Vee;": "\u22c1", + "Verbar;": "\u2016", + "Vert;": "\u2016", + "VerticalBar;": "\u2223", + "VerticalLine;": "|", + "VerticalSeparator;": "\u2758", + "VerticalTilde;": "\u2240", + "VeryThinSpace;": "\u200a", + "Vfr;": "\U0001d519", + "Vopf;": "\U0001d54d", + "Vscr;": "\U0001d4b1", + "Vvdash;": "\u22aa", + "Wcirc;": "\u0174", + "Wedge;": "\u22c0", + "Wfr;": "\U0001d51a", + "Wopf;": "\U0001d54e", + "Wscr;": "\U0001d4b2", + "Xfr;": "\U0001d51b", + "Xi;": "\u039e", + "Xopf;": "\U0001d54f", + "Xscr;": "\U0001d4b3", + "YAcy;": "\u042f", + "YIcy;": "\u0407", + "YUcy;": "\u042e", + "Yacute": "\xdd", + "Yacute;": "\xdd", + "Ycirc;": "\u0176", + "Ycy;": "\u042b", + "Yfr;": "\U0001d51c", + "Yopf;": "\U0001d550", + "Yscr;": "\U0001d4b4", + "Yuml;": "\u0178", + "ZHcy;": "\u0416", + "Zacute;": "\u0179", + "Zcaron;": "\u017d", + "Zcy;": "\u0417", + "Zdot;": "\u017b", + "ZeroWidthSpace;": "\u200b", + "Zeta;": "\u0396", + "Zfr;": "\u2128", + "Zopf;": "\u2124", + "Zscr;": "\U0001d4b5", + "aacute": "\xe1", + "aacute;": "\xe1", + "abreve;": "\u0103", + "ac;": "\u223e", + "acE;": "\u223e\u0333", + "acd;": "\u223f", + "acirc": "\xe2", + "acirc;": "\xe2", + "acute": "\xb4", + "acute;": "\xb4", + "acy;": "\u0430", + "aelig": "\xe6", + "aelig;": "\xe6", + "af;": "\u2061", + "afr;": "\U0001d51e", + "agrave": "\xe0", + "agrave;": "\xe0", + "alefsym;": "\u2135", + "aleph;": "\u2135", + "alpha;": "\u03b1", + "amacr;": "\u0101", + "amalg;": "\u2a3f", + "amp": "&", + "amp;": "&", + "and;": "\u2227", + "andand;": "\u2a55", + "andd;": "\u2a5c", + "andslope;": "\u2a58", + "andv;": "\u2a5a", + "ang;": "\u2220", + "ange;": "\u29a4", + "angle;": "\u2220", + "angmsd;": "\u2221", + "angmsdaa;": "\u29a8", + "angmsdab;": "\u29a9", + "angmsdac;": "\u29aa", + "angmsdad;": "\u29ab", + "angmsdae;": "\u29ac", + "angmsdaf;": "\u29ad", + "angmsdag;": "\u29ae", + "angmsdah;": "\u29af", + "angrt;": "\u221f", + "angrtvb;": "\u22be", + "angrtvbd;": "\u299d", + "angsph;": "\u2222", + "angst;": "\xc5", + "angzarr;": "\u237c", + "aogon;": "\u0105", + "aopf;": "\U0001d552", + "ap;": "\u2248", + "apE;": "\u2a70", + "apacir;": "\u2a6f", + "ape;": "\u224a", + "apid;": "\u224b", + "apos;": "'", + "approx;": "\u2248", + "approxeq;": "\u224a", + "aring": "\xe5", + "aring;": "\xe5", + "ascr;": "\U0001d4b6", + "ast;": "*", + "asymp;": "\u2248", + "asympeq;": "\u224d", + "atilde": "\xe3", + "atilde;": "\xe3", + "auml": "\xe4", + "auml;": "\xe4", + "awconint;": "\u2233", + "awint;": "\u2a11", + "bNot;": "\u2aed", + "backcong;": "\u224c", + "backepsilon;": "\u03f6", + "backprime;": "\u2035", + "backsim;": "\u223d", + "backsimeq;": "\u22cd", + "barvee;": "\u22bd", + "barwed;": "\u2305", + "barwedge;": "\u2305", + "bbrk;": "\u23b5", + "bbrktbrk;": "\u23b6", + "bcong;": "\u224c", + "bcy;": "\u0431", + "bdquo;": "\u201e", + "becaus;": "\u2235", + "because;": "\u2235", + "bemptyv;": "\u29b0", + "bepsi;": "\u03f6", + "bernou;": "\u212c", + "beta;": "\u03b2", + "beth;": "\u2136", + "between;": "\u226c", + "bfr;": "\U0001d51f", + "bigcap;": "\u22c2", + "bigcirc;": "\u25ef", + "bigcup;": "\u22c3", + "bigodot;": "\u2a00", + "bigoplus;": "\u2a01", + "bigotimes;": "\u2a02", + "bigsqcup;": "\u2a06", + "bigstar;": "\u2605", + "bigtriangledown;": "\u25bd", + "bigtriangleup;": "\u25b3", + "biguplus;": "\u2a04", + "bigvee;": "\u22c1", + "bigwedge;": "\u22c0", + "bkarow;": "\u290d", + "blacklozenge;": "\u29eb", + "blacksquare;": "\u25aa", + "blacktriangle;": "\u25b4", + "blacktriangledown;": "\u25be", + "blacktriangleleft;": "\u25c2", + "blacktriangleright;": "\u25b8", + "blank;": "\u2423", + "blk12;": "\u2592", + "blk14;": "\u2591", + "blk34;": "\u2593", + "block;": "\u2588", + "bne;": "=\u20e5", + "bnequiv;": "\u2261\u20e5", + "bnot;": "\u2310", + "bopf;": "\U0001d553", + "bot;": "\u22a5", + "bottom;": "\u22a5", + "bowtie;": "\u22c8", + "boxDL;": "\u2557", + "boxDR;": "\u2554", + "boxDl;": "\u2556", + "boxDr;": "\u2553", + "boxH;": "\u2550", + "boxHD;": "\u2566", + "boxHU;": "\u2569", + "boxHd;": "\u2564", + "boxHu;": "\u2567", + "boxUL;": "\u255d", + "boxUR;": "\u255a", + "boxUl;": "\u255c", + "boxUr;": "\u2559", + "boxV;": "\u2551", + "boxVH;": "\u256c", + "boxVL;": "\u2563", + "boxVR;": "\u2560", + "boxVh;": "\u256b", + "boxVl;": "\u2562", + "boxVr;": "\u255f", + "boxbox;": "\u29c9", + "boxdL;": "\u2555", + "boxdR;": "\u2552", + "boxdl;": "\u2510", + "boxdr;": "\u250c", + "boxh;": "\u2500", + "boxhD;": "\u2565", + "boxhU;": "\u2568", + "boxhd;": "\u252c", + "boxhu;": "\u2534", + "boxminus;": "\u229f", + "boxplus;": "\u229e", + "boxtimes;": "\u22a0", + "boxuL;": "\u255b", + "boxuR;": "\u2558", + "boxul;": "\u2518", + "boxur;": "\u2514", + "boxv;": "\u2502", + "boxvH;": "\u256a", + "boxvL;": "\u2561", + "boxvR;": "\u255e", + "boxvh;": "\u253c", + "boxvl;": "\u2524", + "boxvr;": "\u251c", + "bprime;": "\u2035", + "breve;": "\u02d8", + "brvbar": "\xa6", + "brvbar;": "\xa6", + "bscr;": "\U0001d4b7", + "bsemi;": "\u204f", + "bsim;": "\u223d", + "bsime;": "\u22cd", + "bsol;": "\\", + "bsolb;": "\u29c5", + "bsolhsub;": "\u27c8", + "bull;": "\u2022", + "bullet;": "\u2022", + "bump;": "\u224e", + "bumpE;": "\u2aae", + "bumpe;": "\u224f", + "bumpeq;": "\u224f", + "cacute;": "\u0107", + "cap;": "\u2229", + "capand;": "\u2a44", + "capbrcup;": "\u2a49", + "capcap;": "\u2a4b", + "capcup;": "\u2a47", + "capdot;": "\u2a40", + "caps;": "\u2229\ufe00", + "caret;": "\u2041", + "caron;": "\u02c7", + "ccaps;": "\u2a4d", + "ccaron;": "\u010d", + "ccedil": "\xe7", + "ccedil;": "\xe7", + "ccirc;": "\u0109", + "ccups;": "\u2a4c", + "ccupssm;": "\u2a50", + "cdot;": "\u010b", + "cedil": "\xb8", + "cedil;": "\xb8", + "cemptyv;": "\u29b2", + "cent": "\xa2", + "cent;": "\xa2", + "centerdot;": "\xb7", + "cfr;": "\U0001d520", + "chcy;": "\u0447", + "check;": "\u2713", + "checkmark;": "\u2713", + "chi;": "\u03c7", + "cir;": "\u25cb", + "cirE;": "\u29c3", + "circ;": "\u02c6", + "circeq;": "\u2257", + "circlearrowleft;": "\u21ba", + "circlearrowright;": "\u21bb", + "circledR;": "\xae", + "circledS;": "\u24c8", + "circledast;": "\u229b", + "circledcirc;": "\u229a", + "circleddash;": "\u229d", + "cire;": "\u2257", + "cirfnint;": "\u2a10", + "cirmid;": "\u2aef", + "cirscir;": "\u29c2", + "clubs;": "\u2663", + "clubsuit;": "\u2663", + "colon;": ":", + "colone;": "\u2254", + "coloneq;": "\u2254", + "comma;": ",", + "commat;": "@", + "comp;": "\u2201", + "compfn;": "\u2218", + "complement;": "\u2201", + "complexes;": "\u2102", + "cong;": "\u2245", + "congdot;": "\u2a6d", + "conint;": "\u222e", + "copf;": "\U0001d554", + "coprod;": "\u2210", + "copy": "\xa9", + "copy;": "\xa9", + "copysr;": "\u2117", + "crarr;": "\u21b5", + "cross;": "\u2717", + "cscr;": "\U0001d4b8", + "csub;": "\u2acf", + "csube;": "\u2ad1", + "csup;": "\u2ad0", + "csupe;": "\u2ad2", + "ctdot;": "\u22ef", + "cudarrl;": "\u2938", + "cudarrr;": "\u2935", + "cuepr;": "\u22de", + "cuesc;": "\u22df", + "cularr;": "\u21b6", + "cularrp;": "\u293d", + "cup;": "\u222a", + "cupbrcap;": "\u2a48", + "cupcap;": "\u2a46", + "cupcup;": "\u2a4a", + "cupdot;": "\u228d", + "cupor;": "\u2a45", + "cups;": "\u222a\ufe00", + "curarr;": "\u21b7", + "curarrm;": "\u293c", + "curlyeqprec;": "\u22de", + "curlyeqsucc;": "\u22df", + "curlyvee;": "\u22ce", + "curlywedge;": "\u22cf", + "curren": "\xa4", + "curren;": "\xa4", + "curvearrowleft;": "\u21b6", + "curvearrowright;": "\u21b7", + "cuvee;": "\u22ce", + "cuwed;": "\u22cf", + "cwconint;": "\u2232", + "cwint;": "\u2231", + "cylcty;": "\u232d", + "dArr;": "\u21d3", + "dHar;": "\u2965", + "dagger;": "\u2020", + "daleth;": "\u2138", + "darr;": "\u2193", + "dash;": "\u2010", + "dashv;": "\u22a3", + "dbkarow;": "\u290f", + "dblac;": "\u02dd", + "dcaron;": "\u010f", + "dcy;": "\u0434", + "dd;": "\u2146", + "ddagger;": "\u2021", + "ddarr;": "\u21ca", + "ddotseq;": "\u2a77", + "deg": "\xb0", + "deg;": "\xb0", + "delta;": "\u03b4", + "demptyv;": "\u29b1", + "dfisht;": "\u297f", + "dfr;": "\U0001d521", + "dharl;": "\u21c3", + "dharr;": "\u21c2", + "diam;": "\u22c4", + "diamond;": "\u22c4", + "diamondsuit;": "\u2666", + "diams;": "\u2666", + "die;": "\xa8", + "digamma;": "\u03dd", + "disin;": "\u22f2", + "div;": "\xf7", + "divide": "\xf7", + "divide;": "\xf7", + "divideontimes;": "\u22c7", + "divonx;": "\u22c7", + "djcy;": "\u0452", + "dlcorn;": "\u231e", + "dlcrop;": "\u230d", + "dollar;": "$", + "dopf;": "\U0001d555", + "dot;": "\u02d9", + "doteq;": "\u2250", + "doteqdot;": "\u2251", + "dotminus;": "\u2238", + "dotplus;": "\u2214", + "dotsquare;": "\u22a1", + "doublebarwedge;": "\u2306", + "downarrow;": "\u2193", + "downdownarrows;": "\u21ca", + "downharpoonleft;": "\u21c3", + "downharpoonright;": "\u21c2", + "drbkarow;": "\u2910", + "drcorn;": "\u231f", + "drcrop;": "\u230c", + "dscr;": "\U0001d4b9", + "dscy;": "\u0455", + "dsol;": "\u29f6", + "dstrok;": "\u0111", + "dtdot;": "\u22f1", + "dtri;": "\u25bf", + "dtrif;": "\u25be", + "duarr;": "\u21f5", + "duhar;": "\u296f", + "dwangle;": "\u29a6", + "dzcy;": "\u045f", + "dzigrarr;": "\u27ff", + "eDDot;": "\u2a77", + "eDot;": "\u2251", + "eacute": "\xe9", + "eacute;": "\xe9", + "easter;": "\u2a6e", + "ecaron;": "\u011b", + "ecir;": "\u2256", + "ecirc": "\xea", + "ecirc;": "\xea", + "ecolon;": "\u2255", + "ecy;": "\u044d", + "edot;": "\u0117", + "ee;": "\u2147", + "efDot;": "\u2252", + "efr;": "\U0001d522", + "eg;": "\u2a9a", + "egrave": "\xe8", + "egrave;": "\xe8", + "egs;": "\u2a96", + "egsdot;": "\u2a98", + "el;": "\u2a99", + "elinters;": "\u23e7", + "ell;": "\u2113", + "els;": "\u2a95", + "elsdot;": "\u2a97", + "emacr;": "\u0113", + "empty;": "\u2205", + "emptyset;": "\u2205", + "emptyv;": "\u2205", + "emsp13;": "\u2004", + "emsp14;": "\u2005", + "emsp;": "\u2003", + "eng;": "\u014b", + "ensp;": "\u2002", + "eogon;": "\u0119", + "eopf;": "\U0001d556", + "epar;": "\u22d5", + "eparsl;": "\u29e3", + "eplus;": "\u2a71", + "epsi;": "\u03b5", + "epsilon;": "\u03b5", + "epsiv;": "\u03f5", + "eqcirc;": "\u2256", + "eqcolon;": "\u2255", + "eqsim;": "\u2242", + "eqslantgtr;": "\u2a96", + "eqslantless;": "\u2a95", + "equals;": "=", + "equest;": "\u225f", + "equiv;": "\u2261", + "equivDD;": "\u2a78", + "eqvparsl;": "\u29e5", + "erDot;": "\u2253", + "erarr;": "\u2971", + "escr;": "\u212f", + "esdot;": "\u2250", + "esim;": "\u2242", + "eta;": "\u03b7", + "eth": "\xf0", + "eth;": "\xf0", + "euml": "\xeb", + "euml;": "\xeb", + "euro;": "\u20ac", + "excl;": "!", + "exist;": "\u2203", + "expectation;": "\u2130", + "exponentiale;": "\u2147", + "fallingdotseq;": "\u2252", + "fcy;": "\u0444", + "female;": "\u2640", + "ffilig;": "\ufb03", + "fflig;": "\ufb00", + "ffllig;": "\ufb04", + "ffr;": "\U0001d523", + "filig;": "\ufb01", + "fjlig;": "fj", + "flat;": "\u266d", + "fllig;": "\ufb02", + "fltns;": "\u25b1", + "fnof;": "\u0192", + "fopf;": "\U0001d557", + "forall;": "\u2200", + "fork;": "\u22d4", + "forkv;": "\u2ad9", + "fpartint;": "\u2a0d", + "frac12": "\xbd", + "frac12;": "\xbd", + "frac13;": "\u2153", + "frac14": "\xbc", + "frac14;": "\xbc", + "frac15;": "\u2155", + "frac16;": "\u2159", + "frac18;": "\u215b", + "frac23;": "\u2154", + "frac25;": "\u2156", + "frac34": "\xbe", + "frac34;": "\xbe", + "frac35;": "\u2157", + "frac38;": "\u215c", + "frac45;": "\u2158", + "frac56;": "\u215a", + "frac58;": "\u215d", + "frac78;": "\u215e", + "frasl;": "\u2044", + "frown;": "\u2322", + "fscr;": "\U0001d4bb", + "gE;": "\u2267", + "gEl;": "\u2a8c", + "gacute;": "\u01f5", + "gamma;": "\u03b3", + "gammad;": "\u03dd", + "gap;": "\u2a86", + "gbreve;": "\u011f", + "gcirc;": "\u011d", + "gcy;": "\u0433", + "gdot;": "\u0121", + "ge;": "\u2265", + "gel;": "\u22db", + "geq;": "\u2265", + "geqq;": "\u2267", + "geqslant;": "\u2a7e", + "ges;": "\u2a7e", + "gescc;": "\u2aa9", + "gesdot;": "\u2a80", + "gesdoto;": "\u2a82", + "gesdotol;": "\u2a84", + "gesl;": "\u22db\ufe00", + "gesles;": "\u2a94", + "gfr;": "\U0001d524", + "gg;": "\u226b", + "ggg;": "\u22d9", + "gimel;": "\u2137", + "gjcy;": "\u0453", + "gl;": "\u2277", + "glE;": "\u2a92", + "gla;": "\u2aa5", + "glj;": "\u2aa4", + "gnE;": "\u2269", + "gnap;": "\u2a8a", + "gnapprox;": "\u2a8a", + "gne;": "\u2a88", + "gneq;": "\u2a88", + "gneqq;": "\u2269", + "gnsim;": "\u22e7", + "gopf;": "\U0001d558", + "grave;": "`", + "gscr;": "\u210a", + "gsim;": "\u2273", + "gsime;": "\u2a8e", + "gsiml;": "\u2a90", + "gt": ">", + "gt;": ">", + "gtcc;": "\u2aa7", + "gtcir;": "\u2a7a", + "gtdot;": "\u22d7", + "gtlPar;": "\u2995", + "gtquest;": "\u2a7c", + "gtrapprox;": "\u2a86", + "gtrarr;": "\u2978", + "gtrdot;": "\u22d7", + "gtreqless;": "\u22db", + "gtreqqless;": "\u2a8c", + "gtrless;": "\u2277", + "gtrsim;": "\u2273", + "gvertneqq;": "\u2269\ufe00", + "gvnE;": "\u2269\ufe00", + "hArr;": "\u21d4", + "hairsp;": "\u200a", + "half;": "\xbd", + "hamilt;": "\u210b", + "hardcy;": "\u044a", + "harr;": "\u2194", + "harrcir;": "\u2948", + "harrw;": "\u21ad", + "hbar;": "\u210f", + "hcirc;": "\u0125", + "hearts;": "\u2665", + "heartsuit;": "\u2665", + "hellip;": "\u2026", + "hercon;": "\u22b9", + "hfr;": "\U0001d525", + "hksearow;": "\u2925", + "hkswarow;": "\u2926", + "hoarr;": "\u21ff", + "homtht;": "\u223b", + "hookleftarrow;": "\u21a9", + "hookrightarrow;": "\u21aa", + "hopf;": "\U0001d559", + "horbar;": "\u2015", + "hscr;": "\U0001d4bd", + "hslash;": "\u210f", + "hstrok;": "\u0127", + "hybull;": "\u2043", + "hyphen;": "\u2010", + "iacute": "\xed", + "iacute;": "\xed", + "ic;": "\u2063", + "icirc": "\xee", + "icirc;": "\xee", + "icy;": "\u0438", + "iecy;": "\u0435", + "iexcl": "\xa1", + "iexcl;": "\xa1", + "iff;": "\u21d4", + "ifr;": "\U0001d526", + "igrave": "\xec", + "igrave;": "\xec", + "ii;": "\u2148", + "iiiint;": "\u2a0c", + "iiint;": "\u222d", + "iinfin;": "\u29dc", + "iiota;": "\u2129", + "ijlig;": "\u0133", + "imacr;": "\u012b", + "image;": "\u2111", + "imagline;": "\u2110", + "imagpart;": "\u2111", + "imath;": "\u0131", + "imof;": "\u22b7", + "imped;": "\u01b5", + "in;": "\u2208", + "incare;": "\u2105", + "infin;": "\u221e", + "infintie;": "\u29dd", + "inodot;": "\u0131", + "int;": "\u222b", + "intcal;": "\u22ba", + "integers;": "\u2124", + "intercal;": "\u22ba", + "intlarhk;": "\u2a17", + "intprod;": "\u2a3c", + "iocy;": "\u0451", + "iogon;": "\u012f", + "iopf;": "\U0001d55a", + "iota;": "\u03b9", + "iprod;": "\u2a3c", + "iquest": "\xbf", + "iquest;": "\xbf", + "iscr;": "\U0001d4be", + "isin;": "\u2208", + "isinE;": "\u22f9", + "isindot;": "\u22f5", + "isins;": "\u22f4", + "isinsv;": "\u22f3", + "isinv;": "\u2208", + "it;": "\u2062", + "itilde;": "\u0129", + "iukcy;": "\u0456", + "iuml": "\xef", + "iuml;": "\xef", + "jcirc;": "\u0135", + "jcy;": "\u0439", + "jfr;": "\U0001d527", + "jmath;": "\u0237", + "jopf;": "\U0001d55b", + "jscr;": "\U0001d4bf", + "jsercy;": "\u0458", + "jukcy;": "\u0454", + "kappa;": "\u03ba", + "kappav;": "\u03f0", + "kcedil;": "\u0137", + "kcy;": "\u043a", + "kfr;": "\U0001d528", + "kgreen;": "\u0138", + "khcy;": "\u0445", + "kjcy;": "\u045c", + "kopf;": "\U0001d55c", + "kscr;": "\U0001d4c0", + "lAarr;": "\u21da", + "lArr;": "\u21d0", + "lAtail;": "\u291b", + "lBarr;": "\u290e", + "lE;": "\u2266", + "lEg;": "\u2a8b", + "lHar;": "\u2962", + "lacute;": "\u013a", + "laemptyv;": "\u29b4", + "lagran;": "\u2112", + "lambda;": "\u03bb", + "lang;": "\u27e8", + "langd;": "\u2991", + "langle;": "\u27e8", + "lap;": "\u2a85", + "laquo": "\xab", + "laquo;": "\xab", + "larr;": "\u2190", + "larrb;": "\u21e4", + "larrbfs;": "\u291f", + "larrfs;": "\u291d", + "larrhk;": "\u21a9", + "larrlp;": "\u21ab", + "larrpl;": "\u2939", + "larrsim;": "\u2973", + "larrtl;": "\u21a2", + "lat;": "\u2aab", + "latail;": "\u2919", + "late;": "\u2aad", + "lates;": "\u2aad\ufe00", + "lbarr;": "\u290c", + "lbbrk;": "\u2772", + "lbrace;": "{", + "lbrack;": "[", + "lbrke;": "\u298b", + "lbrksld;": "\u298f", + "lbrkslu;": "\u298d", + "lcaron;": "\u013e", + "lcedil;": "\u013c", + "lceil;": "\u2308", + "lcub;": "{", + "lcy;": "\u043b", + "ldca;": "\u2936", + "ldquo;": "\u201c", + "ldquor;": "\u201e", + "ldrdhar;": "\u2967", + "ldrushar;": "\u294b", + "ldsh;": "\u21b2", + "le;": "\u2264", + "leftarrow;": "\u2190", + "leftarrowtail;": "\u21a2", + "leftharpoondown;": "\u21bd", + "leftharpoonup;": "\u21bc", + "leftleftarrows;": "\u21c7", + "leftrightarrow;": "\u2194", + "leftrightarrows;": "\u21c6", + "leftrightharpoons;": "\u21cb", + "leftrightsquigarrow;": "\u21ad", + "leftthreetimes;": "\u22cb", + "leg;": "\u22da", + "leq;": "\u2264", + "leqq;": "\u2266", + "leqslant;": "\u2a7d", + "les;": "\u2a7d", + "lescc;": "\u2aa8", + "lesdot;": "\u2a7f", + "lesdoto;": "\u2a81", + "lesdotor;": "\u2a83", + "lesg;": "\u22da\ufe00", + "lesges;": "\u2a93", + "lessapprox;": "\u2a85", + "lessdot;": "\u22d6", + "lesseqgtr;": "\u22da", + "lesseqqgtr;": "\u2a8b", + "lessgtr;": "\u2276", + "lesssim;": "\u2272", + "lfisht;": "\u297c", + "lfloor;": "\u230a", + "lfr;": "\U0001d529", + "lg;": "\u2276", + "lgE;": "\u2a91", + "lhard;": "\u21bd", + "lharu;": "\u21bc", + "lharul;": "\u296a", + "lhblk;": "\u2584", + "ljcy;": "\u0459", + "ll;": "\u226a", + "llarr;": "\u21c7", + "llcorner;": "\u231e", + "llhard;": "\u296b", + "lltri;": "\u25fa", + "lmidot;": "\u0140", + "lmoust;": "\u23b0", + "lmoustache;": "\u23b0", + "lnE;": "\u2268", + "lnap;": "\u2a89", + "lnapprox;": "\u2a89", + "lne;": "\u2a87", + "lneq;": "\u2a87", + "lneqq;": "\u2268", + "lnsim;": "\u22e6", + "loang;": "\u27ec", + "loarr;": "\u21fd", + "lobrk;": "\u27e6", + "longleftarrow;": "\u27f5", + "longleftrightarrow;": "\u27f7", + "longmapsto;": "\u27fc", + "longrightarrow;": "\u27f6", + "looparrowleft;": "\u21ab", + "looparrowright;": "\u21ac", + "lopar;": "\u2985", + "lopf;": "\U0001d55d", + "loplus;": "\u2a2d", + "lotimes;": "\u2a34", + "lowast;": "\u2217", + "lowbar;": "_", + "loz;": "\u25ca", + "lozenge;": "\u25ca", + "lozf;": "\u29eb", + "lpar;": "(", + "lparlt;": "\u2993", + "lrarr;": "\u21c6", + "lrcorner;": "\u231f", + "lrhar;": "\u21cb", + "lrhard;": "\u296d", + "lrm;": "\u200e", + "lrtri;": "\u22bf", + "lsaquo;": "\u2039", + "lscr;": "\U0001d4c1", + "lsh;": "\u21b0", + "lsim;": "\u2272", + "lsime;": "\u2a8d", + "lsimg;": "\u2a8f", + "lsqb;": "[", + "lsquo;": "\u2018", + "lsquor;": "\u201a", + "lstrok;": "\u0142", + "lt": "<", + "lt;": "<", + "ltcc;": "\u2aa6", + "ltcir;": "\u2a79", + "ltdot;": "\u22d6", + "lthree;": "\u22cb", + "ltimes;": "\u22c9", + "ltlarr;": "\u2976", + "ltquest;": "\u2a7b", + "ltrPar;": "\u2996", + "ltri;": "\u25c3", + "ltrie;": "\u22b4", + "ltrif;": "\u25c2", + "lurdshar;": "\u294a", + "luruhar;": "\u2966", + "lvertneqq;": "\u2268\ufe00", + "lvnE;": "\u2268\ufe00", + "mDDot;": "\u223a", + "macr": "\xaf", + "macr;": "\xaf", + "male;": "\u2642", + "malt;": "\u2720", + "maltese;": "\u2720", + "map;": "\u21a6", + "mapsto;": "\u21a6", + "mapstodown;": "\u21a7", + "mapstoleft;": "\u21a4", + "mapstoup;": "\u21a5", + "marker;": "\u25ae", + "mcomma;": "\u2a29", + "mcy;": "\u043c", + "mdash;": "\u2014", + "measuredangle;": "\u2221", + "mfr;": "\U0001d52a", + "mho;": "\u2127", + "micro": "\xb5", + "micro;": "\xb5", + "mid;": "\u2223", + "midast;": "*", + "midcir;": "\u2af0", + "middot": "\xb7", + "middot;": "\xb7", + "minus;": "\u2212", + "minusb;": "\u229f", + "minusd;": "\u2238", + "minusdu;": "\u2a2a", + "mlcp;": "\u2adb", + "mldr;": "\u2026", + "mnplus;": "\u2213", + "models;": "\u22a7", + "mopf;": "\U0001d55e", + "mp;": "\u2213", + "mscr;": "\U0001d4c2", + "mstpos;": "\u223e", + "mu;": "\u03bc", + "multimap;": "\u22b8", + "mumap;": "\u22b8", + "nGg;": "\u22d9\u0338", + "nGt;": "\u226b\u20d2", + "nGtv;": "\u226b\u0338", + "nLeftarrow;": "\u21cd", + "nLeftrightarrow;": "\u21ce", + "nLl;": "\u22d8\u0338", + "nLt;": "\u226a\u20d2", + "nLtv;": "\u226a\u0338", + "nRightarrow;": "\u21cf", + "nVDash;": "\u22af", + "nVdash;": "\u22ae", + "nabla;": "\u2207", + "nacute;": "\u0144", + "nang;": "\u2220\u20d2", + "nap;": "\u2249", + "napE;": "\u2a70\u0338", + "napid;": "\u224b\u0338", + "napos;": "\u0149", + "napprox;": "\u2249", + "natur;": "\u266e", + "natural;": "\u266e", + "naturals;": "\u2115", + "nbsp": "\xa0", + "nbsp;": "\xa0", + "nbump;": "\u224e\u0338", + "nbumpe;": "\u224f\u0338", + "ncap;": "\u2a43", + "ncaron;": "\u0148", + "ncedil;": "\u0146", + "ncong;": "\u2247", + "ncongdot;": "\u2a6d\u0338", + "ncup;": "\u2a42", + "ncy;": "\u043d", + "ndash;": "\u2013", + "ne;": "\u2260", + "neArr;": "\u21d7", + "nearhk;": "\u2924", + "nearr;": "\u2197", + "nearrow;": "\u2197", + "nedot;": "\u2250\u0338", + "nequiv;": "\u2262", + "nesear;": "\u2928", + "nesim;": "\u2242\u0338", + "nexist;": "\u2204", + "nexists;": "\u2204", + "nfr;": "\U0001d52b", + "ngE;": "\u2267\u0338", + "nge;": "\u2271", + "ngeq;": "\u2271", + "ngeqq;": "\u2267\u0338", + "ngeqslant;": "\u2a7e\u0338", + "nges;": "\u2a7e\u0338", + "ngsim;": "\u2275", + "ngt;": "\u226f", + "ngtr;": "\u226f", + "nhArr;": "\u21ce", + "nharr;": "\u21ae", + "nhpar;": "\u2af2", + "ni;": "\u220b", + "nis;": "\u22fc", + "nisd;": "\u22fa", + "niv;": "\u220b", + "njcy;": "\u045a", + "nlArr;": "\u21cd", + "nlE;": "\u2266\u0338", + "nlarr;": "\u219a", + "nldr;": "\u2025", + "nle;": "\u2270", + "nleftarrow;": "\u219a", + "nleftrightarrow;": "\u21ae", + "nleq;": "\u2270", + "nleqq;": "\u2266\u0338", + "nleqslant;": "\u2a7d\u0338", + "nles;": "\u2a7d\u0338", + "nless;": "\u226e", + "nlsim;": "\u2274", + "nlt;": "\u226e", + "nltri;": "\u22ea", + "nltrie;": "\u22ec", + "nmid;": "\u2224", + "nopf;": "\U0001d55f", + "not": "\xac", + "not;": "\xac", + "notin;": "\u2209", + "notinE;": "\u22f9\u0338", + "notindot;": "\u22f5\u0338", + "notinva;": "\u2209", + "notinvb;": "\u22f7", + "notinvc;": "\u22f6", + "notni;": "\u220c", + "notniva;": "\u220c", + "notnivb;": "\u22fe", + "notnivc;": "\u22fd", + "npar;": "\u2226", + "nparallel;": "\u2226", + "nparsl;": "\u2afd\u20e5", + "npart;": "\u2202\u0338", + "npolint;": "\u2a14", + "npr;": "\u2280", + "nprcue;": "\u22e0", + "npre;": "\u2aaf\u0338", + "nprec;": "\u2280", + "npreceq;": "\u2aaf\u0338", + "nrArr;": "\u21cf", + "nrarr;": "\u219b", + "nrarrc;": "\u2933\u0338", + "nrarrw;": "\u219d\u0338", + "nrightarrow;": "\u219b", + "nrtri;": "\u22eb", + "nrtrie;": "\u22ed", + "nsc;": "\u2281", + "nsccue;": "\u22e1", + "nsce;": "\u2ab0\u0338", + "nscr;": "\U0001d4c3", + "nshortmid;": "\u2224", + "nshortparallel;": "\u2226", + "nsim;": "\u2241", + "nsime;": "\u2244", + "nsimeq;": "\u2244", + "nsmid;": "\u2224", + "nspar;": "\u2226", + "nsqsube;": "\u22e2", + "nsqsupe;": "\u22e3", + "nsub;": "\u2284", + "nsubE;": "\u2ac5\u0338", + "nsube;": "\u2288", + "nsubset;": "\u2282\u20d2", + "nsubseteq;": "\u2288", + "nsubseteqq;": "\u2ac5\u0338", + "nsucc;": "\u2281", + "nsucceq;": "\u2ab0\u0338", + "nsup;": "\u2285", + "nsupE;": "\u2ac6\u0338", + "nsupe;": "\u2289", + "nsupset;": "\u2283\u20d2", + "nsupseteq;": "\u2289", + "nsupseteqq;": "\u2ac6\u0338", + "ntgl;": "\u2279", + "ntilde": "\xf1", + "ntilde;": "\xf1", + "ntlg;": "\u2278", + "ntriangleleft;": "\u22ea", + "ntrianglelefteq;": "\u22ec", + "ntriangleright;": "\u22eb", + "ntrianglerighteq;": "\u22ed", + "nu;": "\u03bd", + "num;": "#", + "numero;": "\u2116", + "numsp;": "\u2007", + "nvDash;": "\u22ad", + "nvHarr;": "\u2904", + "nvap;": "\u224d\u20d2", + "nvdash;": "\u22ac", + "nvge;": "\u2265\u20d2", + "nvgt;": ">\u20d2", + "nvinfin;": "\u29de", + "nvlArr;": "\u2902", + "nvle;": "\u2264\u20d2", + "nvlt;": "<\u20d2", + "nvltrie;": "\u22b4\u20d2", + "nvrArr;": "\u2903", + "nvrtrie;": "\u22b5\u20d2", + "nvsim;": "\u223c\u20d2", + "nwArr;": "\u21d6", + "nwarhk;": "\u2923", + "nwarr;": "\u2196", + "nwarrow;": "\u2196", + "nwnear;": "\u2927", + "oS;": "\u24c8", + "oacute": "\xf3", + "oacute;": "\xf3", + "oast;": "\u229b", + "ocir;": "\u229a", + "ocirc": "\xf4", + "ocirc;": "\xf4", + "ocy;": "\u043e", + "odash;": "\u229d", + "odblac;": "\u0151", + "odiv;": "\u2a38", + "odot;": "\u2299", + "odsold;": "\u29bc", + "oelig;": "\u0153", + "ofcir;": "\u29bf", + "ofr;": "\U0001d52c", + "ogon;": "\u02db", + "ograve": "\xf2", + "ograve;": "\xf2", + "ogt;": "\u29c1", + "ohbar;": "\u29b5", + "ohm;": "\u03a9", + "oint;": "\u222e", + "olarr;": "\u21ba", + "olcir;": "\u29be", + "olcross;": "\u29bb", + "oline;": "\u203e", + "olt;": "\u29c0", + "omacr;": "\u014d", + "omega;": "\u03c9", + "omicron;": "\u03bf", + "omid;": "\u29b6", + "ominus;": "\u2296", + "oopf;": "\U0001d560", + "opar;": "\u29b7", + "operp;": "\u29b9", + "oplus;": "\u2295", + "or;": "\u2228", + "orarr;": "\u21bb", + "ord;": "\u2a5d", + "order;": "\u2134", + "orderof;": "\u2134", + "ordf": "\xaa", + "ordf;": "\xaa", + "ordm": "\xba", + "ordm;": "\xba", + "origof;": "\u22b6", + "oror;": "\u2a56", + "orslope;": "\u2a57", + "orv;": "\u2a5b", + "oscr;": "\u2134", + "oslash": "\xf8", + "oslash;": "\xf8", + "osol;": "\u2298", + "otilde": "\xf5", + "otilde;": "\xf5", + "otimes;": "\u2297", + "otimesas;": "\u2a36", + "ouml": "\xf6", + "ouml;": "\xf6", + "ovbar;": "\u233d", + "par;": "\u2225", + "para": "\xb6", + "para;": "\xb6", + "parallel;": "\u2225", + "parsim;": "\u2af3", + "parsl;": "\u2afd", + "part;": "\u2202", + "pcy;": "\u043f", + "percnt;": "%", + "period;": ".", + "permil;": "\u2030", + "perp;": "\u22a5", + "pertenk;": "\u2031", + "pfr;": "\U0001d52d", + "phi;": "\u03c6", + "phiv;": "\u03d5", + "phmmat;": "\u2133", + "phone;": "\u260e", + "pi;": "\u03c0", + "pitchfork;": "\u22d4", + "piv;": "\u03d6", + "planck;": "\u210f", + "planckh;": "\u210e", + "plankv;": "\u210f", + "plus;": "+", + "plusacir;": "\u2a23", + "plusb;": "\u229e", + "pluscir;": "\u2a22", + "plusdo;": "\u2214", + "plusdu;": "\u2a25", + "pluse;": "\u2a72", + "plusmn": "\xb1", + "plusmn;": "\xb1", + "plussim;": "\u2a26", + "plustwo;": "\u2a27", + "pm;": "\xb1", + "pointint;": "\u2a15", + "popf;": "\U0001d561", + "pound": "\xa3", + "pound;": "\xa3", + "pr;": "\u227a", + "prE;": "\u2ab3", + "prap;": "\u2ab7", + "prcue;": "\u227c", + "pre;": "\u2aaf", + "prec;": "\u227a", + "precapprox;": "\u2ab7", + "preccurlyeq;": "\u227c", + "preceq;": "\u2aaf", + "precnapprox;": "\u2ab9", + "precneqq;": "\u2ab5", + "precnsim;": "\u22e8", + "precsim;": "\u227e", + "prime;": "\u2032", + "primes;": "\u2119", + "prnE;": "\u2ab5", + "prnap;": "\u2ab9", + "prnsim;": "\u22e8", + "prod;": "\u220f", + "profalar;": "\u232e", + "profline;": "\u2312", + "profsurf;": "\u2313", + "prop;": "\u221d", + "propto;": "\u221d", + "prsim;": "\u227e", + "prurel;": "\u22b0", + "pscr;": "\U0001d4c5", + "psi;": "\u03c8", + "puncsp;": "\u2008", + "qfr;": "\U0001d52e", + "qint;": "\u2a0c", + "qopf;": "\U0001d562", + "qprime;": "\u2057", + "qscr;": "\U0001d4c6", + "quaternions;": "\u210d", + "quatint;": "\u2a16", + "quest;": "?", + "questeq;": "\u225f", + "quot": "\"", + "quot;": "\"", + "rAarr;": "\u21db", + "rArr;": "\u21d2", + "rAtail;": "\u291c", + "rBarr;": "\u290f", + "rHar;": "\u2964", + "race;": "\u223d\u0331", + "racute;": "\u0155", + "radic;": "\u221a", + "raemptyv;": "\u29b3", + "rang;": "\u27e9", + "rangd;": "\u2992", + "range;": "\u29a5", + "rangle;": "\u27e9", + "raquo": "\xbb", + "raquo;": "\xbb", + "rarr;": "\u2192", + "rarrap;": "\u2975", + "rarrb;": "\u21e5", + "rarrbfs;": "\u2920", + "rarrc;": "\u2933", + "rarrfs;": "\u291e", + "rarrhk;": "\u21aa", + "rarrlp;": "\u21ac", + "rarrpl;": "\u2945", + "rarrsim;": "\u2974", + "rarrtl;": "\u21a3", + "rarrw;": "\u219d", + "ratail;": "\u291a", + "ratio;": "\u2236", + "rationals;": "\u211a", + "rbarr;": "\u290d", + "rbbrk;": "\u2773", + "rbrace;": "}", + "rbrack;": "]", + "rbrke;": "\u298c", + "rbrksld;": "\u298e", + "rbrkslu;": "\u2990", + "rcaron;": "\u0159", + "rcedil;": "\u0157", + "rceil;": "\u2309", + "rcub;": "}", + "rcy;": "\u0440", + "rdca;": "\u2937", + "rdldhar;": "\u2969", + "rdquo;": "\u201d", + "rdquor;": "\u201d", + "rdsh;": "\u21b3", + "real;": "\u211c", + "realine;": "\u211b", + "realpart;": "\u211c", + "reals;": "\u211d", + "rect;": "\u25ad", + "reg": "\xae", + "reg;": "\xae", + "rfisht;": "\u297d", + "rfloor;": "\u230b", + "rfr;": "\U0001d52f", + "rhard;": "\u21c1", + "rharu;": "\u21c0", + "rharul;": "\u296c", + "rho;": "\u03c1", + "rhov;": "\u03f1", + "rightarrow;": "\u2192", + "rightarrowtail;": "\u21a3", + "rightharpoondown;": "\u21c1", + "rightharpoonup;": "\u21c0", + "rightleftarrows;": "\u21c4", + "rightleftharpoons;": "\u21cc", + "rightrightarrows;": "\u21c9", + "rightsquigarrow;": "\u219d", + "rightthreetimes;": "\u22cc", + "ring;": "\u02da", + "risingdotseq;": "\u2253", + "rlarr;": "\u21c4", + "rlhar;": "\u21cc", + "rlm;": "\u200f", + "rmoust;": "\u23b1", + "rmoustache;": "\u23b1", + "rnmid;": "\u2aee", + "roang;": "\u27ed", + "roarr;": "\u21fe", + "robrk;": "\u27e7", + "ropar;": "\u2986", + "ropf;": "\U0001d563", + "roplus;": "\u2a2e", + "rotimes;": "\u2a35", + "rpar;": ")", + "rpargt;": "\u2994", + "rppolint;": "\u2a12", + "rrarr;": "\u21c9", + "rsaquo;": "\u203a", + "rscr;": "\U0001d4c7", + "rsh;": "\u21b1", + "rsqb;": "]", + "rsquo;": "\u2019", + "rsquor;": "\u2019", + "rthree;": "\u22cc", + "rtimes;": "\u22ca", + "rtri;": "\u25b9", + "rtrie;": "\u22b5", + "rtrif;": "\u25b8", + "rtriltri;": "\u29ce", + "ruluhar;": "\u2968", + "rx;": "\u211e", + "sacute;": "\u015b", + "sbquo;": "\u201a", + "sc;": "\u227b", + "scE;": "\u2ab4", + "scap;": "\u2ab8", + "scaron;": "\u0161", + "sccue;": "\u227d", + "sce;": "\u2ab0", + "scedil;": "\u015f", + "scirc;": "\u015d", + "scnE;": "\u2ab6", + "scnap;": "\u2aba", + "scnsim;": "\u22e9", + "scpolint;": "\u2a13", + "scsim;": "\u227f", + "scy;": "\u0441", + "sdot;": "\u22c5", + "sdotb;": "\u22a1", + "sdote;": "\u2a66", + "seArr;": "\u21d8", + "searhk;": "\u2925", + "searr;": "\u2198", + "searrow;": "\u2198", + "sect": "\xa7", + "sect;": "\xa7", + "semi;": ";", + "seswar;": "\u2929", + "setminus;": "\u2216", + "setmn;": "\u2216", + "sext;": "\u2736", + "sfr;": "\U0001d530", + "sfrown;": "\u2322", + "sharp;": "\u266f", + "shchcy;": "\u0449", + "shcy;": "\u0448", + "shortmid;": "\u2223", + "shortparallel;": "\u2225", + "shy": "\xad", + "shy;": "\xad", + "sigma;": "\u03c3", + "sigmaf;": "\u03c2", + "sigmav;": "\u03c2", + "sim;": "\u223c", + "simdot;": "\u2a6a", + "sime;": "\u2243", + "simeq;": "\u2243", + "simg;": "\u2a9e", + "simgE;": "\u2aa0", + "siml;": "\u2a9d", + "simlE;": "\u2a9f", + "simne;": "\u2246", + "simplus;": "\u2a24", + "simrarr;": "\u2972", + "slarr;": "\u2190", + "smallsetminus;": "\u2216", + "smashp;": "\u2a33", + "smeparsl;": "\u29e4", + "smid;": "\u2223", + "smile;": "\u2323", + "smt;": "\u2aaa", + "smte;": "\u2aac", + "smtes;": "\u2aac\ufe00", + "softcy;": "\u044c", + "sol;": "/", + "solb;": "\u29c4", + "solbar;": "\u233f", + "sopf;": "\U0001d564", + "spades;": "\u2660", + "spadesuit;": "\u2660", + "spar;": "\u2225", + "sqcap;": "\u2293", + "sqcaps;": "\u2293\ufe00", + "sqcup;": "\u2294", + "sqcups;": "\u2294\ufe00", + "sqsub;": "\u228f", + "sqsube;": "\u2291", + "sqsubset;": "\u228f", + "sqsubseteq;": "\u2291", + "sqsup;": "\u2290", + "sqsupe;": "\u2292", + "sqsupset;": "\u2290", + "sqsupseteq;": "\u2292", + "squ;": "\u25a1", + "square;": "\u25a1", + "squarf;": "\u25aa", + "squf;": "\u25aa", + "srarr;": "\u2192", + "sscr;": "\U0001d4c8", + "ssetmn;": "\u2216", + "ssmile;": "\u2323", + "sstarf;": "\u22c6", + "star;": "\u2606", + "starf;": "\u2605", + "straightepsilon;": "\u03f5", + "straightphi;": "\u03d5", + "strns;": "\xaf", + "sub;": "\u2282", + "subE;": "\u2ac5", + "subdot;": "\u2abd", + "sube;": "\u2286", + "subedot;": "\u2ac3", + "submult;": "\u2ac1", + "subnE;": "\u2acb", + "subne;": "\u228a", + "subplus;": "\u2abf", + "subrarr;": "\u2979", + "subset;": "\u2282", + "subseteq;": "\u2286", + "subseteqq;": "\u2ac5", + "subsetneq;": "\u228a", + "subsetneqq;": "\u2acb", + "subsim;": "\u2ac7", + "subsub;": "\u2ad5", + "subsup;": "\u2ad3", + "succ;": "\u227b", + "succapprox;": "\u2ab8", + "succcurlyeq;": "\u227d", + "succeq;": "\u2ab0", + "succnapprox;": "\u2aba", + "succneqq;": "\u2ab6", + "succnsim;": "\u22e9", + "succsim;": "\u227f", + "sum;": "\u2211", + "sung;": "\u266a", + "sup1": "\xb9", + "sup1;": "\xb9", + "sup2": "\xb2", + "sup2;": "\xb2", + "sup3": "\xb3", + "sup3;": "\xb3", + "sup;": "\u2283", + "supE;": "\u2ac6", + "supdot;": "\u2abe", + "supdsub;": "\u2ad8", + "supe;": "\u2287", + "supedot;": "\u2ac4", + "suphsol;": "\u27c9", + "suphsub;": "\u2ad7", + "suplarr;": "\u297b", + "supmult;": "\u2ac2", + "supnE;": "\u2acc", + "supne;": "\u228b", + "supplus;": "\u2ac0", + "supset;": "\u2283", + "supseteq;": "\u2287", + "supseteqq;": "\u2ac6", + "supsetneq;": "\u228b", + "supsetneqq;": "\u2acc", + "supsim;": "\u2ac8", + "supsub;": "\u2ad4", + "supsup;": "\u2ad6", + "swArr;": "\u21d9", + "swarhk;": "\u2926", + "swarr;": "\u2199", + "swarrow;": "\u2199", + "swnwar;": "\u292a", + "szlig": "\xdf", + "szlig;": "\xdf", + "target;": "\u2316", + "tau;": "\u03c4", + "tbrk;": "\u23b4", + "tcaron;": "\u0165", + "tcedil;": "\u0163", + "tcy;": "\u0442", + "tdot;": "\u20db", + "telrec;": "\u2315", + "tfr;": "\U0001d531", + "there4;": "\u2234", + "therefore;": "\u2234", + "theta;": "\u03b8", + "thetasym;": "\u03d1", + "thetav;": "\u03d1", + "thickapprox;": "\u2248", + "thicksim;": "\u223c", + "thinsp;": "\u2009", + "thkap;": "\u2248", + "thksim;": "\u223c", + "thorn": "\xfe", + "thorn;": "\xfe", + "tilde;": "\u02dc", + "times": "\xd7", + "times;": "\xd7", + "timesb;": "\u22a0", + "timesbar;": "\u2a31", + "timesd;": "\u2a30", + "tint;": "\u222d", + "toea;": "\u2928", + "top;": "\u22a4", + "topbot;": "\u2336", + "topcir;": "\u2af1", + "topf;": "\U0001d565", + "topfork;": "\u2ada", + "tosa;": "\u2929", + "tprime;": "\u2034", + "trade;": "\u2122", + "triangle;": "\u25b5", + "triangledown;": "\u25bf", + "triangleleft;": "\u25c3", + "trianglelefteq;": "\u22b4", + "triangleq;": "\u225c", + "triangleright;": "\u25b9", + "trianglerighteq;": "\u22b5", + "tridot;": "\u25ec", + "trie;": "\u225c", + "triminus;": "\u2a3a", + "triplus;": "\u2a39", + "trisb;": "\u29cd", + "tritime;": "\u2a3b", + "trpezium;": "\u23e2", + "tscr;": "\U0001d4c9", + "tscy;": "\u0446", + "tshcy;": "\u045b", + "tstrok;": "\u0167", + "twixt;": "\u226c", + "twoheadleftarrow;": "\u219e", + "twoheadrightarrow;": "\u21a0", + "uArr;": "\u21d1", + "uHar;": "\u2963", + "uacute": "\xfa", + "uacute;": "\xfa", + "uarr;": "\u2191", + "ubrcy;": "\u045e", + "ubreve;": "\u016d", + "ucirc": "\xfb", + "ucirc;": "\xfb", + "ucy;": "\u0443", + "udarr;": "\u21c5", + "udblac;": "\u0171", + "udhar;": "\u296e", + "ufisht;": "\u297e", + "ufr;": "\U0001d532", + "ugrave": "\xf9", + "ugrave;": "\xf9", + "uharl;": "\u21bf", + "uharr;": "\u21be", + "uhblk;": "\u2580", + "ulcorn;": "\u231c", + "ulcorner;": "\u231c", + "ulcrop;": "\u230f", + "ultri;": "\u25f8", + "umacr;": "\u016b", + "uml": "\xa8", + "uml;": "\xa8", + "uogon;": "\u0173", + "uopf;": "\U0001d566", + "uparrow;": "\u2191", + "updownarrow;": "\u2195", + "upharpoonleft;": "\u21bf", + "upharpoonright;": "\u21be", + "uplus;": "\u228e", + "upsi;": "\u03c5", + "upsih;": "\u03d2", + "upsilon;": "\u03c5", + "upuparrows;": "\u21c8", + "urcorn;": "\u231d", + "urcorner;": "\u231d", + "urcrop;": "\u230e", + "uring;": "\u016f", + "urtri;": "\u25f9", + "uscr;": "\U0001d4ca", + "utdot;": "\u22f0", + "utilde;": "\u0169", + "utri;": "\u25b5", + "utrif;": "\u25b4", + "uuarr;": "\u21c8", + "uuml": "\xfc", + "uuml;": "\xfc", + "uwangle;": "\u29a7", + "vArr;": "\u21d5", + "vBar;": "\u2ae8", + "vBarv;": "\u2ae9", + "vDash;": "\u22a8", + "vangrt;": "\u299c", + "varepsilon;": "\u03f5", + "varkappa;": "\u03f0", + "varnothing;": "\u2205", + "varphi;": "\u03d5", + "varpi;": "\u03d6", + "varpropto;": "\u221d", + "varr;": "\u2195", + "varrho;": "\u03f1", + "varsigma;": "\u03c2", + "varsubsetneq;": "\u228a\ufe00", + "varsubsetneqq;": "\u2acb\ufe00", + "varsupsetneq;": "\u228b\ufe00", + "varsupsetneqq;": "\u2acc\ufe00", + "vartheta;": "\u03d1", + "vartriangleleft;": "\u22b2", + "vartriangleright;": "\u22b3", + "vcy;": "\u0432", + "vdash;": "\u22a2", + "vee;": "\u2228", + "veebar;": "\u22bb", + "veeeq;": "\u225a", + "vellip;": "\u22ee", + "verbar;": "|", + "vert;": "|", + "vfr;": "\U0001d533", + "vltri;": "\u22b2", + "vnsub;": "\u2282\u20d2", + "vnsup;": "\u2283\u20d2", + "vopf;": "\U0001d567", + "vprop;": "\u221d", + "vrtri;": "\u22b3", + "vscr;": "\U0001d4cb", + "vsubnE;": "\u2acb\ufe00", + "vsubne;": "\u228a\ufe00", + "vsupnE;": "\u2acc\ufe00", + "vsupne;": "\u228b\ufe00", + "vzigzag;": "\u299a", + "wcirc;": "\u0175", + "wedbar;": "\u2a5f", + "wedge;": "\u2227", + "wedgeq;": "\u2259", + "weierp;": "\u2118", + "wfr;": "\U0001d534", + "wopf;": "\U0001d568", + "wp;": "\u2118", + "wr;": "\u2240", + "wreath;": "\u2240", + "wscr;": "\U0001d4cc", + "xcap;": "\u22c2", + "xcirc;": "\u25ef", + "xcup;": "\u22c3", + "xdtri;": "\u25bd", + "xfr;": "\U0001d535", + "xhArr;": "\u27fa", + "xharr;": "\u27f7", + "xi;": "\u03be", + "xlArr;": "\u27f8", + "xlarr;": "\u27f5", + "xmap;": "\u27fc", + "xnis;": "\u22fb", + "xodot;": "\u2a00", + "xopf;": "\U0001d569", + "xoplus;": "\u2a01", + "xotime;": "\u2a02", + "xrArr;": "\u27f9", + "xrarr;": "\u27f6", + "xscr;": "\U0001d4cd", + "xsqcup;": "\u2a06", + "xuplus;": "\u2a04", + "xutri;": "\u25b3", + "xvee;": "\u22c1", + "xwedge;": "\u22c0", + "yacute": "\xfd", + "yacute;": "\xfd", + "yacy;": "\u044f", + "ycirc;": "\u0177", + "ycy;": "\u044b", + "yen": "\xa5", + "yen;": "\xa5", + "yfr;": "\U0001d536", + "yicy;": "\u0457", + "yopf;": "\U0001d56a", + "yscr;": "\U0001d4ce", + "yucy;": "\u044e", + "yuml": "\xff", + "yuml;": "\xff", + "zacute;": "\u017a", + "zcaron;": "\u017e", + "zcy;": "\u0437", + "zdot;": "\u017c", + "zeetrf;": "\u2128", + "zeta;": "\u03b6", + "zfr;": "\U0001d537", + "zhcy;": "\u0436", + "zigrarr;": "\u21dd", + "zopf;": "\U0001d56b", + "zscr;": "\U0001d4cf", + "zwj;": "\u200d", + "zwnj;": "\u200c", +} + +replacementCharacters = { + 0x0: "\uFFFD", + 0x0d: "\u000D", + 0x80: "\u20AC", + 0x81: "\u0081", + 0x81: "\u0081", + 0x82: "\u201A", + 0x83: "\u0192", + 0x84: "\u201E", + 0x85: "\u2026", + 0x86: "\u2020", + 0x87: "\u2021", + 0x88: "\u02C6", + 0x89: "\u2030", + 0x8A: "\u0160", + 0x8B: "\u2039", + 0x8C: "\u0152", + 0x8D: "\u008D", + 0x8E: "\u017D", + 0x8F: "\u008F", + 0x90: "\u0090", + 0x91: "\u2018", + 0x92: "\u2019", + 0x93: "\u201C", + 0x94: "\u201D", + 0x95: "\u2022", + 0x96: "\u2013", + 0x97: "\u2014", + 0x98: "\u02DC", + 0x99: "\u2122", + 0x9A: "\u0161", + 0x9B: "\u203A", + 0x9C: "\u0153", + 0x9D: "\u009D", + 0x9E: "\u017E", + 0x9F: "\u0178", +} + +encodings = { + '437': 'cp437', + '850': 'cp850', + '852': 'cp852', + '855': 'cp855', + '857': 'cp857', + '860': 'cp860', + '861': 'cp861', + '862': 'cp862', + '863': 'cp863', + '865': 'cp865', + '866': 'cp866', + '869': 'cp869', + 'ansix341968': 'ascii', + 'ansix341986': 'ascii', + 'arabic': 'iso8859-6', + 'ascii': 'ascii', + 'asmo708': 'iso8859-6', + 'big5': 'big5', + 'big5hkscs': 'big5hkscs', + 'chinese': 'gbk', + 'cp037': 'cp037', + 'cp1026': 'cp1026', + 'cp154': 'ptcp154', + 'cp367': 'ascii', + 'cp424': 'cp424', + 'cp437': 'cp437', + 'cp500': 'cp500', + 'cp775': 'cp775', + 'cp819': 'windows-1252', + 'cp850': 'cp850', + 'cp852': 'cp852', + 'cp855': 'cp855', + 'cp857': 'cp857', + 'cp860': 'cp860', + 'cp861': 'cp861', + 'cp862': 'cp862', + 'cp863': 'cp863', + 'cp864': 'cp864', + 'cp865': 'cp865', + 'cp866': 'cp866', + 'cp869': 'cp869', + 'cp936': 'gbk', + 'cpgr': 'cp869', + 'cpis': 'cp861', + 'csascii': 'ascii', + 'csbig5': 'big5', + 'cseuckr': 'cp949', + 'cseucpkdfmtjapanese': 'euc_jp', + 'csgb2312': 'gbk', + 'cshproman8': 'hp-roman8', + 'csibm037': 'cp037', + 'csibm1026': 'cp1026', + 'csibm424': 'cp424', + 'csibm500': 'cp500', + 'csibm855': 'cp855', + 'csibm857': 'cp857', + 'csibm860': 'cp860', + 'csibm861': 'cp861', + 'csibm863': 'cp863', + 'csibm864': 'cp864', + 'csibm865': 'cp865', + 'csibm866': 'cp866', + 'csibm869': 'cp869', + 'csiso2022jp': 'iso2022_jp', + 'csiso2022jp2': 'iso2022_jp_2', + 'csiso2022kr': 'iso2022_kr', + 'csiso58gb231280': 'gbk', + 'csisolatin1': 'windows-1252', + 'csisolatin2': 'iso8859-2', + 'csisolatin3': 'iso8859-3', + 'csisolatin4': 'iso8859-4', + 'csisolatin5': 'windows-1254', + 'csisolatin6': 'iso8859-10', + 'csisolatinarabic': 'iso8859-6', + 'csisolatincyrillic': 'iso8859-5', + 'csisolatingreek': 'iso8859-7', + 'csisolatinhebrew': 'iso8859-8', + 'cskoi8r': 'koi8-r', + 'csksc56011987': 'cp949', + 'cspc775baltic': 'cp775', + 'cspc850multilingual': 'cp850', + 'cspc862latinhebrew': 'cp862', + 'cspc8codepage437': 'cp437', + 'cspcp852': 'cp852', + 'csptcp154': 'ptcp154', + 'csshiftjis': 'shift_jis', + 'csunicode11utf7': 'utf-7', + 'cyrillic': 'iso8859-5', + 'cyrillicasian': 'ptcp154', + 'ebcdiccpbe': 'cp500', + 'ebcdiccpca': 'cp037', + 'ebcdiccpch': 'cp500', + 'ebcdiccphe': 'cp424', + 'ebcdiccpnl': 'cp037', + 'ebcdiccpus': 'cp037', + 'ebcdiccpwt': 'cp037', + 'ecma114': 'iso8859-6', + 'ecma118': 'iso8859-7', + 'elot928': 'iso8859-7', + 'eucjp': 'euc_jp', + 'euckr': 'cp949', + 'extendedunixcodepackedformatforjapanese': 'euc_jp', + 'gb18030': 'gb18030', + 'gb2312': 'gbk', + 'gb231280': 'gbk', + 'gbk': 'gbk', + 'greek': 'iso8859-7', + 'greek8': 'iso8859-7', + 'hebrew': 'iso8859-8', + 'hproman8': 'hp-roman8', + 'hzgb2312': 'hz', + 'ibm037': 'cp037', + 'ibm1026': 'cp1026', + 'ibm367': 'ascii', + 'ibm424': 'cp424', + 'ibm437': 'cp437', + 'ibm500': 'cp500', + 'ibm775': 'cp775', + 'ibm819': 'windows-1252', + 'ibm850': 'cp850', + 'ibm852': 'cp852', + 'ibm855': 'cp855', + 'ibm857': 'cp857', + 'ibm860': 'cp860', + 'ibm861': 'cp861', + 'ibm862': 'cp862', + 'ibm863': 'cp863', + 'ibm864': 'cp864', + 'ibm865': 'cp865', + 'ibm866': 'cp866', + 'ibm869': 'cp869', + 'iso2022jp': 'iso2022_jp', + 'iso2022jp2': 'iso2022_jp_2', + 'iso2022kr': 'iso2022_kr', + 'iso646irv1991': 'ascii', + 'iso646us': 'ascii', + 'iso88591': 'windows-1252', + 'iso885910': 'iso8859-10', + 'iso8859101992': 'iso8859-10', + 'iso885911987': 'windows-1252', + 'iso885913': 'iso8859-13', + 'iso885914': 'iso8859-14', + 'iso8859141998': 'iso8859-14', + 'iso885915': 'iso8859-15', + 'iso885916': 'iso8859-16', + 'iso8859162001': 'iso8859-16', + 'iso88592': 'iso8859-2', + 'iso885921987': 'iso8859-2', + 'iso88593': 'iso8859-3', + 'iso885931988': 'iso8859-3', + 'iso88594': 'iso8859-4', + 'iso885941988': 'iso8859-4', + 'iso88595': 'iso8859-5', + 'iso885951988': 'iso8859-5', + 'iso88596': 'iso8859-6', + 'iso885961987': 'iso8859-6', + 'iso88597': 'iso8859-7', + 'iso885971987': 'iso8859-7', + 'iso88598': 'iso8859-8', + 'iso885981988': 'iso8859-8', + 'iso88599': 'windows-1254', + 'iso885991989': 'windows-1254', + 'isoceltic': 'iso8859-14', + 'isoir100': 'windows-1252', + 'isoir101': 'iso8859-2', + 'isoir109': 'iso8859-3', + 'isoir110': 'iso8859-4', + 'isoir126': 'iso8859-7', + 'isoir127': 'iso8859-6', + 'isoir138': 'iso8859-8', + 'isoir144': 'iso8859-5', + 'isoir148': 'windows-1254', + 'isoir149': 'cp949', + 'isoir157': 'iso8859-10', + 'isoir199': 'iso8859-14', + 'isoir226': 'iso8859-16', + 'isoir58': 'gbk', + 'isoir6': 'ascii', + 'koi8r': 'koi8-r', + 'koi8u': 'koi8-u', + 'korean': 'cp949', + 'ksc5601': 'cp949', + 'ksc56011987': 'cp949', + 'ksc56011989': 'cp949', + 'l1': 'windows-1252', + 'l10': 'iso8859-16', + 'l2': 'iso8859-2', + 'l3': 'iso8859-3', + 'l4': 'iso8859-4', + 'l5': 'windows-1254', + 'l6': 'iso8859-10', + 'l8': 'iso8859-14', + 'latin1': 'windows-1252', + 'latin10': 'iso8859-16', + 'latin2': 'iso8859-2', + 'latin3': 'iso8859-3', + 'latin4': 'iso8859-4', + 'latin5': 'windows-1254', + 'latin6': 'iso8859-10', + 'latin8': 'iso8859-14', + 'latin9': 'iso8859-15', + 'ms936': 'gbk', + 'mskanji': 'shift_jis', + 'pt154': 'ptcp154', + 'ptcp154': 'ptcp154', + 'r8': 'hp-roman8', + 'roman8': 'hp-roman8', + 'shiftjis': 'shift_jis', + 'tis620': 'cp874', + 'unicode11utf7': 'utf-7', + 'us': 'ascii', + 'usascii': 'ascii', + 'utf16': 'utf-16', + 'utf16be': 'utf-16-be', + 'utf16le': 'utf-16-le', + 'utf8': 'utf-8', + 'windows1250': 'cp1250', + 'windows1251': 'cp1251', + 'windows1252': 'cp1252', + 'windows1253': 'cp1253', + 'windows1254': 'cp1254', + 'windows1255': 'cp1255', + 'windows1256': 'cp1256', + 'windows1257': 'cp1257', + 'windows1258': 'cp1258', + 'windows936': 'gbk', + 'x-x-big5': 'big5'} + +tokenTypes = { + "Doctype": 0, + "Characters": 1, + "SpaceCharacters": 2, + "StartTag": 3, + "EndTag": 4, + "EmptyTag": 5, + "Comment": 6, + "ParseError": 7 +} + +tagTokenTypes = frozenset((tokenTypes["StartTag"], tokenTypes["EndTag"], + tokenTypes["EmptyTag"])) + + +prefixes = dict([(v, k) for k, v in namespaces.items()]) +prefixes["http://www.w3.org/1998/Math/MathML"] = "math" + + +class DataLossWarning(UserWarning): + pass + + +class ReparseException(Exception): + pass diff --git a/html5lib/filters/__init__.py b/html5lib/filters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/html5lib/filters/_base.py b/html5lib/filters/_base.py new file mode 100644 index 0000000..c7dbaed --- /dev/null +++ b/html5lib/filters/_base.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import, division, unicode_literals + + +class Filter(object): + def __init__(self, source): + self.source = source + + def __iter__(self): + return iter(self.source) + + def __getattr__(self, name): + return getattr(self.source, name) diff --git a/html5lib/filters/alphabeticalattributes.py b/html5lib/filters/alphabeticalattributes.py new file mode 100644 index 0000000..fed6996 --- /dev/null +++ b/html5lib/filters/alphabeticalattributes.py @@ -0,0 +1,20 @@ +from __future__ import absolute_import, division, unicode_literals + +from . import _base + +try: + from collections import OrderedDict +except ImportError: + from ordereddict import OrderedDict + + +class Filter(_base.Filter): + def __iter__(self): + for token in _base.Filter.__iter__(self): + if token["type"] in ("StartTag", "EmptyTag"): + attrs = OrderedDict() + for name, value in sorted(token["data"].items(), + key=lambda x: x[0]): + attrs[name] = value + token["data"] = attrs + yield token diff --git a/html5lib/filters/inject_meta_charset.py b/html5lib/filters/inject_meta_charset.py new file mode 100644 index 0000000..ca33b70 --- /dev/null +++ b/html5lib/filters/inject_meta_charset.py @@ -0,0 +1,65 @@ +from __future__ import absolute_import, division, unicode_literals + +from . import _base + + +class Filter(_base.Filter): + def __init__(self, source, encoding): + _base.Filter.__init__(self, source) + self.encoding = encoding + + def __iter__(self): + state = "pre_head" + meta_found = (self.encoding is None) + pending = [] + + for token in _base.Filter.__iter__(self): + type = token["type"] + if type == "StartTag": + if token["name"].lower() == "head": + state = "in_head" + + elif type == "EmptyTag": + if token["name"].lower() == "meta": + # replace charset with actual encoding + has_http_equiv_content_type = False + for (namespace, name), value in token["data"].items(): + if namespace is not None: + continue + elif name.lower() == 'charset': + token["data"][(namespace, name)] = self.encoding + meta_found = True + break + elif name == 'http-equiv' and value.lower() == 'content-type': + has_http_equiv_content_type = True + else: + if has_http_equiv_content_type and (None, "content") in token["data"]: + token["data"][(None, "content")] = 'text/html; charset=%s' % self.encoding + meta_found = True + + elif token["name"].lower() == "head" and not meta_found: + # insert meta into empty head + yield {"type": "StartTag", "name": "head", + "data": token["data"]} + yield {"type": "EmptyTag", "name": "meta", + "data": {(None, "charset"): self.encoding}} + yield {"type": "EndTag", "name": "head"} + meta_found = True + continue + + elif type == "EndTag": + if token["name"].lower() == "head" and pending: + # insert meta into head (if necessary) and flush pending queue + yield pending.pop(0) + if not meta_found: + yield {"type": "EmptyTag", "name": "meta", + "data": {(None, "charset"): self.encoding}} + while pending: + yield pending.pop(0) + meta_found = True + state = "post_head" + + if state == "in_head": + pending.append(token) + else: + yield token diff --git a/html5lib/filters/lint.py b/html5lib/filters/lint.py new file mode 100644 index 0000000..7cc99a4 --- /dev/null +++ b/html5lib/filters/lint.py @@ -0,0 +1,93 @@ +from __future__ import absolute_import, division, unicode_literals + +from gettext import gettext +_ = gettext + +from . import _base +from ..constants import cdataElements, rcdataElements, voidElements + +from ..constants import spaceCharacters +spaceCharacters = "".join(spaceCharacters) + + +class LintError(Exception): + pass + + +class Filter(_base.Filter): + def __iter__(self): + open_elements = [] + contentModelFlag = "PCDATA" + for token in _base.Filter.__iter__(self): + type = token["type"] + if type in ("StartTag", "EmptyTag"): + name = token["name"] + if contentModelFlag != "PCDATA": + raise LintError(_("StartTag not in PCDATA content model flag: %(tag)s") % {"tag": name}) + if not isinstance(name, str): + raise LintError(_("Tag name is not a string: %(tag)r") % {"tag": name}) + if not name: + raise LintError(_("Empty tag name")) + if type == "StartTag" and name in voidElements: + raise LintError(_("Void element reported as StartTag token: %(tag)s") % {"tag": name}) + elif type == "EmptyTag" and name not in voidElements: + raise LintError(_("Non-void element reported as EmptyTag token: %(tag)s") % {"tag": token["name"]}) + if type == "StartTag": + open_elements.append(name) + for name, value in token["data"]: + if not isinstance(name, str): + raise LintError(_("Attribute name is not a string: %(name)r") % {"name": name}) + if not name: + raise LintError(_("Empty attribute name")) + if not isinstance(value, str): + raise LintError(_("Attribute value is not a string: %(value)r") % {"value": value}) + if name in cdataElements: + contentModelFlag = "CDATA" + elif name in rcdataElements: + contentModelFlag = "RCDATA" + elif name == "plaintext": + contentModelFlag = "PLAINTEXT" + + elif type == "EndTag": + name = token["name"] + if not isinstance(name, str): + raise LintError(_("Tag name is not a string: %(tag)r") % {"tag": name}) + if not name: + raise LintError(_("Empty tag name")) + if name in voidElements: + raise LintError(_("Void element reported as EndTag token: %(tag)s") % {"tag": name}) + start_name = open_elements.pop() + if start_name != name: + raise LintError(_("EndTag (%(end)s) does not match StartTag (%(start)s)") % {"end": name, "start": start_name}) + contentModelFlag = "PCDATA" + + elif type == "Comment": + if contentModelFlag != "PCDATA": + raise LintError(_("Comment not in PCDATA content model flag")) + + elif type in ("Characters", "SpaceCharacters"): + data = token["data"] + if not isinstance(data, str): + raise LintError(_("Attribute name is not a string: %(name)r") % {"name": data}) + if not data: + raise LintError(_("%(type)s token with empty data") % {"type": type}) + if type == "SpaceCharacters": + data = data.strip(spaceCharacters) + if data: + raise LintError(_("Non-space character(s) found in SpaceCharacters token: %(token)r") % {"token": data}) + + elif type == "Doctype": + name = token["name"] + if contentModelFlag != "PCDATA": + raise LintError(_("Doctype not in PCDATA content model flag: %(name)s") % {"name": name}) + if not isinstance(name, str): + raise LintError(_("Tag name is not a string: %(tag)r") % {"tag": name}) + # XXX: what to do with token["data"] ? + + elif type in ("ParseError", "SerializeError"): + pass + + else: + raise LintError(_("Unknown token type: %(type)s") % {"type": type}) + + yield token diff --git a/html5lib/filters/optionaltags.py b/html5lib/filters/optionaltags.py new file mode 100644 index 0000000..fefe0b3 --- /dev/null +++ b/html5lib/filters/optionaltags.py @@ -0,0 +1,205 @@ +from __future__ import absolute_import, division, unicode_literals + +from . import _base + + +class Filter(_base.Filter): + def slider(self): + previous1 = previous2 = None + for token in self.source: + if previous1 is not None: + yield previous2, previous1, token + previous2 = previous1 + previous1 = token + yield previous2, previous1, None + + def __iter__(self): + for previous, token, next in self.slider(): + type = token["type"] + if type == "StartTag": + if (token["data"] or + not self.is_optional_start(token["name"], previous, next)): + yield token + elif type == "EndTag": + if not self.is_optional_end(token["name"], next): + yield token + else: + yield token + + def is_optional_start(self, tagname, previous, next): + type = next and next["type"] or None + if tagname in 'html': + # An html element's start tag may be omitted if the first thing + # inside the html element is not a space character or a comment. + return type not in ("Comment", "SpaceCharacters") + elif tagname == 'head': + # A head element's start tag may be omitted if the first thing + # inside the head element is an element. + # XXX: we also omit the start tag if the head element is empty + if type in ("StartTag", "EmptyTag"): + return True + elif type == "EndTag": + return next["name"] == "head" + elif tagname == 'body': + # A body element's start tag may be omitted if the first thing + # inside the body element is not a space character or a comment, + # except if the first thing inside the body element is a script + # or style element and the node immediately preceding the body + # element is a head element whose end tag has been omitted. + if type in ("Comment", "SpaceCharacters"): + return False + elif type == "StartTag": + # XXX: we do not look at the preceding event, so we never omit + # the body element's start tag if it's followed by a script or + # a style element. + return next["name"] not in ('script', 'style') + else: + return True + elif tagname == 'colgroup': + # A colgroup element's start tag may be omitted if the first thing + # inside the colgroup element is a col element, and if the element + # is not immediately preceeded by another colgroup element whose + # end tag has been omitted. + if type in ("StartTag", "EmptyTag"): + # XXX: we do not look at the preceding event, so instead we never + # omit the colgroup element's end tag when it is immediately + # followed by another colgroup element. See is_optional_end. + return next["name"] == "col" + else: + return False + elif tagname == 'tbody': + # A tbody element's start tag may be omitted if the first thing + # inside the tbody element is a tr element, and if the element is + # not immediately preceeded by a tbody, thead, or tfoot element + # whose end tag has been omitted. + if type == "StartTag": + # omit the thead and tfoot elements' end tag when they are + # immediately followed by a tbody element. See is_optional_end. + if previous and previous['type'] == 'EndTag' and \ + previous['name'] in ('tbody', 'thead', 'tfoot'): + return False + return next["name"] == 'tr' + else: + return False + return False + + def is_optional_end(self, tagname, next): + type = next and next["type"] or None + if tagname in ('html', 'head', 'body'): + # An html element's end tag may be omitted if the html element + # is not immediately followed by a space character or a comment. + return type not in ("Comment", "SpaceCharacters") + elif tagname in ('li', 'optgroup', 'tr'): + # A li element's end tag may be omitted if the li element is + # immediately followed by another li element or if there is + # no more content in the parent element. + # An optgroup element's end tag may be omitted if the optgroup + # element is immediately followed by another optgroup element, + # or if there is no more content in the parent element. + # A tr element's end tag may be omitted if the tr element is + # immediately followed by another tr element, or if there is + # no more content in the parent element. + if type == "StartTag": + return next["name"] == tagname + else: + return type == "EndTag" or type is None + elif tagname in ('dt', 'dd'): + # A dt element's end tag may be omitted if the dt element is + # immediately followed by another dt element or a dd element. + # A dd element's end tag may be omitted if the dd element is + # immediately followed by another dd element or a dt element, + # or if there is no more content in the parent element. + if type == "StartTag": + return next["name"] in ('dt', 'dd') + elif tagname == 'dd': + return type == "EndTag" or type is None + else: + return False + elif tagname == 'p': + # A p element's end tag may be omitted if the p element is + # immediately followed by an address, article, aside, + # blockquote, datagrid, dialog, dir, div, dl, fieldset, + # footer, form, h1, h2, h3, h4, h5, h6, header, hr, menu, + # nav, ol, p, pre, section, table, or ul, element, or if + # there is no more content in the parent element. + if type in ("StartTag", "EmptyTag"): + return next["name"] in ('address', 'article', 'aside', + 'blockquote', 'datagrid', 'dialog', + 'dir', 'div', 'dl', 'fieldset', 'footer', + 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', + 'header', 'hr', 'menu', 'nav', 'ol', + 'p', 'pre', 'section', 'table', 'ul') + else: + return type == "EndTag" or type is None + elif tagname == 'option': + # An option element's end tag may be omitted if the option + # element is immediately followed by another option element, + # or if it is immediately followed by an optgroup + # element, or if there is no more content in the parent + # element. + if type == "StartTag": + return next["name"] in ('option', 'optgroup') + else: + return type == "EndTag" or type is None + elif tagname in ('rt', 'rp'): + # An rt element's end tag may be omitted if the rt element is + # immediately followed by an rt or rp element, or if there is + # no more content in the parent element. + # An rp element's end tag may be omitted if the rp element is + # immediately followed by an rt or rp element, or if there is + # no more content in the parent element. + if type == "StartTag": + return next["name"] in ('rt', 'rp') + else: + return type == "EndTag" or type is None + elif tagname == 'colgroup': + # A colgroup element's end tag may be omitted if the colgroup + # element is not immediately followed by a space character or + # a comment. + if type in ("Comment", "SpaceCharacters"): + return False + elif type == "StartTag": + # XXX: we also look for an immediately following colgroup + # element. See is_optional_start. + return next["name"] != 'colgroup' + else: + return True + elif tagname in ('thead', 'tbody'): + # A thead element's end tag may be omitted if the thead element + # is immediately followed by a tbody or tfoot element. + # A tbody element's end tag may be omitted if the tbody element + # is immediately followed by a tbody or tfoot element, or if + # there is no more content in the parent element. + # A tfoot element's end tag may be omitted if the tfoot element + # is immediately followed by a tbody element, or if there is no + # more content in the parent element. + # XXX: we never omit the end tag when the following element is + # a tbody. See is_optional_start. + if type == "StartTag": + return next["name"] in ['tbody', 'tfoot'] + elif tagname == 'tbody': + return type == "EndTag" or type is None + else: + return False + elif tagname == 'tfoot': + # A tfoot element's end tag may be omitted if the tfoot element + # is immediately followed by a tbody element, or if there is no + # more content in the parent element. + # XXX: we never omit the end tag when the following element is + # a tbody. See is_optional_start. + if type == "StartTag": + return next["name"] == 'tbody' + else: + return type == "EndTag" or type is None + elif tagname in ('td', 'th'): + # A td element's end tag may be omitted if the td element is + # immediately followed by a td or th element, or if there is + # no more content in the parent element. + # A th element's end tag may be omitted if the th element is + # immediately followed by a td or th element, or if there is + # no more content in the parent element. + if type == "StartTag": + return next["name"] in ('td', 'th') + else: + return type == "EndTag" or type is None + return False diff --git a/html5lib/filters/sanitizer.py b/html5lib/filters/sanitizer.py new file mode 100644 index 0000000..b206b54 --- /dev/null +++ b/html5lib/filters/sanitizer.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import, division, unicode_literals + +from . import _base +from ..sanitizer import HTMLSanitizerMixin + + +class Filter(_base.Filter, HTMLSanitizerMixin): + def __iter__(self): + for token in _base.Filter.__iter__(self): + token = self.sanitize_token(token) + if token: + yield token diff --git a/html5lib/filters/whitespace.py b/html5lib/filters/whitespace.py new file mode 100644 index 0000000..dfc60ee --- /dev/null +++ b/html5lib/filters/whitespace.py @@ -0,0 +1,38 @@ +from __future__ import absolute_import, division, unicode_literals + +import re + +from . import _base +from ..constants import rcdataElements, spaceCharacters +spaceCharacters = "".join(spaceCharacters) + +SPACES_REGEX = re.compile("[%s]+" % spaceCharacters) + + +class Filter(_base.Filter): + + spacePreserveElements = frozenset(["pre", "textarea"] + list(rcdataElements)) + + def __iter__(self): + preserve = 0 + for token in _base.Filter.__iter__(self): + type = token["type"] + if type == "StartTag" \ + and (preserve or token["name"] in self.spacePreserveElements): + preserve += 1 + + elif type == "EndTag" and preserve: + preserve -= 1 + + elif not preserve and type == "SpaceCharacters" and token["data"]: + # Test on token["data"] above to not introduce spaces where there were not + token["data"] = " " + + elif not preserve and type == "Characters": + token["data"] = collapse_spaces(token["data"]) + + yield token + + +def collapse_spaces(text): + return SPACES_REGEX.sub(' ', text) diff --git a/html5lib/html5parser.py b/html5lib/html5parser.py new file mode 100644 index 0000000..b0f14f3 --- /dev/null +++ b/html5lib/html5parser.py @@ -0,0 +1,2713 @@ +from __future__ import absolute_import, division, unicode_literals +from six import with_metaclass + +import types + +from . import inputstream +from . import tokenizer + +from . import treebuilders +from .treebuilders._base import Marker + +from . import utils +from . import constants +from .constants import spaceCharacters, asciiUpper2Lower +from .constants import specialElements +from .constants import headingElements +from .constants import cdataElements, rcdataElements +from .constants import tokenTypes, ReparseException, namespaces +from .constants import htmlIntegrationPointElements, mathmlTextIntegrationPointElements +from .constants import adjustForeignAttributes as adjustForeignAttributesMap + + +def parse(doc, treebuilder="etree", encoding=None, + namespaceHTMLElements=True): + """Parse a string or file-like object into a tree""" + tb = treebuilders.getTreeBuilder(treebuilder) + p = HTMLParser(tb, namespaceHTMLElements=namespaceHTMLElements) + return p.parse(doc, encoding=encoding) + + +def parseFragment(doc, container="div", treebuilder="etree", encoding=None, + namespaceHTMLElements=True): + tb = treebuilders.getTreeBuilder(treebuilder) + p = HTMLParser(tb, namespaceHTMLElements=namespaceHTMLElements) + return p.parseFragment(doc, container=container, encoding=encoding) + + +def method_decorator_metaclass(function): + class Decorated(type): + def __new__(meta, classname, bases, classDict): + for attributeName, attribute in classDict.items(): + if isinstance(attribute, types.FunctionType): + attribute = function(attribute) + + classDict[attributeName] = attribute + return type.__new__(meta, classname, bases, classDict) + return Decorated + + +class HTMLParser(object): + """HTML parser. Generates a tree structure from a stream of (possibly + malformed) HTML""" + + def __init__(self, tree=None, tokenizer=tokenizer.HTMLTokenizer, + strict=False, namespaceHTMLElements=True, debug=False): + """ + strict - raise an exception when a parse error is encountered + + tree - a treebuilder class controlling the type of tree that will be + returned. Built in treebuilders can be accessed through + html5lib.treebuilders.getTreeBuilder(treeType) + + tokenizer - a class that provides a stream of tokens to the treebuilder. + This may be replaced for e.g. a sanitizer which converts some tags to + text + """ + + # Raise an exception on the first error encountered + self.strict = strict + + if tree is None: + tree = treebuilders.getTreeBuilder("etree") + self.tree = tree(namespaceHTMLElements) + self.tokenizer_class = tokenizer + self.errors = [] + + self.phases = dict([(name, cls(self, self.tree)) for name, cls in + getPhases(debug).items()]) + + def _parse(self, stream, innerHTML=False, container="div", + encoding=None, parseMeta=True, useChardet=True, **kwargs): + + self.innerHTMLMode = innerHTML + self.container = container + self.tokenizer = self.tokenizer_class(stream, encoding=encoding, + parseMeta=parseMeta, + useChardet=useChardet, + parser=self, **kwargs) + self.reset() + + while True: + try: + self.mainLoop() + break + except ReparseException: + self.reset() + + def reset(self): + self.tree.reset() + self.firstStartTag = False + self.errors = [] + self.log = [] # only used with debug mode + # "quirks" / "limited quirks" / "no quirks" + self.compatMode = "no quirks" + + if self.innerHTMLMode: + self.innerHTML = self.container.lower() + + if self.innerHTML in cdataElements: + self.tokenizer.state = self.tokenizer.rcdataState + elif self.innerHTML in rcdataElements: + self.tokenizer.state = self.tokenizer.rawtextState + elif self.innerHTML == 'plaintext': + self.tokenizer.state = self.tokenizer.plaintextState + else: + # state already is data state + # self.tokenizer.state = self.tokenizer.dataState + pass + self.phase = self.phases["beforeHtml"] + self.phase.insertHtmlElement() + self.resetInsertionMode() + else: + self.innerHTML = False + self.phase = self.phases["initial"] + + self.lastPhase = None + + self.beforeRCDataPhase = None + + self.framesetOK = True + + def isHTMLIntegrationPoint(self, element): + if (element.name == "annotation-xml" and + element.namespace == namespaces["mathml"]): + return ("encoding" in element.attributes and + element.attributes["encoding"].translate( + asciiUpper2Lower) in + ("text/html", "application/xhtml+xml")) + else: + return (element.namespace, element.name) in htmlIntegrationPointElements + + def isMathMLTextIntegrationPoint(self, element): + return (element.namespace, element.name) in mathmlTextIntegrationPointElements + + def mainLoop(self): + CharactersToken = tokenTypes["Characters"] + SpaceCharactersToken = tokenTypes["SpaceCharacters"] + StartTagToken = tokenTypes["StartTag"] + EndTagToken = tokenTypes["EndTag"] + CommentToken = tokenTypes["Comment"] + DoctypeToken = tokenTypes["Doctype"] + ParseErrorToken = tokenTypes["ParseError"] + + for token in self.normalizedTokens(): + new_token = token + while new_token is not None: + currentNode = self.tree.openElements[-1] if self.tree.openElements else None + currentNodeNamespace = currentNode.namespace if currentNode else None + currentNodeName = currentNode.name if currentNode else None + + type = new_token["type"] + + if type == ParseErrorToken: + self.parseError(new_token["data"], new_token.get("datavars", {})) + new_token = None + else: + if (len(self.tree.openElements) == 0 or + currentNodeNamespace == self.tree.defaultNamespace or + (self.isMathMLTextIntegrationPoint(currentNode) and + ((type == StartTagToken and + token["name"] not in frozenset(["mglyph", "malignmark"])) or + type in (CharactersToken, SpaceCharactersToken))) or + (currentNodeNamespace == namespaces["mathml"] and + currentNodeName == "annotation-xml" and + token["name"] == "svg") or + (self.isHTMLIntegrationPoint(currentNode) and + type in (StartTagToken, CharactersToken, SpaceCharactersToken))): + phase = self.phase + else: + phase = self.phases["inForeignContent"] + + if type == CharactersToken: + new_token = phase.processCharacters(new_token) + elif type == SpaceCharactersToken: + new_token = phase.processSpaceCharacters(new_token) + elif type == StartTagToken: + new_token = phase.processStartTag(new_token) + elif type == EndTagToken: + new_token = phase.processEndTag(new_token) + elif type == CommentToken: + new_token = phase.processComment(new_token) + elif type == DoctypeToken: + new_token = phase.processDoctype(new_token) + + if (type == StartTagToken and token["selfClosing"] + and not token["selfClosingAcknowledged"]): + self.parseError("non-void-element-with-trailing-solidus", + {"name": token["name"]}) + + # When the loop finishes it's EOF + reprocess = True + phases = [] + while reprocess: + phases.append(self.phase) + reprocess = self.phase.processEOF() + if reprocess: + assert self.phase not in phases + + def normalizedTokens(self): + for token in self.tokenizer: + yield self.normalizeToken(token) + + def parse(self, stream, encoding=None, parseMeta=True, useChardet=True): + """Parse a HTML document into a well-formed tree + + stream - a filelike object or string containing the HTML to be parsed + + The optional encoding parameter must be a string that indicates + the encoding. If specified, that encoding will be used, + regardless of any BOM or later declaration (such as in a meta + element) + """ + self._parse(stream, innerHTML=False, encoding=encoding, + parseMeta=parseMeta, useChardet=useChardet) + return self.tree.getDocument() + + def parseFragment(self, stream, container="div", encoding=None, + parseMeta=False, useChardet=True): + """Parse a HTML fragment into a well-formed tree fragment + + container - name of the element we're setting the innerHTML property + if set to None, default to 'div' + + stream - a filelike object or string containing the HTML to be parsed + + The optional encoding parameter must be a string that indicates + the encoding. If specified, that encoding will be used, + regardless of any BOM or later declaration (such as in a meta + element) + """ + self._parse(stream, True, container=container, encoding=encoding) + return self.tree.getFragment() + + def parseError(self, errorcode="XXX-undefined-error", datavars={}): + # XXX The idea is to make errorcode mandatory. + self.errors.append((self.tokenizer.stream.position(), errorcode, datavars)) + if self.strict: + raise ParseError + + def normalizeToken(self, token): + """ HTML5 specific normalizations to the token stream """ + + if token["type"] == tokenTypes["StartTag"]: + token["data"] = dict(token["data"][::-1]) + + return token + + def adjustMathMLAttributes(self, token): + replacements = {"definitionurl": "definitionURL"} + for k, v in replacements.items(): + if k in token["data"]: + token["data"][v] = token["data"][k] + del token["data"][k] + + def adjustSVGAttributes(self, token): + replacements = { + "attributename": "attributeName", + "attributetype": "attributeType", + "basefrequency": "baseFrequency", + "baseprofile": "baseProfile", + "calcmode": "calcMode", + "clippathunits": "clipPathUnits", + "contentscripttype": "contentScriptType", + "contentstyletype": "contentStyleType", + "diffuseconstant": "diffuseConstant", + "edgemode": "edgeMode", + "externalresourcesrequired": "externalResourcesRequired", + "filterres": "filterRes", + "filterunits": "filterUnits", + "glyphref": "glyphRef", + "gradienttransform": "gradientTransform", + "gradientunits": "gradientUnits", + "kernelmatrix": "kernelMatrix", + "kernelunitlength": "kernelUnitLength", + "keypoints": "keyPoints", + "keysplines": "keySplines", + "keytimes": "keyTimes", + "lengthadjust": "lengthAdjust", + "limitingconeangle": "limitingConeAngle", + "markerheight": "markerHeight", + "markerunits": "markerUnits", + "markerwidth": "markerWidth", + "maskcontentunits": "maskContentUnits", + "maskunits": "maskUnits", + "numoctaves": "numOctaves", + "pathlength": "pathLength", + "patterncontentunits": "patternContentUnits", + "patterntransform": "patternTransform", + "patternunits": "patternUnits", + "pointsatx": "pointsAtX", + "pointsaty": "pointsAtY", + "pointsatz": "pointsAtZ", + "preservealpha": "preserveAlpha", + "preserveaspectratio": "preserveAspectRatio", + "primitiveunits": "primitiveUnits", + "refx": "refX", + "refy": "refY", + "repeatcount": "repeatCount", + "repeatdur": "repeatDur", + "requiredextensions": "requiredExtensions", + "requiredfeatures": "requiredFeatures", + "specularconstant": "specularConstant", + "specularexponent": "specularExponent", + "spreadmethod": "spreadMethod", + "startoffset": "startOffset", + "stddeviation": "stdDeviation", + "stitchtiles": "stitchTiles", + "surfacescale": "surfaceScale", + "systemlanguage": "systemLanguage", + "tablevalues": "tableValues", + "targetx": "targetX", + "targety": "targetY", + "textlength": "textLength", + "viewbox": "viewBox", + "viewtarget": "viewTarget", + "xchannelselector": "xChannelSelector", + "ychannelselector": "yChannelSelector", + "zoomandpan": "zoomAndPan" + } + for originalName in list(token["data"].keys()): + if originalName in replacements: + svgName = replacements[originalName] + token["data"][svgName] = token["data"][originalName] + del token["data"][originalName] + + def adjustForeignAttributes(self, token): + replacements = adjustForeignAttributesMap + + for originalName in token["data"].keys(): + if originalName in replacements: + foreignName = replacements[originalName] + token["data"][foreignName] = token["data"][originalName] + del token["data"][originalName] + + def reparseTokenNormal(self, token): + self.parser.phase() + + def resetInsertionMode(self): + # The name of this method is mostly historical. (It's also used in the + # specification.) + last = False + newModes = { + "select": "inSelect", + "td": "inCell", + "th": "inCell", + "tr": "inRow", + "tbody": "inTableBody", + "thead": "inTableBody", + "tfoot": "inTableBody", + "caption": "inCaption", + "colgroup": "inColumnGroup", + "table": "inTable", + "head": "inBody", + "body": "inBody", + "frameset": "inFrameset", + "html": "beforeHead" + } + for node in self.tree.openElements[::-1]: + nodeName = node.name + new_phase = None + if node == self.tree.openElements[0]: + assert self.innerHTML + last = True + nodeName = self.innerHTML + # Check for conditions that should only happen in the innerHTML + # case + if nodeName in ("select", "colgroup", "head", "html"): + assert self.innerHTML + + if not last and node.namespace != self.tree.defaultNamespace: + continue + + if nodeName in newModes: + new_phase = self.phases[newModes[nodeName]] + break + elif last: + new_phase = self.phases["inBody"] + break + + self.phase = new_phase + + def parseRCDataRawtext(self, token, contentType): + """Generic RCDATA/RAWTEXT Parsing algorithm + contentType - RCDATA or RAWTEXT + """ + assert contentType in ("RAWTEXT", "RCDATA") + + self.tree.insertElement(token) + + if contentType == "RAWTEXT": + self.tokenizer.state = self.tokenizer.rawtextState + else: + self.tokenizer.state = self.tokenizer.rcdataState + + self.originalPhase = self.phase + + self.phase = self.phases["text"] + + +def getPhases(debug): + def log(function): + """Logger that records which phase processes each token""" + type_names = dict((value, key) for key, value in + constants.tokenTypes.items()) + + def wrapped(self, *args, **kwargs): + if function.__name__.startswith("process") and len(args) > 0: + token = args[0] + try: + info = {"type": type_names[token['type']]} + except: + raise + if token['type'] in constants.tagTokenTypes: + info["name"] = token['name'] + + self.parser.log.append((self.parser.tokenizer.state.__name__, + self.parser.phase.__class__.__name__, + self.__class__.__name__, + function.__name__, + info)) + return function(self, *args, **kwargs) + else: + return function(self, *args, **kwargs) + return wrapped + + def getMetaclass(use_metaclass, metaclass_func): + if use_metaclass: + return method_decorator_metaclass(metaclass_func) + else: + return type + + class Phase(with_metaclass(getMetaclass(debug, log))): + """Base class for helper object that implements each phase of processing + """ + + def __init__(self, parser, tree): + self.parser = parser + self.tree = tree + + def processEOF(self): + raise NotImplementedError + + def processComment(self, token): + # For most phases the following is correct. Where it's not it will be + # overridden. + self.tree.insertComment(token, self.tree.openElements[-1]) + + def processDoctype(self, token): + self.parser.parseError("unexpected-doctype") + + def processCharacters(self, token): + self.tree.insertText(token["data"]) + + def processSpaceCharacters(self, token): + self.tree.insertText(token["data"]) + + def processStartTag(self, token): + return self.startTagHandler[token["name"]](token) + + def startTagHtml(self, token): + if not self.parser.firstStartTag and token["name"] == "html": + self.parser.parseError("non-html-root") + # XXX Need a check here to see if the first start tag token emitted is + # this token... If it's not, invoke self.parser.parseError(). + for attr, value in token["data"].items(): + if attr not in self.tree.openElements[0].attributes: + self.tree.openElements[0].attributes[attr] = value + self.parser.firstStartTag = False + + def processEndTag(self, token): + return self.endTagHandler[token["name"]](token) + + class InitialPhase(Phase): + def processSpaceCharacters(self, token): + pass + + def processComment(self, token): + self.tree.insertComment(token, self.tree.document) + + def processDoctype(self, token): + name = token["name"] + publicId = token["publicId"] + systemId = token["systemId"] + correct = token["correct"] + + if (name != "html" or publicId is not None or + systemId is not None and systemId != "about:legacy-compat"): + self.parser.parseError("unknown-doctype") + + if publicId is None: + publicId = "" + + self.tree.insertDoctype(token) + + if publicId != "": + publicId = publicId.translate(asciiUpper2Lower) + + if (not correct or token["name"] != "html" + or publicId.startswith( + ("+//silmaril//dtd html pro v0r11 19970101//", + "-//advasoft ltd//dtd html 3.0 aswedit + extensions//", + "-//as//dtd html 3.0 aswedit + extensions//", + "-//ietf//dtd html 2.0 level 1//", + "-//ietf//dtd html 2.0 level 2//", + "-//ietf//dtd html 2.0 strict level 1//", + "-//ietf//dtd html 2.0 strict level 2//", + "-//ietf//dtd html 2.0 strict//", + "-//ietf//dtd html 2.0//", + "-//ietf//dtd html 2.1e//", + "-//ietf//dtd html 3.0//", + "-//ietf//dtd html 3.2 final//", + "-//ietf//dtd html 3.2//", + "-//ietf//dtd html 3//", + "-//ietf//dtd html level 0//", + "-//ietf//dtd html level 1//", + "-//ietf//dtd html level 2//", + "-//ietf//dtd html level 3//", + "-//ietf//dtd html strict level 0//", + "-//ietf//dtd html strict level 1//", + "-//ietf//dtd html strict level 2//", + "-//ietf//dtd html strict level 3//", + "-//ietf//dtd html strict//", + "-//ietf//dtd html//", + "-//metrius//dtd metrius presentational//", + "-//microsoft//dtd internet explorer 2.0 html strict//", + "-//microsoft//dtd internet explorer 2.0 html//", + "-//microsoft//dtd internet explorer 2.0 tables//", + "-//microsoft//dtd internet explorer 3.0 html strict//", + "-//microsoft//dtd internet explorer 3.0 html//", + "-//microsoft//dtd internet explorer 3.0 tables//", + "-//netscape comm. corp.//dtd html//", + "-//netscape comm. corp.//dtd strict html//", + "-//o'reilly and associates//dtd html 2.0//", + "-//o'reilly and associates//dtd html extended 1.0//", + "-//o'reilly and associates//dtd html extended relaxed 1.0//", + "-//softquad software//dtd hotmetal pro 6.0::19990601::extensions to html 4.0//", + "-//softquad//dtd hotmetal pro 4.0::19971010::extensions to html 4.0//", + "-//spyglass//dtd html 2.0 extended//", + "-//sq//dtd html 2.0 hotmetal + extensions//", + "-//sun microsystems corp.//dtd hotjava html//", + "-//sun microsystems corp.//dtd hotjava strict html//", + "-//w3c//dtd html 3 1995-03-24//", + "-//w3c//dtd html 3.2 draft//", + "-//w3c//dtd html 3.2 final//", + "-//w3c//dtd html 3.2//", + "-//w3c//dtd html 3.2s draft//", + "-//w3c//dtd html 4.0 frameset//", + "-//w3c//dtd html 4.0 transitional//", + "-//w3c//dtd html experimental 19960712//", + "-//w3c//dtd html experimental 970421//", + "-//w3c//dtd w3 html//", + "-//w3o//dtd w3 html 3.0//", + "-//webtechs//dtd mozilla html 2.0//", + "-//webtechs//dtd mozilla html//")) + or publicId in + ("-//w3o//dtd w3 html strict 3.0//en//", + "-/w3c/dtd html 4.0 transitional/en", + "html") + or publicId.startswith( + ("-//w3c//dtd html 4.01 frameset//", + "-//w3c//dtd html 4.01 transitional//")) and + systemId is None + or systemId and systemId.lower() == "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd"): + self.parser.compatMode = "quirks" + elif (publicId.startswith( + ("-//w3c//dtd xhtml 1.0 frameset//", + "-//w3c//dtd xhtml 1.0 transitional//")) + or publicId.startswith( + ("-//w3c//dtd html 4.01 frameset//", + "-//w3c//dtd html 4.01 transitional//")) and + systemId is not None): + self.parser.compatMode = "limited quirks" + + self.parser.phase = self.parser.phases["beforeHtml"] + + def anythingElse(self): + self.parser.compatMode = "quirks" + self.parser.phase = self.parser.phases["beforeHtml"] + + def processCharacters(self, token): + self.parser.parseError("expected-doctype-but-got-chars") + self.anythingElse() + return token + + def processStartTag(self, token): + self.parser.parseError("expected-doctype-but-got-start-tag", + {"name": token["name"]}) + self.anythingElse() + return token + + def processEndTag(self, token): + self.parser.parseError("expected-doctype-but-got-end-tag", + {"name": token["name"]}) + self.anythingElse() + return token + + def processEOF(self): + self.parser.parseError("expected-doctype-but-got-eof") + self.anythingElse() + return True + + class BeforeHtmlPhase(Phase): + # helper methods + def insertHtmlElement(self): + self.tree.insertRoot(impliedTagToken("html", "StartTag")) + self.parser.phase = self.parser.phases["beforeHead"] + + # other + def processEOF(self): + self.insertHtmlElement() + return True + + def processComment(self, token): + self.tree.insertComment(token, self.tree.document) + + def processSpaceCharacters(self, token): + pass + + def processCharacters(self, token): + self.insertHtmlElement() + return token + + def processStartTag(self, token): + if token["name"] == "html": + self.parser.firstStartTag = True + self.insertHtmlElement() + return token + + def processEndTag(self, token): + if token["name"] not in ("head", "body", "html", "br"): + self.parser.parseError("unexpected-end-tag-before-html", + {"name": token["name"]}) + else: + self.insertHtmlElement() + return token + + class BeforeHeadPhase(Phase): + def __init__(self, parser, tree): + Phase.__init__(self, parser, tree) + + self.startTagHandler = utils.MethodDispatcher([ + ("html", self.startTagHtml), + ("head", self.startTagHead) + ]) + self.startTagHandler.default = self.startTagOther + + self.endTagHandler = utils.MethodDispatcher([ + (("head", "body", "html", "br"), self.endTagImplyHead) + ]) + self.endTagHandler.default = self.endTagOther + + def processEOF(self): + self.startTagHead(impliedTagToken("head", "StartTag")) + return True + + def processSpaceCharacters(self, token): + pass + + def processCharacters(self, token): + self.startTagHead(impliedTagToken("head", "StartTag")) + return token + + def startTagHtml(self, token): + return self.parser.phases["inBody"].processStartTag(token) + + def startTagHead(self, token): + self.tree.insertElement(token) + self.tree.headPointer = self.tree.openElements[-1] + self.parser.phase = self.parser.phases["inHead"] + + def startTagOther(self, token): + self.startTagHead(impliedTagToken("head", "StartTag")) + return token + + def endTagImplyHead(self, token): + self.startTagHead(impliedTagToken("head", "StartTag")) + return token + + def endTagOther(self, token): + self.parser.parseError("end-tag-after-implied-root", + {"name": token["name"]}) + + class InHeadPhase(Phase): + def __init__(self, parser, tree): + Phase.__init__(self, parser, tree) + + self.startTagHandler = utils.MethodDispatcher([ + ("html", self.startTagHtml), + ("title", self.startTagTitle), + (("noscript", "noframes", "style"), self.startTagNoScriptNoFramesStyle), + ("script", self.startTagScript), + (("base", "basefont", "bgsound", "command", "link"), + self.startTagBaseLinkCommand), + ("meta", self.startTagMeta), + ("head", self.startTagHead) + ]) + self.startTagHandler.default = self.startTagOther + + self. endTagHandler = utils.MethodDispatcher([ + ("head", self.endTagHead), + (("br", "html", "body"), self.endTagHtmlBodyBr) + ]) + self.endTagHandler.default = self.endTagOther + + # the real thing + def processEOF(self): + self.anythingElse() + return True + + def processCharacters(self, token): + self.anythingElse() + return token + + def startTagHtml(self, token): + return self.parser.phases["inBody"].processStartTag(token) + + def startTagHead(self, token): + self.parser.parseError("two-heads-are-not-better-than-one") + + def startTagBaseLinkCommand(self, token): + self.tree.insertElement(token) + self.tree.openElements.pop() + token["selfClosingAcknowledged"] = True + + def startTagMeta(self, token): + self.tree.insertElement(token) + self.tree.openElements.pop() + token["selfClosingAcknowledged"] = True + + attributes = token["data"] + if self.parser.tokenizer.stream.charEncoding[1] == "tentative": + if "charset" in attributes: + self.parser.tokenizer.stream.changeEncoding(attributes["charset"]) + elif ("content" in attributes and + "http-equiv" in attributes and + attributes["http-equiv"].lower() == "content-type"): + # Encoding it as UTF-8 here is a hack, as really we should pass + # the abstract Unicode string, and just use the + # ContentAttrParser on that, but using UTF-8 allows all chars + # to be encoded and as a ASCII-superset works. + data = inputstream.EncodingBytes(attributes["content"].encode("utf-8")) + parser = inputstream.ContentAttrParser(data) + codec = parser.parse() + self.parser.tokenizer.stream.changeEncoding(codec) + + def startTagTitle(self, token): + self.parser.parseRCDataRawtext(token, "RCDATA") + + def startTagNoScriptNoFramesStyle(self, token): + # Need to decide whether to implement the scripting-disabled case + self.parser.parseRCDataRawtext(token, "RAWTEXT") + + def startTagScript(self, token): + self.tree.insertElement(token) + self.parser.tokenizer.state = self.parser.tokenizer.scriptDataState + self.parser.originalPhase = self.parser.phase + self.parser.phase = self.parser.phases["text"] + + def startTagOther(self, token): + self.anythingElse() + return token + + def endTagHead(self, token): + node = self.parser.tree.openElements.pop() + assert node.name == "head", "Expected head got %s" % node.name + self.parser.phase = self.parser.phases["afterHead"] + + def endTagHtmlBodyBr(self, token): + self.anythingElse() + return token + + def endTagOther(self, token): + self.parser.parseError("unexpected-end-tag", {"name": token["name"]}) + + def anythingElse(self): + self.endTagHead(impliedTagToken("head")) + + # XXX If we implement a parser for which scripting is disabled we need to + # implement this phase. + # + # class InHeadNoScriptPhase(Phase): + class AfterHeadPhase(Phase): + def __init__(self, parser, tree): + Phase.__init__(self, parser, tree) + + self.startTagHandler = utils.MethodDispatcher([ + ("html", self.startTagHtml), + ("body", self.startTagBody), + ("frameset", self.startTagFrameset), + (("base", "basefont", "bgsound", "link", "meta", "noframes", "script", + "style", "title"), + self.startTagFromHead), + ("head", self.startTagHead) + ]) + self.startTagHandler.default = self.startTagOther + self.endTagHandler = utils.MethodDispatcher([(("body", "html", "br"), + self.endTagHtmlBodyBr)]) + self.endTagHandler.default = self.endTagOther + + def processEOF(self): + self.anythingElse() + return True + + def processCharacters(self, token): + self.anythingElse() + return token + + def startTagHtml(self, token): + return self.parser.phases["inBody"].processStartTag(token) + + def startTagBody(self, token): + self.parser.framesetOK = False + self.tree.insertElement(token) + self.parser.phase = self.parser.phases["inBody"] + + def startTagFrameset(self, token): + self.tree.insertElement(token) + self.parser.phase = self.parser.phases["inFrameset"] + + def startTagFromHead(self, token): + self.parser.parseError("unexpected-start-tag-out-of-my-head", + {"name": token["name"]}) + self.tree.openElements.append(self.tree.headPointer) + self.parser.phases["inHead"].processStartTag(token) + for node in self.tree.openElements[::-1]: + if node.name == "head": + self.tree.openElements.remove(node) + break + + def startTagHead(self, token): + self.parser.parseError("unexpected-start-tag", {"name": token["name"]}) + + def startTagOther(self, token): + self.anythingElse() + return token + + def endTagHtmlBodyBr(self, token): + self.anythingElse() + return token + + def endTagOther(self, token): + self.parser.parseError("unexpected-end-tag", {"name": token["name"]}) + + def anythingElse(self): + self.tree.insertElement(impliedTagToken("body", "StartTag")) + self.parser.phase = self.parser.phases["inBody"] + self.parser.framesetOK = True + + class InBodyPhase(Phase): + # http://www.whatwg.org/specs/web-apps/current-work/#parsing-main-inbody + # the really-really-really-very crazy mode + def __init__(self, parser, tree): + Phase.__init__(self, parser, tree) + + # Keep a ref to this for special handling of whitespace in
    +            self.processSpaceCharactersNonPre = self.processSpaceCharacters
    +
    +            self.startTagHandler = utils.MethodDispatcher([
    +                ("html", self.startTagHtml),
    +                (("base", "basefont", "bgsound", "command", "link", "meta",
    +                  "noframes", "script", "style", "title"),
    +                 self.startTagProcessInHead),
    +                ("body", self.startTagBody),
    +                ("frameset", self.startTagFrameset),
    +                (("address", "article", "aside", "blockquote", "center", "details",
    +                  "details", "dir", "div", "dl", "fieldset", "figcaption", "figure",
    +                  "footer", "header", "hgroup", "main", "menu", "nav", "ol", "p",
    +                  "section", "summary", "ul"),
    +                 self.startTagCloseP),
    +                (headingElements, self.startTagHeading),
    +                (("pre", "listing"), self.startTagPreListing),
    +                ("form", self.startTagForm),
    +                (("li", "dd", "dt"), self.startTagListItem),
    +                ("plaintext", self.startTagPlaintext),
    +                ("a", self.startTagA),
    +                (("b", "big", "code", "em", "font", "i", "s", "small", "strike",
    +                  "strong", "tt", "u"), self.startTagFormatting),
    +                ("nobr", self.startTagNobr),
    +                ("button", self.startTagButton),
    +                (("applet", "marquee", "object"), self.startTagAppletMarqueeObject),
    +                ("xmp", self.startTagXmp),
    +                ("table", self.startTagTable),
    +                (("area", "br", "embed", "img", "keygen", "wbr"),
    +                 self.startTagVoidFormatting),
    +                (("param", "source", "track"), self.startTagParamSource),
    +                ("input", self.startTagInput),
    +                ("hr", self.startTagHr),
    +                ("image", self.startTagImage),
    +                ("isindex", self.startTagIsIndex),
    +                ("textarea", self.startTagTextarea),
    +                ("iframe", self.startTagIFrame),
    +                (("noembed", "noframes", "noscript"), self.startTagRawtext),
    +                ("select", self.startTagSelect),
    +                (("rp", "rt"), self.startTagRpRt),
    +                (("option", "optgroup"), self.startTagOpt),
    +                (("math"), self.startTagMath),
    +                (("svg"), self.startTagSvg),
    +                (("caption", "col", "colgroup", "frame", "head",
    +                  "tbody", "td", "tfoot", "th", "thead",
    +                  "tr"), self.startTagMisplaced)
    +            ])
    +            self.startTagHandler.default = self.startTagOther
    +
    +            self.endTagHandler = utils.MethodDispatcher([
    +                ("body", self.endTagBody),
    +                ("html", self.endTagHtml),
    +                (("address", "article", "aside", "blockquote", "button", "center",
    +                  "details", "dialog", "dir", "div", "dl", "fieldset", "figcaption", "figure",
    +                  "footer", "header", "hgroup", "listing", "main", "menu", "nav", "ol", "pre",
    +                  "section", "summary", "ul"), self.endTagBlock),
    +                ("form", self.endTagForm),
    +                ("p", self.endTagP),
    +                (("dd", "dt", "li"), self.endTagListItem),
    +                (headingElements, self.endTagHeading),
    +                (("a", "b", "big", "code", "em", "font", "i", "nobr", "s", "small",
    +                  "strike", "strong", "tt", "u"), self.endTagFormatting),
    +                (("applet", "marquee", "object"), self.endTagAppletMarqueeObject),
    +                ("br", self.endTagBr),
    +            ])
    +            self.endTagHandler.default = self.endTagOther
    +
    +        def isMatchingFormattingElement(self, node1, node2):
    +            if node1.name != node2.name or node1.namespace != node2.namespace:
    +                return False
    +            elif len(node1.attributes) != len(node2.attributes):
    +                return False
    +            else:
    +                attributes1 = sorted(node1.attributes.items())
    +                attributes2 = sorted(node2.attributes.items())
    +                for attr1, attr2 in zip(attributes1, attributes2):
    +                    if attr1 != attr2:
    +                        return False
    +            return True
    +
    +        # helper
    +        def addFormattingElement(self, token):
    +            self.tree.insertElement(token)
    +            element = self.tree.openElements[-1]
    +
    +            matchingElements = []
    +            for node in self.tree.activeFormattingElements[::-1]:
    +                if node is Marker:
    +                    break
    +                elif self.isMatchingFormattingElement(node, element):
    +                    matchingElements.append(node)
    +
    +            assert len(matchingElements) <= 3
    +            if len(matchingElements) == 3:
    +                self.tree.activeFormattingElements.remove(matchingElements[-1])
    +            self.tree.activeFormattingElements.append(element)
    +
    +        # the real deal
    +        def processEOF(self):
    +            allowed_elements = frozenset(("dd", "dt", "li", "p", "tbody", "td",
    +                                          "tfoot", "th", "thead", "tr", "body",
    +                                          "html"))
    +            for node in self.tree.openElements[::-1]:
    +                if node.name not in allowed_elements:
    +                    self.parser.parseError("expected-closing-tag-but-got-eof")
    +                    break
    +            # Stop parsing
    +
    +        def processSpaceCharactersDropNewline(self, token):
    +            # Sometimes (start of 
    , , and