From 642f2c6232214e259f81da98130bc639a92cb507 Mon Sep 17 00:00:00 2001 From: Jim Miller Date: Sat, 27 Dec 2014 11:39:54 -0600 Subject: [PATCH] Fix for first-time plugin install so it offers initial add-to-menu dialog. --- 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 | 1626 ++++++ calibre-plugin/translations/es.po | 1625 ++++++ calibre-plugin/translations/fr.po | 1624 ++++++ calibre-plugin/translations/messages.pot | 1567 ++++++ calibre-plugin/translations/pt_BR.po | 1624 ++++++ 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 | 2006 ++++++++ delete_fic.py | 59 + downloader.py | 302 ++ editconfig.html | 89 + epubmerge.py | 25 + example.ini | 103 + fanficdownloader.zip | Bin 0 -> 863242 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 | 315 ++ .../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 | 208 + 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 | 2006 ++++++++ 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, 87773 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..196a681 --- /dev/null +++ b/app.yaml @@ -0,0 +1,46 @@ +# ffd-retief-hrd fanfictiondownloader +application: fanfictiondownloader +version: 2-1-00 +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..13e4a85 --- /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 + +[www.ficwad.com] +#username:YourUsername +#password:YourPassword + +[www.twiwrite.net] +#username:YourName +#password:yourpassword +## default is false +#collect_series: true + +[www.adastrafanfic.com] +## Some sites do not require a login, but do require the user to +## confirm they are adult for adult content. +#is_adult:true + +[www.thewriterscoffeeshop.com] +#username:YourName +#password:yourpassword +#is_adult:true +## default is false +#collect_series: true + +[www.fictionalley.org] +#is_adult:true + +[www.harrypotterfanfiction.com] +#is_adult:true + +[www.fimfiction.net] +#is_adult:true +#fail_on_password: false + +[www.tthfanfic.org] +#is_adult:true +## tth is a little unusual--it doesn't require user/pass, but the site +## keeps track of which chapters you've read and won't send another +## update until it thinks you're up to date. This way, on download, +## it thinks you're up to date. +#username:YourName +#password:yourpassword + + +## 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..32bc434cddf34c0bfd9117d5ad14bdfe963455a2 GIT binary patch literal 863242 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 O`5f&G8gzoFr8p6=(G= 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

TMSO(*93Otz7CdawZ~`Yv&In$ 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 z

bI6xC>;205w}KugAPphtJ*n=NIZIVRs~M7C}8 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ⅇ9qf*1lvrj`7 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 z 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 z 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 #{k)A>rgCQ^d5#Z^s_Ycz}EbKu?=H_$V*d){eNe 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 z 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 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..e3fdd59 --- /dev/null +++ b/fanficdownloader/adapters/adapter_ksarchivecom.py @@ -0,0 +1,315 @@ +# -*- 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 '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