Compare commits

...
Author SHA1 Message Date
Jim Miller e8706b19fd Update CLI download zip. 2014-03-09 21:10:41 -05:00
Jim Miller dc86c90b9e Bump versions. 2014-03-09 21:09:47 -05:00
Jim Miller b90399d674 On auto-convert, delete target format else convert doesn't run. 2014-03-08 21:18:14 -06:00
Jim Miller 7c13b5aed9 Remove some dead code. 2014-03-08 15:45:16 -06:00
Jim Miller 8d87564158 Fix for AO3 https urls. 2014-03-08 15:45:03 -06:00
Jim Miller 1663f13950 Fix for seriesUrl check. 2014-03-08 15:44:51 -06:00
Jim Miller 10aedfbce6 Configurable connection timeout as per FaceDeer's recommendation. 2014-03-01 17:00:07 -06:00
Jim Miller d5dd328a05 Change Job Log output so URLs are more copy/pasteable. 2014-03-01 16:59:30 -06:00
Jim Miller d08bb7ac21 Fix for potionsandsnitches.net, extra text after date. 2014-03-01 16:59:02 -06:00
Jim Miller 6687601074 Added tag FanFictionDownLoader-4.4.94 for changeset 1873e173ef01 2014-02-28 17:10:35 -06:00
Jim Miller 4847a2b1b4 Added tag calibre-plugin-1.8.13 for changeset 1873e173ef01 2014-02-28 17:10:18 -06:00
Jim Miller 4718955ede Update CLI download zip. 2014-02-28 17:10:05 -06:00
Jim Miller d60ad9892d Fix for KeyError:'site' error and https with ffnet.
Also don't retry 404 errors and bump versions.
2014-02-28 17:08:07 -06:00
Jim Miller 35b5eb3dee Added tag FanFictionDownLoader-4.4.93 for changeset f603b2ffd630 2014-02-24 21:11:11 -06:00
Jim Miller f9d8e2f3db Added tag calibre-plugin-1.8.12 for changeset f603b2ffd630 2014-02-24 21:10:58 -06:00
Jim Miller f291625ae3 Update CLI download zip. 2014-02-24 21:10:46 -06:00
Jim Miller 6ef28cee6a Add tweak_fg_sleep feature set for ffnet, make ffnet chapter look ahead optional 2014-02-24 21:08:56 -06:00
Jim Miller fc3431098d Add slow_down_sleep_time to test1 adapter for... testing. 2014-02-21 22:59:03 -06:00
Jim Miller 8980eaf21b Set boolean custom columns from personal.ini when t/true/yes/y/1 vs
f/false/no/n/0.
2014-02-21 19:07:32 -06:00
Jim Miller c8a89fa8b2 More Fimf fixes from FaceDeer. 2014-02-21 19:05:37 -06:00
Jim Miller 683ad6182d Fix for calibre converted <body ...> tags. 2014-02-21 19:04:40 -06:00
Jim Miller 3267064576 Added tag FanFictionDownLoader-4.4.92 for changeset 43b0cca5b451 2014-02-15 09:45:07 -06:00
Jim Miller ab128c417c Added tag calibre-plugin-1.8.11 for changeset e3c25dc63ece 2014-02-15 09:44:35 -06:00
Jim Miller dbc9b5e250 Update CLI download zip. 2014-02-15 09:44:19 -06:00
Jim Miller fffdd89254 Update translations to include new strings (not yet translated). 2014-02-15 09:42:02 -06:00
Jim Miller cbb17aeba1 Bump versions. 2014-02-15 09:39:27 -06:00
Jim Miller 7433d2494f Increase default slow_down_sleep_time for ffnet. 2014-02-15 09:07:52 -06:00
Jim Miller d0fa72a002 Don't autoconvert on Update Calibre Only. 2014-02-11 10:48:48 -06:00
Jim Miller 7412acd007 Fix image download for ffnet, make buffynfaith.net use User-agent. 2014-02-10 13:27:33 -06:00
Jim Miller 196eff7f60 Add option to call Polish on GC to inject cover in epub. 2014-02-10 13:26:58 -06:00
Jim Miller 8f06a374cb Moved tag FanFictionDownLoader-4.4.91 to changeset 40b20c6d208b (from changeset 135173f84a14) 2014-02-05 19:07:23 -06:00
Jim Miller 4f5738108f Update CLI download zip. 2014-02-05 19:07:08 -06:00
Jim Miller 6f22a3c701 Added tag calibre-plugin-1.8.10 for changeset 135173f84a14 2014-02-05 18:58:20 -06:00
Jim Miller 589076eed9 Added tag FanFictionDownLoader-4.4.91 for changeset 135173f84a14 2014-02-05 18:58:06 -06:00
Jim Miller ee07ac8919 Bump versions. 2014-02-05 18:57:41 -06:00
Jim Miller dc5def5cc8 Change to storiesonline.net by davidfor to address overzealous culling in text. 2014-02-05 18:52:07 -06:00
Jim Miller 23ad8700bb Quick fix for storiesonline.net & merging heuristics change. 2014-02-01 18:54:31 -06:00
asbjorn grandt 250121e137 Fix divide by zero error on chapters with no content. 2014-02-02 00:55:34 +01:00
Jim Miller 56a639508e Translations(fr&de) for auto-convert feature strings. nbsp for French. 2014-01-28 13:14:07 -06:00
Jim Miller d398b62c1a Add auto-convert on download/update feature. Still need translations. 2014-01-26 11:57:49 -06:00
Jim Miller bff3cfbd25 Fix for typo in French that breaks FFDL. 2014-01-22 17:04:51 -06:00
Jim Miller 34558b1653 Added tag calibre-plugin-1.8.09 for changeset ca8fa4c502c5 2014-01-22 10:08:33 -06:00
Jim Miller 0ce37974e2 calibre/python doesn't like 0 leading version numers anymore? Bump to 1.8.9 2014-01-22 10:07:09 -06:00
Jim Miller 929cf36e79 Added tag calibre-plugin-1.8.08 for changeset ba5441b903ae 2014-01-21 20:43:18 -06:00
Jim Miller b86c15d5a6 Bump calibre version only. 2014-01-21 20:43:06 -06:00
Jim Miller df622dfbf0 Allow literal strings in custom_columns_settings to set columns. 2014-01-21 20:42:04 -06:00
Jim Miller 2c16b5e148 Added tag FanFictionDownLoader-4.4.90 for changeset f1ff0a1eed93 2014-01-13 19:09:35 -06:00
Jim Miller 86ce293f78 Update CLI download 4.4.90 2014-01-13 19:09:02 -06:00
Jim Miller 63e8fc8bb2 Added tag calibre-plugin-1.8.07 for changeset 5aa783520099 2014-01-13 19:05:50 -06:00
Jim Miller 38ab66907b Bump versions. 2014-01-13 19:05:36 -06:00
Jim Miller 1dedde2f9d Fix for hpfanficarchive.com changes. 2014-01-13 18:42:36 -06:00
Jim Miller fc5dd20853 Updated French translation. 2014-01-08 17:05:07 -06:00
Jim Miller 4e9fe9f86f First version of French translation. 2014-01-08 14:56:09 -06:00
Jim Miller 7325be98f5 davidfor's changes for storiesonline.net universe/series. 2014-01-05 18:30:58 -06:00
Jim Miller e2545a0da8 Encoding, series & universe for storiesonline.net. 2014-01-05 10:06:03 -06:00
Jim Miller 5cd3dba4a6 Default utf8 first for finestories.com. 2014-01-04 12:11:47 -06:00
Jim Miller bc2420d143 Fix for 'update' with no updates and 'mark' enabled. 2014-01-04 12:11:13 -06:00
Jim Miller 099e8a62ca Added tag FanFictionDownLoader-4.4.89 for changeset d8f5d11b860f 2013-12-23 17:49:48 -05:00
Jim Miller 7f9e22e1b9 Update index and CLI download zip. 2013-12-23 17:49:28 -05:00
Jim Miller a46532bc72 Added tag calibre-plugin-1.8.06 for changeset dd7269e25be8 2013-12-23 17:46:44 -05:00
Jim Miller f7429140f8 Bump versions. 2013-12-23 17:45:19 -05:00
Jim Miller 7872b25034 Minor tweak to ffnet fix in case they change it back, sleep on ffnet meta lookup. 2013-12-23 17:45:08 -05:00
Jim Miller 3d468a4dc5 New site: literotica.com, tweaks to htmlheuristics for it. 2013-12-23 17:43:32 -05:00
asbjorn grandt 413525a881 url for fictionratings changed to https, breaking the soup find. 2013-12-23 08:14:10 +01:00
Jim Miller c066a809c7 Added tag FanFictionDownLoader-4.4.88 for changeset 7ed8deaec5d2 2013-12-12 18:10:51 -06:00
Jim Miller 443b2b5ef8 Save CLI download version *4.4.88* 2013-12-12 18:10:35 -06:00
Jim Miller 3e7f05ba44 Save CLI download version 4.4.87 2013-12-12 18:09:22 -06:00
Jim Miller 25e65da239 Added tag calibre-plugin-1.8.05 for changeset 8254e19f1ab2 2013-12-12 18:03:35 -06:00
Jim Miller c5f211f49f Moved tag calibre-plugin-1.8.04 to changeset 6e301aeb7f4b (from changeset e45187f4215e) 2013-12-12 18:03:26 -06:00
Jim Miller 20fff58813 Moved tag calibre-plugin-1.8.03 to changeset ea92e962bcaa (from changeset 6e301aeb7f4b) 2013-12-12 18:03:00 -06:00
Jim Miller 8fe250747c Added tag calibre-plugin-1.8.04 for changeset e45187f4215e 2013-12-12 17:59:56 -06:00
Jim Miller 4d41c28f3c Fix a bug with AO3 single chapter stories introduced with do_update_hook. 2013-12-12 17:59:41 -06:00
Jim Miller f8d1b21007 Moved tag FanFictionDownLoader-4.4.87 to changeset 8d78c6f57151 (from changeset 6e301aeb7f4b) 2013-12-12 13:35:55 -06:00
Jim Miller 72861e6cd5 Save CLI Zip file. 2013-12-12 13:35:30 -06:00
Jim Miller bd9dba5813 Added tag FanFictionDownLoader-4.4.87 for changeset 6e301aeb7f4b 2013-12-12 13:33:46 -06:00
Jim Miller 1478df572e Added tag calibre-plugin-1.8.03 for changeset 6e301aeb7f4b 2013-12-12 13:33:30 -06:00
Jim Miller 68466af944 Bump versions. 2013-12-12 13:33:13 -06:00
Jim Miller 7c9c84256c Graceful fail for 'k'HTML when no 'k'Url, bump min calibre version. 2013-12-11 12:14:55 -06:00
Jim Miller fcbfe96c64 Add do_update_hook to AO3 for updated chapters in the middle, trekiverse alt URL,
reorder defaults.ini files to put user/pass is_adult first in all sections.
2013-12-11 12:13:22 -06:00
Jim Miller e47748dbbc Added tag calibre-plugin-1.8.02 for changeset a277cb866377 2013-12-05 13:16:45 -06:00
Jim Miller 91a6c668d1 Added tag FanFictionDownLoader-4.4.86 for changeset a277cb866377 2013-12-05 13:16:22 -06:00
Jim Miller 4875265fd3 Bump versions. 2013-12-05 13:15:59 -06:00
Jim Miller bf651df38f Add prequels/sequels & fix groups site specific metadata for fimfiction.net. 2013-12-05 13:15:46 -06:00
Jim Miller 77e2d7beff New site: trekiverse.org/efiction/ 2013-12-03 21:56:46 -06:00
Jim Miller 9c85563ffd Fix: identifier searchs, overwrite newer with ffnet timestamps. 2013-12-02 12:49:16 -06:00
Jim Miller e217c6fe23 Fix: Reject lines shouldn't be translated 2013-12-02 12:48:44 -06:00
Jim Miller 17b6d090db Fix for higher rated stories with not logged in on TtH. 2013-11-30 16:55:00 -06:00
Jim Miller 4e1e04ef06 Update translations, add German. 2013-11-30 16:54:36 -06:00
Jim Miller c81ee20151 Added tag FanFictionDownLoader-4.4.85 for changeset dbc1c8e89543 2013-11-25 12:54:07 -06:00
Jim Miller eb5b447b09 Added tag calibre-plugin-1.8.01 for changeset dbc1c8e89543 2013-11-25 12:53:46 -06:00
Jim Miller 857ae5b7fa Fix dc:language issue with defaulting to English. Bump versions. 2013-11-25 12:48:17 -06:00
Jim Miller 1037fff11f Disable nook_img_fix if replace_br_with_p. 2013-11-25 12:25:11 -06:00
Jim Miller f611a1e26f Correction for 'Default' GC setting. 2013-11-25 11:06:42 -06:00
asbjorn grandt f3cfa72051 New code to correct the HTML when changing breaks to paragraphs. Example. <em> tags would not be properly terminated. 2013-11-25 09:31:39 +01:00
Jim Miller f13a698808 Fix for 1000+ chapters(commas) and 'Default' GC with translations. 2013-11-24 22:51:15 -06:00
Jim Miller 18064c269b Update index.html. 2013-11-18 07:49:57 -06:00
Jim Miller ccb4af4f63 Added tag FanFictionDownLoader-4.4.84 for changeset 9039b37d99c7 2013-11-18 07:47:53 -06:00
Jim Miller 0ee30ddeac Added tag calibre-plugin-1.8.00 for changeset 9039b37d99c7 2013-11-18 07:47:39 -06:00
Jim Miller 2bb5e5b74e Bump versions. 2013-11-18 07:47:21 -06:00
Jim Miller f21553bc4a Improved error messaging for ficwad & squidge.org/peja 2013-11-18 07:40:59 -06:00
Jim Miller 16ff8a1a23 Fix for ffnet crossover categories. 2013-11-18 07:40:29 -06:00
Jim Miller 045d43c040 Change checkforurlchange tooltip for ffnet http vs https change. 2013-11-16 10:07:40 -06:00
Jim Miller f3d88d73f3 ffnet fix & update: dates as characters, http->https. 2013-11-16 09:23:55 -06:00
Jim Miller 943d34ba35 Adding i18n/l10n support. Involves much changing of user messages. 2013-11-15 14:19:44 -06:00
Jim Miller ba022ec34e Added tag FanFictionDownLoader-4.4.83 for changeset e81d1c1f8bd4 2013-11-13 12:19:12 -06:00
Jim Miller c1914651aa Added tag calibre-plugin-1.7.54 for changeset e81d1c1f8bd4 2013-11-13 12:18:56 -06:00
Jim Miller dfef356249 Bump PI version. 2013-11-13 12:18:40 -06:00
Jim Miller d78709f850 Fix for ffnet's timestamp published/updated. 2013-11-13 10:56:39 -06:00
asbjorn grandt d6e8e69eaf They use two different date formats now. 2013-11-13 12:50:30 +01:00
asbjorn grandt 0626649f1c Fix latest FF.net shenanigans.
They changed the dateformat for the "Published' and 'Updated' sections from
%m-%d-%y (ie. 11-13-13) to %m/%d/%Y (ie. 11/13/2013)
2013-11-13 12:25:26 +01:00
Jim Miller 565569e819 Update index.html 2013-11-11 09:55:32 -06:00
Jim Miller 97e3f5796b Added tag FanFictionDownLoader-4.4.82 for changeset c5de562a2ea3 2013-11-11 09:46:18 -06:00
Jim Miller c1a3abb773 Added tag calibre-plugin-1.7.53 for changeset c5de562a2ea3 2013-11-11 09:46:06 -06:00
Jim Miller fc0f970c61 Fix for yet another ffnet change, bump versions. 2013-11-11 09:45:36 -06:00
asbjorn grandt 4208f4ae6e Fixed a problem where an html comment was detected as a valid tag, and crashed
the soup processing.
2013-11-10 19:47:28 +01:00
asbjorn grandt d89a5132ce Restructuring, and added a pre parse of the content to catch unincapsulated segments, fixing for instance http://archiveofourown.org/works/1036957 2013-11-10 15:30:19 +01:00
Jim Miller a763fc3650 Added tag FanFictionDownLoader-4.4.81 for changeset 331c4a525da1 2013-11-09 11:27:21 -06:00
Jim Miller c37b91a093 Added tag calibre-plugin-1.7.52 for changeset 331c4a525da1 2013-11-09 11:27:08 -06:00
Jim Miller 4f85d4eb41 Bump versions, update web service to announce fimf blocking. 2013-11-09 11:26:46 -06:00
Jim Miller 44f42c0db7 Additional fixes for storiesonline.net from davidfor. 2013-11-09 10:45:44 -06:00
Jim Miller 3e3b24e921 Partial _() translation--fixes for where it already was, but wrong. 2013-11-09 10:44:52 -06:00
asbjorn grandt d0a952ac3a Added code preparing for an "aggressive" mode, currently commented out though.
Also cleaned up paragraphs a little more, mostly cosmetic.
2013-11-06 20:32:37 +01:00
asbjorn grandt e8e4180621 Added "shielding" for break tags inside paragraphs, pre and blockquote tags. These should be left alone. 2013-11-06 11:37:56 +01:00
asbjorn grandt 7d29b281b7 Added improved paragraph detection for stories that have fixed length lines, usually copied from an email or similar source. 2013-11-05 11:35:35 +01:00
Jim Miller 98460d785d Minor fix for 'groups' site specific metadata on fimf. 2013-11-02 17:02:53 -05:00
asbjorn grandt 37803690e5 Small change in how the paragraphs are detected, and it now uses an empty
paragraph for sections breaks with one additional break than the paragraph
 break, and <hr /> for anything above that.
2013-11-01 08:11:09 +01:00
asbjorn grandt c6ddd8e6d7 base_adapter were removing empty paragraphs used as section breaks on some sites (ie. AO3) 2013-11-01 08:08:44 +01:00
asbjorn grandt 3b04b6ad61 TtH.org sometimes place a div tag around hr tags, this caused the heuristics to generate faulty html
Also TtH adds an empty div at the end, when placed inside the last paragraph, the ePub will fail.
2013-10-31 22:07:07 +01:00
Jim Miller f046605517 Added tag FanFictionDownLoader-4.4.80 for changeset 20bfb189709d 2013-10-30 17:28:00 -05:00
Jim Miller e504ee29c1 Added tag calibre-plugin-1.7.51 for changeset 20bfb189709d 2013-10-30 17:27:45 -05:00
Jim Miller 22994d203a Bump versions, make ready for release. 2013-10-30 17:27:30 -05:00
Jim Miller cbc02b749b Add site storiesonline.net. 2013-10-29 23:20:05 -05:00
Jim Miller 8761b766ca Minor fix for finestories.com. 2013-10-29 23:19:09 -05:00
Jim Miller 44bd7f6319 Handling for fictionpad.com empty chapters. "This chapter has no text." 2013-10-29 22:19:12 -05:00
asbjorn grandt 9d8508ee6f htmlheuristics needed to chek if the parsed body actually had any tags in it,
and is now removing ASCII character x!0 (whitespace) as it interfered with
  regexp.
base_adapter called replace_br_with_p with two argument, where teh function
  only support one.
2013-10-27 23:01:09 +01:00
Jim Miller df5a91daed Incorporating heuristic br->p tag processing as an optional feature. 2013-10-26 15:52:40 -05:00
Jim Miller 2195ea5792 Add fanfiction.net's new 'pairings'. 2013-10-26 10:04:02 -05:00
Jim Miller 03da5f8eb8 Added tag FanFictionDownLoader-4.4.79 for changeset 75de8b7a66ce 2013-10-23 19:32:09 -05:00
Jim Miller 1b9412e36e Added tag calibre-plugin-1.7.50 for changeset 75de8b7a66ce 2013-10-23 19:31:39 -05:00
Jim Miller ba7b718170 Fix for latest ffnet tweak. 2013-10-23 19:31:28 -05:00
Jim Miller a43d9f7a03 Added tag fanfictionDownLoader-4.4.78 for changeset 3f7539804f25 2013-10-19 11:24:42 -05:00
Jim Miller 4b17ecf6fa Added tag calibre-plugin-1.7.49 for changeset 3f7539804f25 2013-10-19 11:24:28 -05:00
Jim Miller b6dd579c93 Bump versions. 2013-10-19 11:24:18 -05:00
Jim Miller aa685a4c7d Reorg Basic config a little and wrap the tab widget in a scrollarea. 2013-10-18 15:01:26 -05:00
Jim Miller b0248daf07 dark-solace.org is now insisting on www.dark-solace.org, for login at least. 2013-10-17 19:26:31 -05:00
Jim Miller be5fe49ab8 Added tag FanFictionDownLoader-4.4.77 for changeset e85271dd28c9 2013-10-13 14:33:42 -05:00
Jim Miller f42f440f1b Added tag calibre-plugin-1.7.48 for changeset e85271dd28c9 2013-10-13 14:33:33 -05:00
Jim Miller cbf50a36ee Trust ffnet to change their html right after I released a new version. 2013-10-13 14:33:26 -05:00
Jim Miller fda0fda84e Added tag FanFictionDownLoader-4.4.76 for changeset c60e9ebb849d 2013-10-13 11:00:00 -05:00
Jim Miller c6f5c524be Added tag calibre-plugin-1.7.47 for changeset c60e9ebb849d 2013-10-13 10:59:48 -05:00
Jim Miller 00a46a7cc0 Bump versions, make "Mark added/updated books when finished" default False. 2013-10-13 10:59:36 -05:00
Jim Miller 5c1ca5a188 Mark failed updates even when all failed. 2013-10-12 11:16:31 -05:00
Jim Miller 17c6dddfac Add user/pass support for 18+ rated stories on fictionpad.com 2013-10-11 18:14:35 -05:00
Jim Miller 33451f1119 Make marking updated books optional since it's now visible. 2013-10-11 15:04:45 -05:00
Jim Miller 192ade1fca Adding fictionpad.com. Doesn't support login and 18+ yet. 2013-10-11 15:04:21 -05:00
Jim Miller aa286a9d0d Change prints to logger calls and clean up some of them. 2013-10-10 21:34:46 -05:00
Jim Miller a1c19ac12e Added tag FanFictionDownLoader-4.4.75 for changeset 3779dfd8a385 2013-09-29 14:22:03 -05:00
Jim Miller 85b6e305be Added tag calibre-plugin-1.7.46 for changeset 3779dfd8a385 2013-09-29 14:21:46 -05:00
Jim Miller 40fb061a86 Bump versions. 2013-09-29 14:21:33 -05:00
Jim Miller 7fdc59691f do_update_hook for fimf, find newest chapter and update from there. 2013-09-29 14:19:18 -05:00
Jim Miller 78845d0d1e TtH uses both Non-BtVS/... and Non-BTVS/... 2013-09-29 14:17:41 -05:00
Jim Miller 786b1d5cdf Exclude indeath.net emoticons from being covers. 2013-09-26 21:45:32 -05:00
besnef b5d176f007 Merge fix for inDeath.net 2013-09-26 22:20:19 -04:00
besnef 1c71dedfa3 Fix for inDeath.net 2013-09-21 14:57:45 -04:00
Jim Miller 2484f0f5c7 Added tag FanFictionDownLoader-4.4.74 for changeset 53f662218344 2013-09-21 13:24:25 -05:00
Jim Miller a087927929 Added tag calibre-plugin-1.7.45 for changeset 53f662218344 2013-09-21 13:24:13 -05:00
Jim Miller e64d49e3e6 Bump versions. 2013-09-21 13:23:58 -05:00
Jim Miller d9ad95467b Set custom column only if there's a value (mostly for int/float columns). 2013-09-21 12:53:28 -05:00
Jim Miller 668e0e08b5 Add ffnet warning to status page on error. 2013-09-16 19:06:44 -05:00
Jim Miller 2b0b4ca2af Don't include empty string tags. 2013-09-16 18:55:02 -05:00
Jim Miller dca3707eac Fix for whofic.com. 2013-09-16 13:10:09 -05:00
Jim Miller a3b23857a3 Added tag FanFictionDownLoader-4.4.73 for changeset 07134453ebb0 2013-09-14 12:25:49 -05:00
Jim Miller 9cdccb576c Added tag calibre-plugin-1.7.44 for changeset 07134453ebb0 2013-09-14 12:25:28 -05:00
Jim Miller cf24021f14 Bump versions. 2013-09-14 12:25:17 -05:00
Jim Miller 0bbe6287b6 *Really* commit changes to portkey.org. 2013-09-12 22:57:04 -05:00
Jim Miller 0c585b6e52 Remove call do dateutil.parser in fimf. Requires dateutil package for CLI. 2013-09-12 22:41:09 -05:00
Jim Miller aacf6dc6a2 Switch portkey.org to non-stone soup and brute-force remove <HTML> tags. 2013-09-12 22:40:41 -05:00
Jim Miller 9f56952950 Fix for empty word num into int column. 2013-09-12 19:11:30 -05:00
Jim Miller c49cad8889 Added tag FanFictionDownLoader-4.4.72 for changeset 75db10f77cb4 2013-09-11 10:34:07 -05:00
Jim Miller bf1d8e18bf Added tag calibre-plugin-1.7.43 for changeset 75db10f77cb4 2013-09-11 10:33:55 -05:00
Jim Miller e41c7dabeb Fix fimfic dates, add fimfic site specific 'groups' metadata. Bump versions. 2013-09-11 10:32:57 -05:00
Jim Miller face0af074 Add default_value_status (etc) feature. 2013-09-10 10:24:45 -05:00
Jim Miller 02f38eaa2e Added tag FanFictionDownLoader-4.4.71 for changeset b4e7c2ec827d 2013-09-09 09:11:47 -05:00
Jim Miller d4b5438bc5 Added tag calibre-plugin-1.7.42 for changeset b4e7c2ec827d 2013-09-09 09:11:29 -05:00
Jim Miller 83db111ba7 Changes for ffnet--longer sleep, config UA, bump versions. 2013-09-09 09:10:56 -05:00
Jim Miller 2eb8c27c80 fanfiction.net blocked by IP - index.html 2013-09-08 14:55:52 -05:00
Jim Miller a79066750c Changes on index.html. 2013-09-08 14:48:05 -05:00
Jim Miller 3857a6a89b Added tag FanFictionDownLoader-4.4.70 for changeset 9d99f91e062e 2013-09-08 14:33:14 -05:00
Jim Miller 016d4af149 Added tag calibre-plugin-1.7.41 for changeset 9d99f91e062e 2013-09-08 14:33:00 -05:00
Jim Miller a4e499866e Fix for AO3 stories without series, bump versions. 2013-09-08 14:32:41 -05:00
Jim Miller 5736f4f181 Added tag calibre-plugin-1.7.40 for changeset db9a23c65412 2013-09-07 14:43:55 -05:00
Jim Miller e04453d73f Removed tag calibre-plugin-1.7.340 2013-09-07 14:43:47 -05:00
Jim Miller c3778502dc Added tag FanFictionDownLoader-4.4.69 for changeset db9a23c65412 2013-09-07 14:42:23 -05:00
Jim Miller 925b12e776 Added tag calibre-plugin-1.7.340 for changeset db9a23c65412 2013-09-07 14:41:47 -05:00
Jim Miller 62ed503d10 Bump versions. 2013-09-07 14:41:30 -05:00
Jim Miller 7e35d0b710 Add User-agent="FFDL/1.7" for all adapters. (Remove from specific adapters.) 2013-09-07 14:36:48 -05:00
Jim Miller 20c7e7f075 Fixes for changes to harrypotterfanfictioncom. 2013-09-07 14:36:13 -05:00
Jim Miller 9f61cff7cd Additional series as site specific data for AO3. 2013-09-07 14:35:32 -05:00
Jim Miller cb5587b0c7 Fix for calibre pre-1.0.0. 2013-09-07 14:32:48 -05:00
Jim Miller f2a74c9ffc Better doc section override order in ini files. 2013-09-04 14:33:29 -05:00
Jim Miller eb0f013550 Added tag calibre-plugin-1.7.39 for changeset 1af0c2a05450 2013-08-31 19:57:27 -05:00
Jim Miller 6674e19f94 Remove extra code for unchanged author link for calibre 1.0.0, bump version. 2013-08-31 19:50:18 -05:00
Jim Miller 29b1ca9a7e Attempted debuging for another Mac issue. 2013-08-31 19:42:58 -05:00
Jim Miller 7cae65300a Feature: add_genre_when_multi_category setting 2013-08-31 15:38:44 -05:00
Jim Miller 88fe13f603 Work around calibre's auto-split when author contains 'and' 'with' ',' or '&'. 2013-08-29 19:23:23 -05:00
Jim Miller a9b6989341 Added tag calibre-plugin-1.7.38 for changeset d44d5f1006f6 2013-08-27 17:11:43 -05:00
Jim Miller a54277058c Bump version. 2013-08-27 17:11:32 -05:00
Jim Miller 4c27e19aa8 'Fix' author link updates bumping last_modified. User msg on metadata error. 2013-08-27 14:16:44 -05:00
Jim Miller 6f5ed2bcac Added tag calibre-plugin-1.7.37 for changeset 73fa3ac021d3 2013-08-24 22:43:11 -05:00
Jim Miller cc0bccd420 Added tag calibre-plugin-1.7.37 for changeset 73fa3ac021d3 2013-08-24 22:40:12 -05:00
Jim Miller 85bbcac92e Fold only-case-different tags together to work around calibre bug 1216398
https://bugs.launchpad.net/calibre/+bug/1216398
2013-08-24 22:39:36 -05:00
Jim Miller f8776e118a Remove superfluous Fanfiction category set in adapter. 2013-08-24 22:38:30 -05:00
Jim Miller 31b7257496 Strip HTML from story titles in all the adapters that don't already. 2013-08-21 09:19:58 -05:00
Jim Miller b5d00951de Added tag calibre-plugin-1.7.36 for changeset e6f30eaf1bc7 2013-08-20 08:47:30 -05:00
Jim Miller c73f7693a4 Bump PI version. 2013-08-20 08:47:18 -05:00
Jim Miller e35927c9a2 Add options to force author & title values into author_sort and title_sort. 2013-08-18 17:19:26 -05:00
Jim Miller e8e93b010a Make check for changing story URL configurable. 2013-08-18 15:52:51 -05:00
Jim Miller 6414a09dbd Added tag FanFictionDownLoader-4.4.68 for changeset db28f113a88e 2013-08-06 17:18:42 -05:00
Jim Miller d5e7907068 Added tag calibre-plugin-1.7.35 for changeset db28f113a88e 2013-08-06 17:18:29 -05:00
Jim Miller 3175346214 Warn if no chapters on update in CLI, too. Bump versions. 2013-08-06 17:18:03 -05:00
Jim Miller 8b31ba59af Warning if existing epub has no recognizable chapters. 2013-08-02 13:39:36 -05:00
Jim Miller 92c60d065e Get story url lists from sites with adapter, get urls from non-fic sites in CLI. 2013-07-30 23:08:56 -05:00
Jim Miller ba6852eec4 Remove logger.debug("storyId: ... from all adapters. 2013-07-30 21:57:22 -05:00
Jim Miller 72086e6ecf Optimization for adapter lookup and anyurl (on site) adapter lookup. 2013-07-30 21:56:33 -05:00
Jim Miller f8a2cb949f Added tag calibre-plugin-1.7.34 for changeset c40bd6f33a0e 2013-07-28 16:50:37 -05:00
Jim Miller 07d1a1096f Added tag FanFictionDownLoader-4.4.67 for changeset c40bd6f33a0e 2013-07-28 16:50:26 -05:00
Jim Miller 9f4fe192be Bump versions. 2013-07-28 16:50:05 -05:00
Jim Miller 56cd2b2039 Fixes for dark-solace.org/elysian pagination of author/series story lists. 2013-07-28 16:46:00 -05:00
Jim Miller b2a41a9b10 Use FFDL/1.7 User-Agent with hpfanficarchive.com because of python blocking. 2013-07-28 10:53:47 -05:00
Jim Miller 55e18f9446 Add is_adult to *chapter* downloads for storiesofarda.com. 2013-07-26 12:28:53 -05:00
Jim Miller 855c284389 Another fix for fanfiktion.de changes. 2013-07-25 12:02:49 -05:00
Jim Miller a31de1d5ef Added tag calibre-plugin-1.7.33 for changeset 9f7be11b5754 2013-07-24 13:02:25 -05:00
Jim Miller 7ac00b53ab Fix for smarten_punctuation setting in pre-0.9.39 calibre. 2013-07-24 13:02:14 -05:00
Jim Miller 9c0519316d Added tag FanFictionDownLoader-4.4.66 for changeset c32e4193a161 2013-07-23 15:19:01 -05:00
Jim Miller d9fbccbef5 Added tag calibre-plugin-1.7.32 for changeset c32e4193a161 2013-07-23 15:18:48 -05:00
Jim Miller 0c622b379e Bump versions. 2013-07-23 15:18:37 -05:00
Jim Miller 83d210566b Fixes for fanfiktion.de & thehexfiles.net, add entries to teststory valid list. 2013-07-23 15:17:16 -05:00
Jim Miller ab0b25317b Added tag FanFictionDownLoader-4.4.65 for changeset 1def4c08104d 2013-07-18 11:05:16 -05:00
Jim Miller e10ae5fbe4 Added tag calibre-plugin-1.7.31 for changeset 1def4c08104d 2013-07-18 11:04:59 -05:00
Jim Miller 62eb103ff5 Bump versions, update ini files for new parameters. 2013-07-18 11:04:47 -05:00
Jim Miller 8e984e62fb Only do Smarten Punctuation on calibre 0.9.39+. 2013-07-18 11:00:10 -05:00
Jim Miller fd270789ac Smarten Punctuation option on epub download/update. 2013-07-17 16:53:00 -05:00
Jim Miller f6bd941369 Drag and drop onto the plugin button on toolbar. 2013-07-17 15:21:39 -05:00
Jim Miller 54ae77082a sort_ships option. Ship b/a is changed to a/b, etc. 2013-07-15 14:17:52 -05:00
Jim Miller b3c30f6a21 Make output_filename_safepattern configurable. 2013-07-13 16:46:54 -05:00
Jim Miller 84bc9b3a3e Add join_string_<entry>, keep_in_order_<entry> and replace_metadata <entry>_LIST
options.
2013-07-12 15:36:36 -05:00
Jim Miller e59673fdd7 Change CLI u/--update-epub option to update existing file when given URL, too. 2013-07-12 15:34:54 -05:00
Jim Miller 8d2b9646fb Commit Previous Version update. 2013-07-10 14:24:20 -05:00
Jim Miller 7fc3a2ad2d Added tag FanFictionDownLoader-4.4.64 for changeset d771f0b4b2f2 2013-07-10 14:17:27 -05:00
Jim Miller 8c0a216e46 Make 'supported list' generated from adapter Examples. Bump web version. 2013-07-10 14:17:06 -05:00
Jim Miller 9b08fe3bfb Added tag calibre-plugin-1.7.30 for changeset d1e6cd170b48 2013-07-09 19:29:54 -05:00
Jim Miller b4ffb2f9d9 Added tag FanFictionDownLoader-4.4.63 for changeset d1e6cd170b48 2013-07-09 19:29:45 -05:00
Jim Miller f276ca081f Bump versions. 2013-07-09 19:29:16 -05:00
iatheia dc2116dd3e New adapter for scarhead.net 2013-07-08 22:19:35 -04:00
Jim Miller 737bed8bfa GUI improvements to Basic config and Add/Update dialogs. 2013-07-08 15:58:04 -05:00
Jim Miller 178d83d733 Redo Basic config layout into better groupings. 2013-07-05 16:08:54 -05:00
Jim Miller ccc07676fb Phoenixsong can have individual chapters req login. Add force_login parameter. 2013-07-03 09:51:39 -05:00
Jim Miller a6b9ca52bb Improved error handling when ffnet is having problems. 2013-07-02 23:40:10 -05:00
Jim Miller cbb8eb14db Added tag FanFictionDownLoader-4.4.62 for changeset 1fd9bacc4f62 2013-06-28 11:06:25 -05:00
Jim Miller b472f8d216 Added tag calibre-plugin-1.7.29 for changeset 1fd9bacc4f62 2013-06-28 11:06:08 -05:00
Jim Miller a4d97500b5 Bump versions. 2013-06-28 11:05:30 -05:00
Jim Miller 22d9a674a3 Allow domain fanfic.mugglenet.com for mugglenet.com. 2013-06-28 10:09:26 -05:00
iatheia 1e908c3440 New adapter for fanfic.potterheadsanonymous.com and www.simplyundeniable.com (latter needs user input for restricted stories) 2013-06-28 00:06:29 -04:00
Jim Miller ec7c02f2ec Added tag calibre-plugin-1.7.28 for changeset 1a40ab7c9ba4 2013-06-22 19:20:18 -05:00
Jim Miller 2049910bb0 Added tag FanFictionDownLoader-4.4.61 for changeset 1a40ab7c9ba4 2013-06-22 19:20:03 -05:00
Jim Miller 1e61bebf21 Fix for siye.co.uk stories with HTML in the title, bump versions. 2013-06-22 19:19:42 -05:00
Jim Miller 7f47100db4 Couple minor fixes. 2013-06-18 13:53:21 -05:00
Jim Miller 83b48d15d8 Added tag FanFictionDownLoader-4.4.60 for changeset de6789e70c76 2013-06-10 21:12:06 -05:00
Jim Miller 5aec8188a2 Added tag calibre-plugin-1.7.27 for changeset de6789e70c76 2013-06-10 21:11:53 -05:00
Jim Miller 5a34f4c86a Bump versions, update index.html. 2013-06-10 21:11:39 -05:00
Jim Miller 8e5ba7b634 Restrict url list search on scarvesandcoffee.net. 2013-06-10 19:46:25 -05:00
Jim Miller 7a47d1fad2 Fix for scarvesandcoffee.net taking author from featured stories. 2013-06-10 19:45:24 -05:00
Jim Miller 7f4749a022 Fix for nha.magical-worlds.us: reviews link disappeared, fix finding story url for
meta section, fix highbyte chars in description.
2013-06-10 19:44:58 -05:00
Jim Miller b44f059e57 Added tag calibre-plugin-1.7.26 for changeset 127933a4d5f3 2013-06-09 11:54:53 -05:00
Jim Miller 6b9058a9eb Added tag FanFictionDownLoader-4.4.59 for changeset 127933a4d5f3 2013-06-09 11:54:30 -05:00
Jim Miller 5acf9a8d0b Add 'Download as New Book?' dialog after 'Change Story URL?', fix author URLs when new author. 2013-06-08 23:11:55 -05:00
Jim Miller 454c7ffb2f Fall back category parse for ffnet when broken crossover cat link. 2013-06-08 23:10:51 -05:00
Jim Miller f6dcb447b0 Bump versions, update index.html. 2013-05-30 21:37:48 -05:00
Jim Miller 5ce064bf92 Fix so non-anthology numeric custom columns populate correctly. 2013-05-30 19:37:50 -05:00
Jim Miller c9a1537190 Fix for numeric site specific values into float/int custom columns. 2013-05-30 12:52:57 -05:00
Jim Miller 36e192e82c Add Read & Review counts to dramione.org and grangerenchanted.com 2013-05-30 12:52:27 -05:00
iatheia 42058d02b3 Adapter for nickandgreg.net 2013-05-30 01:18:53 -04:00
Jim Miller e0832b9deb Update index.html 2013-05-26 16:15:19 -05:00
Jim Miller 4ccb94fca0 Added tag FanFictionDownLoader-4.4.58 for changeset b53a5015e7b3 2013-05-26 16:07:46 -05:00
Jim Miller 0e5b64bee0 Added tag calibre-plugin-1.7.25 for changeset b53a5015e7b3 2013-05-26 16:07:30 -05:00
Jim Miller 53a325ffa3 Bump versions. 2013-05-26 16:07:19 -05:00
Jim Miller 29f900199e Configurable testing stories in test1.com. 2013-05-22 21:18:36 -05:00
Jim Miller f56fb0efed Add "keep_style_attr" option. 2013-05-22 21:14:56 -05:00
Jim Miller a703011aef Correct a misspelling. 2013-05-02 13:16:53 -05:00
Jim Miller a635272bcf Added tag FanFictionDownLoader-4.4.57 for changeset c5d34faaec8f 2013-04-28 11:35:48 -05:00
Jim Miller 1079ed565c Added tag calibre-plugin-1.7.24 for changeset c5d34faaec8f 2013-04-28 11:35:33 -05:00
Jim Miller 17ab4a3d25 Bump versions. 2013-04-28 11:35:23 -05:00
Jim Miller 2566e434ee Tweak 'skip update when the story URL would be changed' feature. 2013-04-28 11:34:46 -05:00
Jim Miller 91f5269453 Offer user chance to skip update when the story URL would be changed. 2013-04-27 18:17:41 -05:00
Jim Miller 235f00a5e6 After adding to reject list, delete only rejects, not those removed from list. 2013-04-27 17:36:28 -05:00
Jim Miller aad4a26131 Don't strip lead/trail whitespace from replace_metadata, add \s->' ' in replacements 2013-04-26 10:40:13 -05:00
Jim Miller f20a02a2bc Catch errors on db.set_cover(). 2013-04-25 22:00:58 -05:00
Jim Miller 1ba1da4d65 Moved tag FanFictionDownLoader-4.4.56 to changeset 6ea4b9910113 (from changeset 0252e03d11c4) 2013-04-12 10:35:27 -05:00
Jim Miller a951fc3d6c Fix index.html. Really this time. 2013-04-12 10:35:08 -05:00
Jim Miller 606785f6e7 Moved tag FanFictionDownLoader-4.4.56 to changeset 0252e03d11c4 (from changeset 14a9a3ca53c1) 2013-04-12 10:32:37 -05:00
Jim Miller 842ab4feeb Fix index.html. 2013-04-12 10:32:18 -05:00
Jim Miller 029af794cf Added tag FanFictionDownLoader-4.4.56 for changeset 14a9a3ca53c1 2013-04-12 10:25:11 -05:00
Jim Miller 87033bd4a3 Added tag calibre-plugin-1.7.23 for changeset 14a9a3ca53c1 2013-04-12 10:24:59 -05:00
Jim Miller c8577893c4 Bump versions. 2013-04-12 10:24:48 -05:00
Jim Miller a822064da0 Remove dead site jlaunlimited.com 2013-04-11 19:51:29 -05:00
Jim Miller af7c717c20 Add new sites to index.html 2013-04-11 17:11:54 -05:00
Jim Miller 56c75350a6 anthology_title_pattern feature 2013-04-11 17:04:45 -05:00
Jim Miller 34b5076753 Add 'url' to chapter custom formats and class="skip_on_ffdl_update" for updates. 2013-04-10 13:30:05 -05:00
Jim Miller f0847809ad Improved error handling for mobi issues. 2013-04-09 20:17:37 -05:00
Jim Miller 976ddf827e Fix for no chapter name for one chapter stories on TtH. 2013-04-09 20:17:16 -05:00
Jim Miller da188234ac Sync up plugin-defaults.ini 2013-04-09 17:38:18 -05:00
Jim Miller 89a676d0b6 Add site: asr3.slashzone.org 2013-04-09 17:36:54 -05:00
Jim Miller 1d48b7c72c Add site: netraptor.org 2013-04-09 17:12:23 -05:00
Jim Miller ce82d96163 Fix author URLs for sites with leading 'dir' in URL. 2013-04-09 16:58:36 -05:00
Jim Miller 049486a059 Add site: tokra.fandomnet.com. 2013-04-09 16:30:24 -05:00
Jim Miller d098bdbdc8 Improve error reporting on CLI. 2013-04-09 16:29:56 -05:00
Jim Miller 2dd85dd044 Added tag FanFictionDownLoader-4.4.55 for changeset 69dd14744c87 2013-04-08 12:28:10 -05:00
Jim Miller fcc5c7ffcc Added tag calibre-plugin-1.7.22 for changeset 69dd14744c87 2013-04-08 12:27:58 -05:00
Jim Miller 9c25271a59 Remove dead sites prisonbreakfic.net, svufiction.com; bump versions. 2013-04-08 12:27:13 -05:00
Jim Miller 6dd5522b7a Include notes with chapter text for thewriterscoffeeshop.com. 2013-04-07 12:13:16 -05:00
Jim Miller aaccb45df5 Fix AO3 series url. 2013-04-07 12:12:51 -05:00
Jim Miller 7b052d353a Added tag FanFictionDownLoader-4.4.54 for changeset fa9a7e44cf2f 2013-04-05 12:12:22 -05:00
Jim Miller d969fbd251 Added tag calibre-plugin-1.7.21 for changeset fa9a7e44cf2f 2013-04-05 12:11:59 -05:00
Jim Miller bcc4ee5efd Bump versions. 2013-04-05 12:11:49 -05:00
Jim Miller 6b2a03dc8f Use ini sections both w/ and w/o www. for all sites. Without takes precedence. 2013-04-05 12:08:55 -05:00
Jim Miller a5a3d284b5 Fix persisting AddDialog size. PI. 2013-04-04 21:30:57 -05:00
Jim Miller b3bf2fc50a Update dark-solace.org/elysian adapter to properly do is_adult. 2013-04-04 17:04:12 -05:00
Jim Miller 22f4cc76fc Use ini sections both w/ and w/o www. for all sites. Canonical takes precedence. 2013-04-04 17:03:44 -05:00
Jim Miller c02ad3f6d5 Update dark-solace.org/elysian adapter for site changes. 2013-04-03 12:15:47 -05:00
Jim Miller e65e209ab3 Fix for drag & drop of text emails. 2013-04-03 12:12:34 -05:00
Jim Miller d940936713 Added tag FanFictionDownLoader-4.4.53 for changeset 5b6e38359ca4 2013-04-01 10:06:06 -05:00
Jim Miller 423e1d2840 Added tag calibre-plugin-1.7.20 for changeset 5b6e38359ca4 2013-04-01 10:05:47 -05:00
136 changed files with 15978 additions and 3386 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# ffd-retief-hrd fanfictiondownloader
application: fanfictiondownloader
version: 4-4-53
version: 4-4-95
runtime: python27
api_version: 1
threadsafe: true
+20 -5
View File
@@ -1,13 +1,28 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2011, Jim Miller'
__copyright__ = '2013, Jim Miller'
__docformat__ = 'restructuredtext en'
import sys
if sys.version_info >= (2, 7):
import logging
logger = logging.getLogger(__name__)
loghandler=logging.StreamHandler()
loghandler.setFormatter(logging.Formatter("FFDL:%(levelname)s:%(filename)s(%(lineno)d):%(message)s"))
logger.addHandler(loghandler)
loghandler.setLevel(logging.DEBUG)
logger.setLevel(logging.DEBUG)
# pulls in translation files for _() strings
try:
load_translations()
except NameError:
pass # load_translations() added in calibre 1.9
# The class that all Interface Action plugin wrappers must inherit from
from calibre.customize import InterfaceActionBase
@@ -24,11 +39,11 @@ class FanFictionDownLoaderBase(InterfaceActionBase):
calibre utilities to run without needing to load the GUI libraries.
'''
name = 'FanFictionDownLoader'
description = 'UI plugin to download FanFiction stories from various sites.'
description = _('UI plugin to download FanFiction stories from various sites.')
supported_platforms = ['windows', 'osx', 'linux']
author = 'Jim Miller'
version = (1, 7, 20)
minimum_calibre_version = (0, 8, 57)
version = (1, 8, 14)
minimum_calibre_version = (1, 13, 0)
#: This field defines the GUI plugin class that contains all the code
#: that actually does something. Its format is module_path:class_name
+2 -2
View File
@@ -239,8 +239,8 @@ class SizePersistedDialog(QDialog):
self.restoreGeometry(self.geom)
def dialog_closing(self, result):
geom = bytearray(self.saveGeometry())
gprefs[self.unique_pref_name] = geom
self.geom = bytearray(self.saveGeometry())
gprefs[self.unique_pref_name] = self.geom
class ReadOnlyTableWidgetItem(QTableWidgetItem):
+307 -204
View File
@@ -7,24 +7,55 @@ __license__ = 'GPL v3'
__copyright__ = '2012, Jim Miller'
__docformat__ = 'restructuredtext en'
import logging
logger = logging.getLogger(__name__)
import traceback, copy, threading
from collections import OrderedDict
from PyQt4.Qt import (QDialog, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QLineEdit, QFont, QWidget, QTextEdit, QComboBox,
QCheckBox, QPushButton, QTabWidget, QVariant, QScrollArea,
QDialogButtonBox )
QDialogButtonBox, QGroupBox )
from calibre.utils.config import JSONConfig
from calibre.gui2.ui import get_gui
from calibre.gui2 import dynamic, info_dialog
from calibre.constants import numeric_version as calibre_version
# pulls in translation files for _() strings
try:
load_translations()
except NameError:
pass # load_translations() added in calibre 1.9
# There are a number of things used several times that shouldn't be
# translated. This is just a way to make that easier by keeping them
# out of the _() strings.
# I'm tempted to override _() to include them...
no_trans = { 'pini':'personal.ini',
'imgset':'\n\n[epub]\ninclude_images:true\nkeep_summary_html:true\nmake_firstimage_cover:true\n\n',
'gcset':'generate_cover_settings',
'ccset':'custom_columns_settings',
'gc':'Generate Cover',
'rl':'Reading List',
'cp':'Count Pages',
'cmplt':'Completed',
'inprog':'In-Progress',
'lul':'Last Updated',
'lus':'lastupdate',
'is':'include_subject',
'isa':'is_adult',
'u':'username',
'p':'password',
}
from calibre_plugins.fanfictiondownloader_plugin.prefs import prefs, PREFS_NAMESPACE
from calibre_plugins.fanfictiondownloader_plugin.dialogs \
import (UPDATE, UPDATEALWAYS, OVERWRITE, collision_order, RejectListDialog,
import (UPDATE, UPDATEALWAYS, collision_order, save_collisions, RejectListDialog,
EditTextDialog, RejectUrlEntry)
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.adapters \
import (getConfigSections, getNormalStoryURL)
import getConfigSections
from calibre_plugins.fanfictiondownloader_plugin.common_utils \
import ( KeyboardConfigDialog, PrefsViewerDialog )
@@ -121,15 +152,21 @@ class ConfigWidget(QWidget):
self.l = QVBoxLayout()
self.setLayout(self.l)
label = QLabel('<a href="http://code.google.com/p/fanficdownloader/wiki/FanFictionDownloaderSupportedsites">List of Supported Sites</a> -- <a href="http://code.google.com/p/fanficdownloader/wiki/FanFictionDownloaderFAQs">FAQs</a>')
label = QLabel('<a href="http://code.google.com/p/fanficdownloader/wiki/FanFictionDownloaderSupportedsites">'+_('List of Supported Sites')+'</a> -- <a href="http://code.google.com/p/fanficdownloader/wiki/FanFictionDownloaderFAQs">'+_('FAQs')+'</a>')
label.setOpenExternalLinks(True)
self.l.addWidget(label)
tab_widget = QTabWidget(self)
self.l.addWidget(tab_widget)
self.scroll_area = QScrollArea(self)
self.scroll_area.setFrameShape(QScrollArea.NoFrame)
self.scroll_area.setWidgetResizable(True)
self.l.addWidget(self.scroll_area)
tab_widget = QTabWidget(self)
self.scroll_area.setWidget(tab_widget)
self.basic_tab = BasicTab(self, plugin_action)
tab_widget.addTab(self.basic_tab, 'Basic')
tab_widget.addTab(self.basic_tab, _('Basic'))
self.personalini_tab = PersonalIniTab(self, plugin_action)
tab_widget.addTab(self.personalini_tab, 'personal.ini')
@@ -150,25 +187,29 @@ class ConfigWidget(QWidget):
self.countpages_tab.setEnabled(False)
self.std_columns_tab = StandardColumnsTab(self, plugin_action)
tab_widget.addTab(self.std_columns_tab, 'Standard Columns')
tab_widget.addTab(self.std_columns_tab, _('Standard Columns'))
self.cust_columns_tab = CustomColumnsTab(self, plugin_action)
tab_widget.addTab(self.cust_columns_tab, 'Custom Columns')
tab_widget.addTab(self.cust_columns_tab, _('Custom Columns'))
self.other_tab = OtherTab(self, plugin_action)
tab_widget.addTab(self.other_tab, 'Other')
tab_widget.addTab(self.other_tab, _('Other'))
def save_settings(self):
# basic
prefs['fileform'] = unicode(self.basic_tab.fileform.currentText())
prefs['collision'] = unicode(self.basic_tab.collision.currentText())
prefs['collision'] = save_collisions[unicode(self.basic_tab.collision.currentText())]
prefs['updatemeta'] = self.basic_tab.updatemeta.isChecked()
prefs['updatecover'] = self.basic_tab.updatecover.isChecked()
prefs['updateepubcover'] = self.basic_tab.updateepubcover.isChecked()
prefs['keeptags'] = self.basic_tab.keeptags.isChecked()
prefs['suppressauthorsort'] = self.basic_tab.suppressauthorsort.isChecked()
prefs['suppresstitlesort'] = self.basic_tab.suppresstitlesort.isChecked()
prefs['mark'] = self.basic_tab.mark.isChecked()
prefs['showmarked'] = self.basic_tab.showmarked.isChecked()
prefs['autoconvert'] = self.basic_tab.autoconvert.isChecked()
prefs['urlsfromclip'] = self.basic_tab.urlsfromclip.isChecked()
prefs['updatedefault'] = self.basic_tab.updatedefault.isChecked()
prefs['deleteotherforms'] = self.basic_tab.deleteotherforms.isChecked()
@@ -176,7 +217,9 @@ class ConfigWidget(QWidget):
prefs['includeimages'] = self.basic_tab.includeimages.isChecked()
prefs['lookforurlinhtml'] = self.basic_tab.lookforurlinhtml.isChecked()
prefs['checkforseriesurlid'] = self.basic_tab.checkforseriesurlid.isChecked()
prefs['checkforurlchange'] = self.basic_tab.checkforurlchange.isChecked()
prefs['injectseries'] = self.basic_tab.injectseries.isChecked()
prefs['smarten_punctuation'] = self.basic_tab.smarten_punctuation.isChecked()
if self.readinglist_tab:
# lists
@@ -206,6 +249,7 @@ class ConfigWidget(QWidget):
#print("gc_site_settings[%s]:%s"%(site,gc_site_settings[site]))
prefs['gc_site_settings'] = gc_site_settings
prefs['allow_gc_from_ini'] = self.generatecover_tab.allow_gc_from_ini.isChecked()
prefs['gc_polish_cover'] = self.generatecover_tab.gc_polish_cover.isChecked()
# Count Pages tab
countpagesstats = []
@@ -266,17 +310,20 @@ class BasicTab(QWidget):
self.plugin_action = plugin_action
QWidget.__init__(self)
self.l = QVBoxLayout()
self.setLayout(self.l)
topl = QVBoxLayout()
self.setLayout(topl)
label = QLabel('These settings control the basic features of the plugin--downloading FanFiction.')
label = QLabel(_('These settings control the basic features of the plugin--downloading FanFiction.'))
label.setWordWrap(True)
self.l.addWidget(label)
self.l.addSpacing(5)
topl.addWidget(label)
tooltip = "On each download, FFDL offers an option to select the output format. <br />This sets what that option will default to."
defs_gb = groupbox = QGroupBox(_("Defaults Options on Download"))
self.l = QVBoxLayout()
groupbox.setLayout(self.l)
tooltip = _("On each download, FFDL offers an option to select the output format. <br />This sets what that option will default to.")
horz = QHBoxLayout()
label = QLabel('Default Output &Format:')
label = QLabel(_('Default Output &Format:'))
label.setToolTip(tooltip)
horz.addWidget(label)
self.fileform = QComboBox(self)
@@ -291,15 +338,15 @@ class BasicTab(QWidget):
horz.addWidget(self.fileform)
self.l.addLayout(horz)
tooltip = "On each download, FFDL offers an option of what happens if that story already exists. <br />This sets what that option will default to."
tooltip = _("On each download, FFDL offers an option of what happens if that story already exists. <br />This sets what that option will default to.")
horz = QHBoxLayout()
label = QLabel('Default If Story Already Exists?')
label = QLabel(_('Default If Story Already Exists?'))
label.setToolTip(tooltip)
horz.addWidget(label)
self.collision = QComboBox(self)
# add collision options
self.set_collisions()
i = self.collision.findText(prefs['collision'])
i = self.collision.findText(save_collisions[prefs['collision']])
if i > -1:
self.collision.setCurrentIndex(i)
self.collision.setToolTip(tooltip)
@@ -307,103 +354,149 @@ class BasicTab(QWidget):
horz.addWidget(self.collision)
self.l.addLayout(horz)
self.updatemeta = QCheckBox('Default Update Calibre &Metadata?',self)
self.updatemeta.setToolTip("On each download, FFDL offers an option to update Calibre's metadata (title, author, URL, tags, custom columns, etc) from the web site. <br />This sets whether that will default to on or off. <br />Columns set to 'New Only' in the column tabs will only be set for new books.")
self.updatemeta = QCheckBox(_('Default Update Calibre &Metadata?'),self)
self.updatemeta.setToolTip(_("On each download, FFDL offers an option to update Calibre's metadata (title, author, URL, tags, custom columns, etc) from the web site. <br />This sets whether that will default to on or off. <br />Columns set to 'New Only' in the column tabs will only be set for new books."))
self.updatemeta.setChecked(prefs['updatemeta'])
self.l.addWidget(self.updatemeta)
self.updateepubcover = QCheckBox('Default Update EPUB Cover when Updating EPUB?',self)
self.updateepubcover.setToolTip("On each download, FFDL offers an option to update the book cover image <i>inside</i> the EPUB from the web site when the EPUB is updated.<br />This sets whether that will default to on or off.")
self.updateepubcover = QCheckBox(_('Default Update EPUB Cover when Updating EPUB?'),self)
self.updateepubcover.setToolTip(_("On each download, FFDL offers an option to update the book cover image <i>inside</i> the EPUB from the web site when the EPUB is updated.<br />This sets whether that will default to on or off."))
self.updateepubcover.setChecked(prefs['updateepubcover'])
self.l.addWidget(self.updateepubcover)
self.l.addSpacing(10)
self.smarten_punctuation = QCheckBox(_('Smarten Punctuation (EPUB only)'),self)
self.smarten_punctuation.setToolTip(_("Run Smarten Punctuation from Calibre's Polish Book feature on each EPUB download and update."))
self.smarten_punctuation.setChecked(prefs['smarten_punctuation'])
if calibre_version >= (0, 9, 39):
self.l.addWidget(self.smarten_punctuation)
self.deleteotherforms = QCheckBox('Delete other existing formats?',self)
self.deleteotherforms.setToolTip('Check this to automatically delete all other ebook formats when updating an existing book.\nHandy if you have both a Nook(epub) and Kindle(mobi), for example.')
cali_gb = groupbox = QGroupBox(_("Updating Calibre Options"))
self.l = QVBoxLayout()
groupbox.setLayout(self.l)
self.deleteotherforms = QCheckBox(_('Delete other existing formats?'),self)
self.deleteotherforms.setToolTip(_('Check this to automatically delete all other ebook formats when updating an existing book.\nHandy if you have both a Nook(epub) and Kindle(mobi), for example.'))
self.deleteotherforms.setChecked(prefs['deleteotherforms'])
self.l.addWidget(self.deleteotherforms)
self.updatecover = QCheckBox('Update Calibre Cover when Updating Metadata?',self)
self.updatecover.setToolTip("Update calibre book cover image from EPUB when metadata is updated. (EPUB only.)\nDoesn't go looking for new images on 'Update Calibre Metadata Only'.")
self.updatecover = QCheckBox(_('Update Calibre Cover when Updating Metadata?'),self)
self.updatecover.setToolTip(_("Update calibre book cover image from EPUB when metadata is updated. (EPUB only.)\nDoesn't go looking for new images on 'Update Calibre Metadata Only'."))
self.updatecover.setChecked(prefs['updatecover'])
self.l.addWidget(self.updatecover)
self.keeptags = QCheckBox('Keep Existing Tags when Updating Metadata?',self)
self.keeptags.setToolTip("Existing tags will be kept and any new tags added.\nCompleted and In-Progress tags will be still be updated, if known.\nLast Updated tags will be updated if lastupdate in include_subject_tags.\n(If Tags is set to 'New Only' in the Standard Columns tab, this has no effect.)")
self.keeptags = QCheckBox(_('Keep Existing Tags when Updating Metadata?'),self)
self.keeptags.setToolTip(_("Existing tags will be kept and any new tags added.\n%(cmplt)s and %(inprog)s tags will be still be updated, if known.\n%(lul)s tags will be updated if %(lus)s in %(is)s.\n(If Tags is set to 'New Only' in the Standard Columns tab, this has no effect.)")%no_trans)
self.keeptags.setChecked(prefs['keeptags'])
self.l.addWidget(self.keeptags)
self.l.addSpacing(10)
self.suppressauthorsort = QCheckBox(_('Force Author into Author Sort?'),self)
self.suppressauthorsort.setToolTip(_("If checked, the author(s) as given will be used for the Author Sort, too.\nIf not checked, calibre will apply it's built in algorithm which makes 'Bob Smith' sort as 'Smith, Bob', etc."))
self.suppressauthorsort.setChecked(prefs['suppressauthorsort'])
self.l.addWidget(self.suppressauthorsort)
self.showmarked = QCheckBox("Show added/updated books when finished?",self)
self.showmarked.setToolTip("Show added/updated books only when finished.\nYou can also manually search for 'marked:ffdl_success'.\n'marked:ffdl_failed' is also available, or search 'marked:ffdl' for both.")
self.showmarked.setChecked(prefs['showmarked'])
self.l.addWidget(self.showmarked)
self.suppresstitlesort = QCheckBox(_('Force Title into Title Sort?'),self)
self.suppresstitlesort.setToolTip(_("If checked, the title as given will be used for the Title Sort, too.\nIf not checked, calibre will apply it's built in algorithm which makes 'The Title' sort as 'Title, The', etc."))
self.suppresstitlesort.setChecked(prefs['suppresstitlesort'])
self.l.addWidget(self.suppresstitlesort)
self.l.addSpacing(10)
self.urlsfromclip = QCheckBox('Take URLs from Clipboard?',self)
self.urlsfromclip.setToolTip('Prefill URLs from valid URLs in Clipboard when Adding New.')
self.urlsfromclip.setChecked(prefs['urlsfromclip'])
self.l.addWidget(self.urlsfromclip)
self.updatedefault = QCheckBox('Default to Update when books selected?',self)
self.updatedefault.setToolTip('The top FanFictionDownLoader plugin button will start Update if\n'+
'books are selected. If unchecked, it will always bring up \'Add New\'.')
self.updatedefault.setChecked(prefs['updatedefault'])
self.l.addWidget(self.updatedefault)
self.adddialogstaysontop = QCheckBox("Keep 'Add New from URL(s)' dialog on top?",self)
self.adddialogstaysontop.setToolTip("Instructs the OS and Window Manager to keep the 'Add New from URL(s)'\ndialog on top of all other windows. Useful for dragging URLs onto it.")
self.adddialogstaysontop.setChecked(prefs['adddialogstaysontop'])
self.l.addWidget(self.adddialogstaysontop)
self.l.addSpacing(10)
# this is a cheat to make it easier for users to realize there's a new include_images features.
self.includeimages = QCheckBox("Include images in EPUBs?",self)
self.includeimages.setToolTip("Download and include images in EPUB stories. This is equivalent to adding:\n\n[epub]\ninclude_images:true\nkeep_summary_html:true\nmake_firstimage_cover:true\n\n ...to the top of personal.ini. Your settings in personal.ini will override this.")
self.includeimages.setChecked(prefs['includeimages'])
self.l.addWidget(self.includeimages)
self.lookforurlinhtml = QCheckBox("Search EPUB text for Story URL?",self)
self.lookforurlinhtml.setToolTip("Look for first valid story URL inside EPUB text if not found in metadata.\nSomewhat risky, could find wrong URL depending on EPUB content.\nAlso finds and corrects bad ffnet URLs from ficsaver.com files.")
self.lookforurlinhtml.setChecked(prefs['lookforurlinhtml'])
self.l.addWidget(self.lookforurlinhtml)
self.checkforseriesurlid = QCheckBox("Check for existing Series Anthology books?",self)
self.checkforseriesurlid.setToolTip("Check for existings Series Anthology books using each new story's series URL before downloading.\nOffer to skip downloading if a Series Anthology is found.")
self.checkforseriesurlid = QCheckBox(_("Check for existing Series Anthology books?"),self)
self.checkforseriesurlid.setToolTip(_("Check for existings Series Anthology books using each new story's series URL before downloading.\nOffer to skip downloading if a Series Anthology is found."))
self.checkforseriesurlid.setChecked(prefs['checkforseriesurlid'])
self.l.addWidget(self.checkforseriesurlid)
self.injectseries = QCheckBox("Inject calibre Series when none found?",self)
self.injectseries.setToolTip("If no series is found, inject the calibre series (if there is one) so it appears on the FFDL title page(not cover).")
self.checkforurlchange = QCheckBox(_("Check for changed Story URL?"),self)
self.checkforurlchange.setToolTip(_("Warn you if an update will change the URL of an existing book.\nfanfiction.net URLs will change from http to https silently."))
self.checkforurlchange.setChecked(prefs['checkforurlchange'])
self.l.addWidget(self.checkforurlchange)
self.lookforurlinhtml = QCheckBox(_("Search EPUB text for Story URL?"),self)
self.lookforurlinhtml.setToolTip(_("Look for first valid story URL inside EPUB text if not found in metadata.\nSomewhat risky, could find wrong URL depending on EPUB content.\nAlso finds and corrects bad ffnet URLs from ficsaver.com files."))
self.lookforurlinhtml.setChecked(prefs['lookforurlinhtml'])
self.l.addWidget(self.lookforurlinhtml)
self.mark = QCheckBox(_("Mark added/updated books when finished?"),self)
self.mark.setToolTip(_("Mark added/updated books when finished. Use with option below.\nYou can also manually search for 'marked:ffdl_success'.\n'marked:ffdl_failed' is also available, or search 'marked:ffdl' for both."))
self.mark.setChecked(prefs['mark'])
self.l.addWidget(self.mark)
self.showmarked = QCheckBox(_("Show Marked books when finished?"),self)
self.showmarked.setToolTip(_("Show Marked added/updated books only when finished.\nYou can also manually search for 'marked:ffdl_success'.\n'marked:ffdl_failed' is also available, or search 'marked:ffdl' for both."))
self.showmarked.setChecked(prefs['showmarked'])
self.l.addWidget(self.showmarked)
self.autoconvert = QCheckBox(_("Automatically Convert new/update books?"),self)
self.autoconvert.setToolTip(_("Automatically call calibre's Convert for new/update books.\nConverts to the current output format as chosen in calibre's\nPreferences->Behavior settings."))
self.autoconvert.setChecked(prefs['autoconvert'])
self.l.addWidget(self.autoconvert)
gui_gb = groupbox = QGroupBox(_("GUI Options"))
self.l = QVBoxLayout()
groupbox.setLayout(self.l)
self.urlsfromclip = QCheckBox(_('Take URLs from Clipboard?'),self)
self.urlsfromclip.setToolTip(_('Prefill URLs from valid URLs in Clipboard when Adding New.'))
self.urlsfromclip.setChecked(prefs['urlsfromclip'])
self.l.addWidget(self.urlsfromclip)
self.updatedefault = QCheckBox(_('Default to Update when books selected?'),self)
self.updatedefault.setToolTip(_('The top FanFictionDownLoader plugin button will start Update if\nbooks are selected. If unchecked, it will always bring up \'Add New\'.'))
self.updatedefault.setChecked(prefs['updatedefault'])
self.l.addWidget(self.updatedefault)
self.adddialogstaysontop = QCheckBox(_("Keep 'Add New from URL(s)' dialog on top?"),self)
self.adddialogstaysontop.setToolTip(_("Instructs the OS and Window Manager to keep the 'Add New from URL(s)'\ndialog on top of all other windows. Useful for dragging URLs onto it."))
self.adddialogstaysontop.setChecked(prefs['adddialogstaysontop'])
self.l.addWidget(self.adddialogstaysontop)
misc_gb = groupbox = QGroupBox(_("Misc Options"))
self.l = QVBoxLayout()
groupbox.setLayout(self.l)
# this is a cheat to make it easier for users to realize there's a new include_images features.
self.includeimages = QCheckBox(_("Include images in EPUBs?"),self)
self.includeimages.setToolTip(_("Download and include images in EPUB stories. This is equivalent to adding:%(imgset)s ...to the top of %(pini)s. Your settings in %(pini)s will override this.")%no_trans)
self.includeimages.setChecked(prefs['includeimages'])
self.l.addWidget(self.includeimages)
self.injectseries = QCheckBox(_("Inject calibre Series when none found?"),self)
self.injectseries.setToolTip(_("If no series is found, inject the calibre series (if there is one) so it appears on the FFDL title page(not cover)."))
self.injectseries.setChecked(prefs['injectseries'])
self.l.addWidget(self.injectseries)
self.l.addSpacing(10)
rej_gb = groupbox = QGroupBox(_("Reject List"))
self.l = QVBoxLayout()
groupbox.setLayout(self.l)
self.rejectlist = QPushButton(_('Edit Reject URL List'), self)
self.rejectlist.setToolTip(_("Edit list of URLs FFDL will automatically Reject."))
self.rejectlist.clicked.connect(self.show_rejectlist)
self.l.addWidget(self.rejectlist)
self.reject_urls = QPushButton(_('Add Reject URLs'), self)
self.reject_urls.setToolTip(_("Add additional URLs to Reject as text."))
self.reject_urls.clicked.connect(self.add_reject_urls)
self.l.addWidget(self.reject_urls)
self.reject_reasons = QPushButton(_('Edit Reject Reasons List'), self)
self.reject_reasons.setToolTip(_("Customize the Reasons presented when Rejecting URLs"))
self.reject_reasons.clicked.connect(self.show_reject_reasons)
self.l.addWidget(self.reject_reasons)
topl.addWidget(defs_gb)
horz = QHBoxLayout()
horz.addWidget(cali_gb)
vert = QVBoxLayout()
vert.addWidget(gui_gb)
vert.addWidget(misc_gb)
vert.addWidget(rej_gb)
self.rejectlist = QPushButton('Edit Reject URL List', self)
self.rejectlist.setToolTip("Edit list of URLs FFDL will automatically Reject.")
self.rejectlist.clicked.connect(self.show_rejectlist)
horz.addWidget(self.rejectlist)
horz.addLayout(vert)
self.reject_urls = QPushButton('Add Reject URLs', self)
self.reject_urls.setToolTip("Add additional URLs to Reject as text.")
self.reject_urls.clicked.connect(self.add_reject_urls)
horz.addWidget(self.reject_urls)
self.reject_reasons = QPushButton('Edit Reject Reasons List', self)
self.reject_reasons.setToolTip("Customize the Reasons presented when Rejecting URLs")
self.reject_reasons.clicked.connect(self.show_reject_reasons)
horz.addWidget(self.reject_reasons)
self.l.addLayout(horz)
self.l.insertStretch(-1)
topl.addLayout(horz)
topl.insertStretch(-1)
def set_collisions(self):
prev=self.collision.currentText()
@@ -423,7 +516,7 @@ class BasicTab(QWidget):
d = RejectListDialog(self,
rejecturllist.get_list(),
rejectreasons=rejecturllist.get_reject_reasons(),
header="Edit Reject URLs List",
header=_("Edit Reject URLs List"),
show_delete=False,
show_all_reasons=False)
d.exec_()
@@ -437,22 +530,22 @@ class BasicTab(QWidget):
d = EditTextDialog(self,
prefs['rejectreasons'],
icon=self.windowIcon(),
title="Reject Reasons",
label="Customize Reject List Reasons",
tooltip="Customize the Reasons presented when Rejecting URLs")
title=_("Reject Reasons"),
label=_("Customize Reject List Reasons"),
tooltip=_("Customize the Reasons presented when Rejecting URLs"))
d.exec_()
if d.result() == d.Accepted:
prefs['rejectreasons'] = d.get_plain_text()
def add_reject_urls(self):
d = EditTextDialog(self,
"http://example.com/story.php?sid=5,Reason why I rejected it\nhttp://example.com/story.php?sid=6,Title by Author - Reason why I rejected it",
"http://example.com/story.php?sid=5,"+_("Reason why I rejected it")+"\nhttp://example.com/story.php?sid=6,"+_("Title by Author")+" - "+_("Reason why I rejected it"),
icon=self.windowIcon(),
title="Add Reject URLs",
label="Add Reject URLs. Use: <b>http://...,note</b> or <b>http://...,title by author - note</b><br>Invalid story URLs will be ignored.",
tooltip="One URL per line:\n<b>http://...,note</b>\n<b>http://...,title by author - note</b>",
title=_("Add Reject URLs"),
label=_("Add Reject URLs. Use: <b>http://...,note</b> or <b>http://...,title by author - note</b><br>Invalid story URLs will be ignored."),
tooltip=_("One URL per line:\n<b>http://...,note</b>\n<b>http://...,title by author - note</b>"),
rejectreasons=rejecturllist.get_reject_reasons(),
reasonslabel='Add this reason to all URLs added:')
reasonslabel=_('Add this reason to all URLs added:'))
d.exec_()
if d.result() == d.Accepted:
rejecturllist.add_text(d.get_plain_text(),d.get_reason_text())
@@ -467,7 +560,7 @@ class PersonalIniTab(QWidget):
self.l = QVBoxLayout()
self.setLayout(self.l)
label = QLabel('These settings provide more detailed control over what metadata will be displayed inside the ebook as well as let you set is_adult and user/password for different sites.')
label = QLabel(_('These settings provide more detailed control over what metadata will be displayed inside the ebook as well as let you set %(isa)s and %(u)s/%(p)s for different sites.')%no_trans)
label.setWordWrap(True)
self.l.addWidget(label)
self.l.addSpacing(5)
@@ -478,15 +571,15 @@ class PersonalIniTab(QWidget):
self.ini = QTextEdit(self)
try:
self.ini.setFont(QFont("Courier",
self.plugin_action.gui.font().pointSize()+1));
self.plugin_action.gui.font().pointSize()+1))
except Exception as e:
print("Couldn't get font: %s"%e)
logger.error("Couldn't get font: %s"%e)
self.ini.setLineWrapMode(QTextEdit.NoWrap)
self.ini.setText(prefs['personal.ini'])
self.l.addWidget(self.ini)
self.defaults = QPushButton('View Defaults (plugin-defaults.ini)', self)
self.defaults.setToolTip("View all of the plugin's configurable settings\nand their default settings.")
self.defaults = QPushButton(_('View Defaults')+' (plugin-defaults.ini)', self)
self.defaults.setToolTip(_("View all of the plugin's configurable settings\nand their default settings."))
self.defaults.clicked.connect(self.show_defaults)
self.l.addWidget(self.defaults)
@@ -504,25 +597,25 @@ class ShowDefaultsIniDialog(QDialog):
self.resize(600, 500)
self.l = QVBoxLayout()
self.setLayout(self.l)
self.label = QLabel("Plugin Defaults (plugin-defaults.ini) (Read-Only)")
self.label.setToolTip("These are all of the plugin's configurable options\nand their default settings.")
self.label = QLabel(_("Plugin Defaults (%s) (Read-Only)")%'plugin-defaults.ini')
self.label.setToolTip(_("These are all of the plugin's configurable options\nand their default settings."))
self.setWindowTitle(_('Plugin Defaults'))
self.setWindowIcon(icon)
self.l.addWidget(self.label)
self.ini = QTextEdit(self)
self.ini.setToolTip("These are all of the plugin's configurable options\nand their default settings.")
self.ini.setToolTip(_("These are all of the plugin's configurable options\nand their default settings."))
try:
self.ini.setFont(QFont("Courier",
get_gui().font().pointSize()+1));
get_gui().font().pointSize()+1))
except Exception as e:
print("Couldn't get font: %s"%e)
logger.error("Couldn't get font: %s"%e)
self.ini.setLineWrapMode(QTextEdit.NoWrap)
self.ini.setText(text)
self.ini.setReadOnly(True)
self.l.addWidget(self.ini)
self.ok_button = QPushButton('OK', self)
self.ok_button = QPushButton(_('OK'), self)
self.ok_button.clicked.connect(self.hide)
self.l.addWidget(self.ok_button)
@@ -542,45 +635,45 @@ class ReadingListTab(QWidget):
except KeyError:
reading_lists= []
label = QLabel('These settings provide integration with the Reading List Plugin. Reading List can automatically send to devices and change custom columns. You have to create and configure the lists in Reading List to be useful.')
label = QLabel(_('These settings provide integration with the %(rl)s Plugin. %(rl)s can automatically send to devices and change custom columns. You have to create and configure the lists in %(rl)s to be useful.')%no_trans)
label.setWordWrap(True)
self.l.addWidget(label)
self.l.addSpacing(5)
self.addtolists = QCheckBox('Add new/updated stories to "Send to Device" Reading List(s).',self)
self.addtolists.setToolTip('Automatically add new/updated stories to these lists in the Reading List plugin.')
self.addtolists = QCheckBox(_('Add new/updated stories to "Send to Device" Reading List(s).'),self)
self.addtolists.setToolTip(_('Automatically add new/updated stories to these lists in the %(rl)s plugin.')%no_trans)
self.addtolists.setChecked(prefs['addtolists'])
self.l.addWidget(self.addtolists)
horz = QHBoxLayout()
label = QLabel('"Send to Device" Reading Lists')
label.setToolTip("When enabled, new/updated stories will be automatically added to these lists.")
label = QLabel(_('"Send to Device" Reading Lists'))
label.setToolTip(_("When enabled, new/updated stories will be automatically added to these lists."))
horz.addWidget(label)
self.send_lists_box = MultiCompleteLineEdit(self)
self.send_lists_box.setToolTip("When enabled, new/updated stories will be automatically added to these lists.")
self.send_lists_box.setToolTip(_("When enabled, new/updated stories will be automatically added to these lists."))
self.send_lists_box.update_items_cache(reading_lists)
self.send_lists_box.setText(prefs['send_lists'])
horz.addWidget(self.send_lists_box)
self.l.addLayout(horz)
self.addtoreadlists = QCheckBox('Add new/updated stories to "To Read" Reading List(s).',self)
self.addtoreadlists.setToolTip('Automatically add new/updated stories to these lists in the Reading List plugin.\nAlso offers menu option to remove stories from the "To Read" lists.')
self.addtoreadlists = QCheckBox(_('Add new/updated stories to "To Read" Reading List(s).'),self)
self.addtoreadlists.setToolTip(_('Automatically add new/updated stories to these lists in the %(rl)s plugin.\nAlso offers menu option to remove stories from the "To Read" lists.')%no_trans)
self.addtoreadlists.setChecked(prefs['addtoreadlists'])
self.l.addWidget(self.addtoreadlists)
horz = QHBoxLayout()
label = QLabel('"To Read" Reading Lists')
label.setToolTip("When enabled, new/updated stories will be automatically added to these lists.")
label = QLabel(_('"To Read" Reading Lists'))
label.setToolTip(_("When enabled, new/updated stories will be automatically added to these lists."))
horz.addWidget(label)
self.read_lists_box = MultiCompleteLineEdit(self)
self.read_lists_box.setToolTip("When enabled, new/updated stories will be automatically added to these lists.")
self.read_lists_box.setToolTip(_("When enabled, new/updated stories will be automatically added to these lists."))
self.read_lists_box.update_items_cache(reading_lists)
self.read_lists_box.setText(prefs['read_lists'])
horz.addWidget(self.read_lists_box)
self.l.addLayout(horz)
self.addtolistsonread = QCheckBox('Add stories back to "Send to Device" Reading List(s) when marked "Read".',self)
self.addtolistsonread.setToolTip('Menu option to remove from "To Read" lists will also add stories back to "Send to Device" Reading List(s)')
self.addtolistsonread = QCheckBox(_('Add stories back to "Send to Device" Reading List(s) when marked "Read".'),self)
self.addtolistsonread.setToolTip(_('Menu option to remove from "To Read" lists will also add stories back to "Send to Device" Reading List(s)'))
self.addtolistsonread.setChecked(prefs['addtolistsonread'])
self.l.addWidget(self.addtolistsonread)
@@ -602,7 +695,7 @@ class GenerateCoverTab(QWidget):
except KeyError:
gc_settings= []
label = QLabel('The Generate Cover plugin can create cover images for books using various metadata and configurations. If you have GC installed, FFDL can run GC on new downloads and metadata updates. Pick a GC setting by site or Default.')
label = QLabel(_('The %(gc)s plugin can create cover images for books using various metadata and configurations. If you have GC installed, FFDL can run GC on new downloads and metadata updates. Pick a GC setting by site or Default.')%no_trans)
label.setWordWrap(True)
self.l.addWidget(label)
self.l.addSpacing(5)
@@ -620,14 +713,15 @@ class GenerateCoverTab(QWidget):
sitelist = getConfigSections()
sitelist.sort()
sitelist.insert(0,u"Default")
sitelist.insert(0,_("Default"))
for site in sitelist:
horz = QHBoxLayout()
label = QLabel(site)
if site == u"Default":
s = "On Metadata update, run Generate Cover with this setting, if not selected for specific site."
if site == _("Default"):
s = _("On Metadata update, run %(gc)s with this setting, if not selected for specific site.")%no_trans
else:
s = "On Metadata update, run Generate Cover with this setting for %s stories."%site
no_trans['site']=site # not ideal, but, meh.
s = _("On Metadata update, run %(gc)s with this setting for %(site)s stories.")%no_trans
label.setToolTip(s)
horz.addWidget(label)
@@ -636,7 +730,12 @@ class GenerateCoverTab(QWidget):
dropdown.addItem('',QVariant('none'))
for setting in gc_settings:
dropdown.addItem(setting,QVariant(setting))
self.gc_dropdowns[site] = dropdown
if site == _("Default"):
self.gc_dropdowns["Default"] = dropdown
if 'Default' in prefs['gc_site_settings']:
dropdown.setCurrentIndex(dropdown.findData(QVariant(prefs['gc_site_settings']['Default'])))
else:
self.gc_dropdowns[site] = dropdown
if site in prefs['gc_site_settings']:
dropdown.setCurrentIndex(dropdown.findData(QVariant(prefs['gc_site_settings'][site])))
@@ -645,16 +744,21 @@ class GenerateCoverTab(QWidget):
self.sl.insertStretch(-1)
self.gcnewonly = QCheckBox("Run Generate Cover Only on New Books",self)
self.gcnewonly.setToolTip("Default is to run GC any time the calibre metadata is updated.")
self.gcnewonly = QCheckBox(_("Run %(gc)s Only on New Books")%no_trans,self)
self.gcnewonly.setToolTip(_("Default is to run GC any time the calibre metadata is updated."))
self.gcnewonly.setChecked(prefs['gcnewonly'])
self.l.addWidget(self.gcnewonly)
self.allow_gc_from_ini = QCheckBox('Allow generate_cover_settings from personal.ini to override',self)
self.allow_gc_from_ini.setToolTip("The personal.ini parameter generate_cover_settings allows you to choose a GC setting based on metadata rather than site, but it's much more complex.<br \>generate_cover_settings is ignored when this is off.")
self.allow_gc_from_ini = QCheckBox(_('Allow %(gcset)s from %(pini)s to override')%no_trans,self)
self.allow_gc_from_ini.setToolTip(_("The %(pini)s parameter %(gcset)s allows you to choose a GC setting based on metadata rather than site, but it's much more complex.<br \>%(gcset)s is ignored when this is off.")%no_trans)
self.allow_gc_from_ini.setChecked(prefs['allow_gc_from_ini'])
self.l.addWidget(self.allow_gc_from_ini)
self.gc_polish_cover = QCheckBox(_("Use calibre's Polish feature to inject/update the cover"),self)
self.gc_polish_cover.setToolTip(_("Calibre's Polish feature will be used to inject or update the generated cover into the ebook, EPUB only."))
self.gc_polish_cover.setChecked(prefs['gc_polish_cover'])
self.l.addWidget(self.gc_polish_cover)
class CountPagesTab(QWidget):
def __init__(self, parent_dialog, plugin_action):
@@ -665,39 +769,41 @@ class CountPagesTab(QWidget):
self.l = QVBoxLayout()
self.setLayout(self.l)
label = QLabel('These settings provide integration with the Count Pages Plugin. Count Pages can automatically update custom columns with page, word and reading level statistics. You have to create and configure the columns in Count Pages first.')
label = QLabel(_('These settings provide integration with the %(cp)s Plugin. %(cp)s can automatically update custom columns with page, word and reading level statistics. You have to create and configure the columns in %(cp)s first.')%no_trans)
label.setWordWrap(True)
self.l.addWidget(label)
self.l.addSpacing(5)
label = QLabel('If any of the settings below are checked, when stories are added or updated, the Count Pages Plugin will be called to update the checked statistics.')
label = QLabel(_('If any of the settings below are checked, when stories are added or updated, the %(cp)s Plugin will be called to update the checked statistics.')%no_trans)
label.setWordWrap(True)
self.l.addWidget(label)
self.l.addSpacing(5)
# the same for all settings. Mostly.
tooltip = _('Which column and algorithm to use are configured in %(cp)s.')%no_trans
# 'PageCount', 'WordCount', 'FleschReading', 'FleschGrade', 'GunningFog'
self.pagecount = QCheckBox('Page Count',self)
self.pagecount.setToolTip('Which column and algorithm to use are configured in Count Pages.')
self.pagecount.setToolTip(tooltip)
self.pagecount.setChecked('PageCount' in prefs['countpagesstats'])
self.l.addWidget(self.pagecount)
self.wordcount = QCheckBox('Word Count',self)
self.wordcount.setToolTip('Which column and algorithm to use are configured in Count Words.\nWill overwrite word count from FFDL metadata if set to update the same custom column.')
self.wordcount.setToolTip(tooltip+"\n"+_('Will overwrite word count from FFDL metadata if set to update the same custom column.'))
self.wordcount.setChecked('WordCount' in prefs['countpagesstats'])
self.l.addWidget(self.wordcount)
self.fleschreading = QCheckBox('Flesch Reading Ease',self)
self.fleschreading.setToolTip('Which column and algorithm to use are configured in Count Pages.')
self.fleschreading.setToolTip(tooltip)
self.fleschreading.setChecked('FleschReading' in prefs['countpagesstats'])
self.l.addWidget(self.fleschreading)
self.fleschgrade = QCheckBox('Flesch-Kincaid Grade Level',self)
self.fleschgrade.setToolTip('Which column and algorithm to use are configured in Count Pages.')
self.fleschgrade.setToolTip(tooltip)
self.fleschgrade.setChecked('FleschGrade' in prefs['countpagesstats'])
self.l.addWidget(self.fleschgrade)
self.gunningfog = QCheckBox('Gunning Fog Index',self)
self.gunningfog.setToolTip('Which column and algorithm to use are configured in Count Pages.')
self.gunningfog.setToolTip(tooltip)
self.gunningfog.setChecked('GunningFog' in prefs['countpagesstats'])
self.l.addWidget(self.gunningfog)
@@ -713,26 +819,23 @@ class OtherTab(QWidget):
self.l = QVBoxLayout()
self.setLayout(self.l)
label = QLabel("These controls aren't plugin settings as such, but convenience buttons for setting Keyboard shortcuts and getting all the FanFictionDownLoader confirmation dialogs back again.")
label = QLabel(_("These controls aren't plugin settings as such, but convenience buttons for setting Keyboard shortcuts and getting all the FanFictionDownLoader confirmation dialogs back again."))
label.setWordWrap(True)
self.l.addWidget(label)
self.l.addSpacing(5)
keyboard_shortcuts_button = QPushButton('Keyboard shortcuts...', self)
keyboard_shortcuts_button.setToolTip(_(
'Edit the keyboard shortcuts associated with this plugin'))
keyboard_shortcuts_button = QPushButton(_('Keyboard shortcuts...'), self)
keyboard_shortcuts_button.setToolTip(_('Edit the keyboard shortcuts associated with this plugin'))
keyboard_shortcuts_button.clicked.connect(parent_dialog.edit_shortcuts)
self.l.addWidget(keyboard_shortcuts_button)
reset_confirmation_button = QPushButton(_('Reset disabled &confirmation dialogs'), self)
reset_confirmation_button.setToolTip(_(
'Reset all show me again dialogs for the FanFictionDownLoader plugin'))
reset_confirmation_button.setToolTip(_('Reset all show me again dialogs for the FanFictionDownLoader plugin'))
reset_confirmation_button.clicked.connect(self.reset_dialogs)
self.l.addWidget(reset_confirmation_button)
view_prefs_button = QPushButton('&View library preferences...', self)
view_prefs_button.setToolTip(_(
'View data stored in the library database for this plugin'))
view_prefs_button = QPushButton(_('&View library preferences...'), self)
view_prefs_button.setToolTip(_('View data stored in the library database for this plugin'))
view_prefs_button.clicked.connect(self.view_prefs)
self.l.addWidget(view_prefs_button)
@@ -792,35 +895,35 @@ permitted_values['text'] = permitted_values['enumeration']
permitted_values['comments'] = permitted_values['enumeration']
titleLabels = {
'category':'Category',
'genre':'Genre',
'language':'Language',
'status':'Status',
'status-C':'Status:Completed',
'status-I':'Status:In-Progress',
'series':'Series',
'characters':'Characters',
'ships':'Relationships',
'datePublished':'Published',
'dateUpdated':'Updated',
'dateCreated':'Created',
'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':'Description',
'author':'Author',
'authorUrl':'Author URL',
'formatname':'File Format',
'formatext':'File Extension',
'siteabbrev':'Site Abbrev',
'version':'FFDL Version'
'category':_('Category'),
'genre':_('Genre'),
'language':_('Language'),
'status':_('Status'),
'status-C':_('Status:%(cmplt)s')%no_trans,
'status-I':_('Status:%(inprog)s')%no_trans,
'series':_('Series'),
'characters':_('Characters'),
'ships':_('Relationships'),
'datePublished':_('Published'),
'dateUpdated':_('Updated'),
'dateCreated':_('Created'),
'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':_('Description'),
'author':_('Author'),
'authorUrl':_('Author URL'),
'formatname':_('File Format'),
'formatext':_('File Extension'),
'siteabbrev':_('Site Abbrev'),
'version':_('FFDL Version')
}
class CustomColumnsTab(QWidget):
@@ -835,7 +938,7 @@ class CustomColumnsTab(QWidget):
self.l = QVBoxLayout()
self.setLayout(self.l)
label = QLabel("If you have custom columns defined, they will be listed below. Choose a metadata value type to fill your columns automatically.")
label = QLabel(_("If you have custom columns defined, they will be listed below. Choose a metadata value type to fill your columns automatically."))
label.setWordWrap(True)
self.l.addWidget(label)
self.l.addSpacing(5)
@@ -860,7 +963,7 @@ class CustomColumnsTab(QWidget):
# print("column['%s'] => %s"%(k,v))
horz = QHBoxLayout()
label = QLabel(column['name'])
label.setToolTip("Update this %s column(%s) with..."%(key,column['datatype']))
label.setToolTip(_("Update this %s column(%s) with...")%(key,column['datatype']))
horz.addWidget(label)
dropdown = QComboBox(self)
dropdown.addItem('',QVariant('none'))
@@ -870,13 +973,13 @@ class CustomColumnsTab(QWidget):
if key in prefs['custom_cols']:
dropdown.setCurrentIndex(dropdown.findData(QVariant(prefs['custom_cols'][key])))
if column['datatype'] == 'enumeration':
dropdown.setToolTip("Metadata values valid for this type of column.\nValues that aren't valid for this enumeration column will be ignored.")
dropdown.setToolTip(_("Metadata values valid for this type of column.")+"\n"+_("Values that aren't valid for this enumeration column will be ignored."))
else:
dropdown.setToolTip("Metadata values valid for this type of column.")
dropdown.setToolTip(_("Metadata values valid for this type of column."))
horz.addWidget(dropdown)
newonlycheck = QCheckBox("New Only",self)
newonlycheck.setToolTip("Write to %s(%s) only for new\nbooks, not updates to existing books."%(column['name'],key))
newonlycheck = QCheckBox(_("New Only"),self)
newonlycheck.setToolTip(_("Write to %s(%s) only for new\nbooks, not updates to existing books.")%(column['name'],key))
self.custcol_newonlycheck[key] = newonlycheck
if key in prefs['custom_cols_newonly']:
newonlycheck.setChecked(prefs['custom_cols_newonly'][key])
@@ -887,19 +990,19 @@ class CustomColumnsTab(QWidget):
self.sl.insertStretch(-1)
self.l.addSpacing(5)
self.allow_custcol_from_ini = QCheckBox('Allow custom_columns_settings from personal.ini to override',self)
self.allow_custcol_from_ini.setToolTip("The personal.ini parameter custom_columns_settings allows you to set custom columns to site specific values that aren't common to all sites.<br \>custom_columns_settings is ignored when this is off.")
self.allow_custcol_from_ini = QCheckBox(_('Allow %(ccset)s from %(pini)s to override')%no_trans,self)
self.allow_custcol_from_ini.setToolTip(_("The %(pini)s parameter %(ccset)s allows you to set custom columns to site specific values that aren't common to all sites.<br />%(ccset)s is ignored when this is off.")%no_trans)
self.allow_custcol_from_ini.setChecked(prefs['allow_custcol_from_ini'])
self.l.addWidget(self.allow_custcol_from_ini)
self.l.addSpacing(5)
label = QLabel("Special column:")
label = QLabel(_("Special column:"))
label.setWordWrap(True)
self.l.addWidget(label)
horz = QHBoxLayout()
label = QLabel("Update/Overwrite Error Column:")
tooltip="When an update or overwrite of an existing story fails, record the reason in this column.\n(Text and Long Text columns only.)"
label = QLabel(_("Update/Overwrite Error Column:"))
tooltip=_("When an update or overwrite of an existing story fails, record the reason in this column.\n(Text and Long Text columns only.)")
label.setToolTip(tooltip)
horz.addWidget(label)
self.errorcol = QComboBox(self)
@@ -924,21 +1027,21 @@ class StandardColumnsTab(QWidget):
columns=OrderedDict()
columns["title"]="Title"
columns["authors"]="Author(s)"
columns["publisher"]="Publisher"
columns["tags"]="Tags"
columns["languages"]="Languages"
columns["pubdate"]="Published Date"
columns["timestamp"]="Date"
columns["comments"]="Comments"
columns["series"]="Series"
columns["identifiers"]="Ids(url id only)"
columns["title"]=_("Title")
columns["authors"]=_("Author(s)")
columns["publisher"]=_("Publisher")
columns["tags"]=_("Tags")
columns["languages"]=_("Languages")
columns["pubdate"]=_("Published Date")
columns["timestamp"]=_("Date")
columns["comments"]=_("Comments")
columns["series"]=_("Series")
columns["identifiers"]=_("Ids(url id only)")
self.l = QVBoxLayout()
self.setLayout(self.l)
label = QLabel("The standard calibre metadata columns are listed below. You may choose whether FFDL will fill each column automatically on updates or only for new books.")
label = QLabel(_("The standard calibre metadata columns are listed below. You may choose whether FFDL will fill each column automatically on updates or only for new books."))
label.setWordWrap(True)
self.l.addWidget(label)
self.l.addSpacing(5)
@@ -951,8 +1054,8 @@ class StandardColumnsTab(QWidget):
#label.setToolTip("Update this %s column(%s) with..."%(key,column['datatype']))
horz.addWidget(label)
newonlycheck = QCheckBox("New Only",self)
newonlycheck.setToolTip("Write to %s only for new\nbooks, not updates to existing books."%column)
newonlycheck = QCheckBox(_("New Only"),self)
newonlycheck.setToolTip(_("Write to %s only for new\nbooks, not updates to existing books.")%column)
self.stdcol_newonlycheck[key] = newonlycheck
if key in prefs['std_cols_newonly']:
newonlycheck.setChecked(prefs['std_cols_newonly'][key])
+182 -86
View File
@@ -7,9 +7,15 @@ __license__ = 'GPL v3'
__copyright__ = '2011, Jim Miller'
__docformat__ = 'restructuredtext en'
import logging
logger = logging.getLogger(__name__)
import traceback, re
from functools import partial
import logging
logger = logging.getLogger(__name__)
import urllib
import email
@@ -17,11 +23,18 @@ from PyQt4 import QtGui
from PyQt4.Qt import (QDialog, QTableWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
QPushButton, QString, QLabel, QCheckBox, QIcon, QLineEdit,
QComboBox, QVariant, QProgressDialog, QTimer, QDialogButtonBox,
QPixmap, Qt, QAbstractItemView, SIGNAL, QTextEdit, pyqtSignal)
QPixmap, Qt, QAbstractItemView, SIGNAL, QTextEdit, pyqtSignal,
QGroupBox, QFrame)
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.complete2 import EditWithComplete
# pulls in translation files for _() strings
try:
load_translations()
except NameError:
pass # load_translations() added in calibre 1.9
from calibre_plugins.fanfictiondownloader_plugin.common_utils \
import (ReadOnlyTableWidgetItem, ReadOnlyTextIconWidgetItem, SizePersistedDialog,
ImageTitleLayout, get_icon)
@@ -29,13 +42,13 @@ from calibre_plugins.fanfictiondownloader_plugin.common_utils \
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.geturls import get_urls_from_html, get_urls_from_text
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.adapters import getNormalStoryURL
SKIP=u'Skip'
ADDNEW=u'Add New Book'
UPDATE=u'Update EPUB if New Chapters'
UPDATEALWAYS=u'Update EPUB Always'
OVERWRITE=u'Overwrite if Newer'
OVERWRITEALWAYS=u'Overwrite Always'
CALIBREONLY=u'Update Calibre Metadata Only'
SKIP=_('Skip')
ADDNEW=_('Add New Book')
UPDATE=_('Update EPUB if New Chapters')
UPDATEALWAYS=_('Update EPUB Always')
OVERWRITE=_('Overwrite if Newer')
OVERWRITEALWAYS=_('Overwrite Always')
CALIBREONLY=_('Update Calibre Metadata Only')
collision_order=[SKIP,
ADDNEW,
UPDATE,
@@ -44,22 +57,51 @@ collision_order=[SKIP,
OVERWRITEALWAYS,
CALIBREONLY,]
# best idea I've had for how to deal with config/pref saving the
# collision name in english.
SAVE_SKIP='Skip'
SAVE_ADDNEW='Add New Book'
SAVE_UPDATE='Update EPUB if New Chapters'
SAVE_UPDATEALWAYS='Update EPUB Always'
SAVE_OVERWRITE='Overwrite if Newer'
SAVE_OVERWRITEALWAYS='Overwrite Always'
SAVE_CALIBREONLY='Update Calibre Metadata Only'
save_collisions={
SKIP:SAVE_SKIP,
ADDNEW:SAVE_ADDNEW,
UPDATE:SAVE_UPDATE,
UPDATEALWAYS:SAVE_UPDATEALWAYS,
OVERWRITE:SAVE_OVERWRITE,
OVERWRITEALWAYS:SAVE_OVERWRITEALWAYS,
CALIBREONLY:SAVE_CALIBREONLY,
SAVE_SKIP:SKIP,
SAVE_ADDNEW:ADDNEW,
SAVE_UPDATE:UPDATE,
SAVE_UPDATEALWAYS:UPDATEALWAYS,
SAVE_OVERWRITE:OVERWRITE,
SAVE_OVERWRITEALWAYS:OVERWRITEALWAYS,
SAVE_CALIBREONLY:CALIBREONLY,
}
anthology_collision_order=[UPDATE,
UPDATEALWAYS,
OVERWRITEALWAYS]
gpstyle='QGroupBox {border:0; padding-top:10px; padding-bottom:0px; margin-bottom:0px;}' # background-color:red;
class RejectUrlEntry:
matchpat=re.compile(r"^(?P<url>[^,]+)(,(?P<fullnote>(((?P<title>.+) by (?P<auth>.+?)( - (?P<note>.+))?)|.*)))?$")
def __init__(self,url_or_line,note=None,title=None,auth=None,
addreasontext=None,fromline=False):
addreasontext=None,fromline=False,book_id=None):
self.url=url_or_line
self.note=note
self.title=title
self.auth=auth
self.valid=False
self.book_id=book_id
if fromline:
mc = re.match(self.matchpat,url_or_line)
@@ -94,6 +136,7 @@ class RejectUrlEntry:
def fullnote(self):
retval = ""
if self.title and self.auth:
# don't translate--ends up being saved and confuses regex above.
retval = retval + "%s by %s"%(self.title,self.auth)
if self.note:
retval = retval + " - "
@@ -160,7 +203,7 @@ class DroppableQTextEdit(QTextEdit):
def canInsertFromMimeData(self, source):
if source.hasUrls():
return True;
return True
else:
return QTextEdit.canInsertFromMimeData(self,source)
@@ -182,7 +225,7 @@ class AddNewDialog(SizePersistedDialog):
self.l = QVBoxLayout()
self.setLayout(self.l)
self.setWindowTitle('FanFictionDownLoader')
self.setWindowTitle(_('FanFictionDownLoader'))
self.setWindowIcon(icon)
self.toplabel=QLabel("Toplabel")
@@ -199,8 +242,26 @@ class AddNewDialog(SizePersistedDialog):
# elements to show again when doing *update* merge
self.mergeupdateshow = []
self.groupbox = QGroupBox(_("Show Download Options"))
self.groupbox.setCheckable(True)
self.groupbox.setChecked(False)
self.groupbox.setFlat(True)
#print("style:%s"%self.groupbox.styleSheet())
self.groupbox.setStyleSheet(gpstyle)
self.gbf = QFrame()
self.gbl = QVBoxLayout()
self.gbl.addWidget(self.gbf)
self.groupbox.setLayout(self.gbl)
self.gbl = QVBoxLayout()
self.gbf.setLayout(self.gbl)
self.l.addWidget(self.groupbox)
self.gbf.setVisible(False)
self.groupbox.toggled.connect(self.gbf.setVisible)
horz = QHBoxLayout()
label = QLabel('Output &Format:')
label = QLabel(_('Output &Format:'))
self.mergehide.append(label)
self.fileform = QComboBox(self)
@@ -208,13 +269,13 @@ class AddNewDialog(SizePersistedDialog):
self.fileform.addItem('mobi')
self.fileform.addItem('html')
self.fileform.addItem('txt')
self.fileform.setToolTip('Choose output format to create. May set default from plugin configuration.')
self.fileform.setToolTip(_('Choose output format to create. May set default from plugin configuration.'))
self.fileform.activated.connect(self.set_collisions)
horz.addWidget(label)
label.setBuddy(self.fileform)
horz.addWidget(self.fileform)
self.l.addLayout(horz)
self.gbl.addLayout(horz)
self.mergehide.append(self.fileform)
horz = QHBoxLayout()
@@ -224,32 +285,32 @@ class AddNewDialog(SizePersistedDialog):
self.collision.setToolTip("CollisionToolTip")
# add collision options
self.set_collisions()
i = self.collision.findText(prefs['collision'])
i = self.collision.findText(save_collisions[prefs['collision']])
if i > -1:
self.collision.setCurrentIndex(i)
self.collisionlabel.setBuddy(self.collision)
horz.addWidget(self.collision)
self.l.addLayout(horz)
self.gbl.addLayout(horz)
self.mergehide.append(self.collisionlabel)
self.mergehide.append(self.collision)
self.mergeupdateshow.append(self.collisionlabel)
self.mergeupdateshow.append(self.collision)
horz = QHBoxLayout()
self.updatemeta = QCheckBox('Update Calibre &Metadata?',self)
self.updatemeta.setToolTip("Update metadata for existing stories in Calibre from web site?\n(Columns set to 'New Only' in the column tabs will only be set for new books.)")
self.updatemeta = QCheckBox(_('Update Calibre &Metadata?'),self)
self.updatemeta.setToolTip(_("Update metadata for existing stories in Calibre from web site?\n(Columns set to 'New Only' in the column tabs will only be set for new books.)"))
self.updatemeta.setChecked(prefs['updatemeta'])
horz.addWidget(self.updatemeta)
self.mergehide.append(self.updatemeta)
self.mergeupdateshow.append(self.updatemeta)
self.updateepubcover = QCheckBox('Update EPUB Cover?',self)
self.updateepubcover.setToolTip('Update book cover image from site or defaults (if found) <i>inside</i> the EPUB when EPUB is updated.')
self.updateepubcover = QCheckBox(_('Update EPUB Cover?'),self)
self.updateepubcover.setToolTip(_('Update book cover image from site or defaults (if found) <i>inside</i> the EPUB when EPUB is updated.'))
self.updateepubcover.setChecked(prefs['updateepubcover'])
horz.addWidget(self.updateepubcover)
self.mergehide.append(self.updateepubcover)
self.l.addLayout(horz)
self.gbl.addLayout(horz)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.ok_clicked)
@@ -258,8 +319,8 @@ class AddNewDialog(SizePersistedDialog):
# invoke the
def ok_clicked(self):
self.dialog_closing(None) # save persistent size.
self.hide()
print("ok_clicked called")
self.go_signal.emit( self.get_ffdl_options(),
self.get_urlstext(),
self.merge,
@@ -287,12 +348,14 @@ class AddNewDialog(SizePersistedDialog):
self.newmerge = newmerge
self.extraoptions = extraoptions
self.extrapayload = extrapayload
self.groupbox.setVisible(not(self.merge and self.newmerge))
if self.merge:
self.toplabel.setText('Story URL(s) for anthology, one per line:')
self.url.setToolTip('URLs for stories to include in the anthology, one per line.\nWill take URLs from clipboard, but only valid URLs.')
self.collisionlabel.setText('If Story Already Exists in Anthology?')
self.collision.setToolTip("What to do if there's already an existing story with the same URL in the anthology.")
self.toplabel.setText(_('Story URL(s) for anthology, one per line:'))
self.url.setToolTip(_('URLs for stories to include in the anthology, one per line.\nWill take URLs from clipboard, but only valid URLs.'))
self.collisionlabel.setText(_('If Story Already Exists in Anthology?'))
self.collision.setToolTip(_("What to do if there's already an existing story with the same URL in the anthology."))
for widget in self.mergehide:
widget.setVisible(False)
if not self.newmerge:
@@ -301,10 +364,10 @@ class AddNewDialog(SizePersistedDialog):
else:
for widget in self.mergehide:
widget.setVisible(True)
self.toplabel.setText('Story URL(s), one per line:')
self.url.setToolTip('URLs for stories, one per line.\nWill take URLs from clipboard, but only valid URLs.\nAdd [1,5] after the URL to limit the download to chapters 1-5.')
self.collisionlabel.setText('If Story Already Exists?')
self.collision.setToolTip("What to do if there's already an existing story with the same URL or title and author.")
self.toplabel.setText(_('Story URL(s), one per line:'))
self.url.setToolTip(_('URLs for stories, one per line.\nWill take URLs from clipboard, but only valid URLs.\nAdd [1,5] after the URL to limit the download to chapters 1-5.'))
self.collisionlabel.setText(_('If Story Already Exists?'))
self.collision.setToolTip(_("What to do if there's already an existing story with the same URL or title and author."))
# Need to re-able after hiding/showing
self.setAcceptDrops(True)
@@ -320,10 +383,10 @@ class AddNewDialog(SizePersistedDialog):
# add collision options
self.set_collisions()
i = self.collision.findText(self.prefs['collision'])
i = self.collision.findText(save_collisions[self.prefs['collision']])
if i > -1:
self.collision.setCurrentIndex(i)
self.updatemeta.setChecked(self.prefs['updatemeta'])
if not self.merge:
@@ -359,6 +422,7 @@ class AddNewDialog(SizePersistedDialog):
'collision': unicode(self.collision.currentText()),
'updatemeta': self.updatemeta.isChecked(),
'updateepubcover': self.updateepubcover.isChecked(),
'smarten_punctuation':self.prefs['smarten_punctuation']
}
if self.merge:
@@ -403,18 +467,18 @@ class CollectURLDialog(SizePersistedDialog):
self.url.setText(url_text)
self.l.addWidget(self.url,1,1,1,2)
self.indiv_button = QPushButton('For Individual Books', self)
self.indiv_button.setToolTip('Get URLs and go to dialog for individual story downloads.')
self.indiv_button = QPushButton(_('For Individual Books'), self)
self.indiv_button.setToolTip(_('Get URLs and go to dialog for individual story downloads.'))
self.indiv_button.clicked.connect(self.indiv)
self.l.addWidget(self.indiv_button,2,0)
self.merge_button = QPushButton('For Anthology Epub', self)
self.merge_button.setToolTip('Get URLs and go to dialog for Anthology download.\nRequires EpubMerge 1.3.1+ plugin.')
self.merge_button = QPushButton(_('For Anthology Epub'), self)
self.merge_button.setToolTip(_('Get URLs and go to dialog for Anthology download.\nRequires %s plugin.')%'EpubMerge 1.3.1+')
self.merge_button.clicked.connect(self.merge)
self.l.addWidget(self.merge_button,2,1)
self.merge_button.setEnabled(epubmerge_plugin!=None)
self.cancel_button = QPushButton('Cancel', self)
self.cancel_button = QPushButton(_('Cancel'), self)
self.cancel_button.clicked.connect(self.cancel)
self.l.addWidget(self.cancel_button,2,2)
@@ -446,29 +510,29 @@ class UserPassDialog(QDialog):
self.setLayout(self.l)
if exception.passwdonly:
self.setWindowTitle('Password')
self.l.addWidget(QLabel("Author requires a password for this story(%s)."%exception.url),0,0,1,2)
self.setWindowTitle(_('Password'))
self.l.addWidget(QLabel(_("Author requires a password for this story(%s).")%exception.url),0,0,1,2)
# user isn't used, but it's easier to still have it for
# post processing.
self.user = FakeLineEdit()
else:
self.setWindowTitle('User/Password')
self.l.addWidget(QLabel("%s requires you to login to download this story."%site),0,0,1,2)
self.setWindowTitle(_('User/Password'))
self.l.addWidget(QLabel(_("%s requires you to login to download this story.")%site),0,0,1,2)
self.l.addWidget(QLabel("User:"),1,0)
self.l.addWidget(QLabel(_("User:")),1,0)
self.user = QLineEdit(self)
self.l.addWidget(self.user,1,1)
self.l.addWidget(QLabel("Password:"),2,0)
self.l.addWidget(QLabel(_("Password:")),2,0)
self.passwd = QLineEdit(self)
self.passwd.setEchoMode(QLineEdit.Password)
self.l.addWidget(self.passwd,2,1)
self.ok_button = QPushButton('OK', self)
self.ok_button = QPushButton(_('OK'), self)
self.ok_button.clicked.connect(self.ok)
self.l.addWidget(self.ok_button,3,0)
self.cancel_button = QPushButton('Cancel', self)
self.cancel_button = QPushButton(_('Cancel'), self)
self.cancel_button.clicked.connect(self.cancel)
self.l.addWidget(self.cancel_button,3,1)
@@ -490,9 +554,9 @@ class LoopProgressDialog(QProgressDialog):
book_list,
foreach_function,
finish_function,
init_label="Fetching metadata for stories...",
win_title="Downloading metadata for stories",
status_prefix="Fetched metadata for"):
init_label=_("Fetching metadata for stories..."),
win_title=_("Downloading metadata for stories"),
status_prefix=_("Fetched metadata for")):
QProgressDialog.__init__(self,
init_label,
QString(), 0, len(book_list), gui)
@@ -510,9 +574,9 @@ class LoopProgressDialog(QProgressDialog):
self.exec_()
def updateStatus(self):
self.setLabelText("%s %d of %d"%(self.status_prefix,self.i+1,len(self.book_list)))
self.setLabelText("%s %d / %d"%(self.status_prefix,self.i+1,len(self.book_list)))
self.setValue(self.i+1)
print(self.labelText())
#print(self.labelText())
def do_loop(self):
@@ -533,7 +597,7 @@ class LoopProgressDialog(QProgressDialog):
except Exception as e:
book['good']=False
book['comment']=unicode(e)
print("Exception: %s:%s"%(book,unicode(e)))
logger.error("Exception: %s:%s"%(book,unicode(e)))
traceback.print_exc()
self.updateStatus()
@@ -594,7 +658,8 @@ class UpdateExistingDialog(SizePersistedDialog):
def __init__(self, gui, header, prefs, icon, books,
save_size_name='fanfictiondownloader_plugin:update list dialog'):
SizePersistedDialog.__init__(self, gui, save_size_name)
self.prefs = prefs
self.setWindowTitle(header)
self.setWindowIcon(icon)
@@ -615,7 +680,7 @@ class UpdateExistingDialog(SizePersistedDialog):
spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
button_layout.addItem(spacerItem)
self.remove_button = QtGui.QToolButton(self)
self.remove_button.setToolTip('Remove selected books from the list')
self.remove_button.setToolTip(_('Remove selected books from the list'))
self.remove_button.setIcon(get_icon('list_remove.png'))
self.remove_button.clicked.connect(self.remove_from_list)
button_layout.addWidget(self.remove_button)
@@ -624,40 +689,59 @@ class UpdateExistingDialog(SizePersistedDialog):
options_layout = QHBoxLayout()
label = QLabel('Output &Format:')
options_layout.addWidget(label)
groupbox = QGroupBox(_("Show Download Options"))
groupbox.setCheckable(True)
groupbox.setChecked(False)
groupbox.setFlat(True)
groupbox.setStyleSheet(gpstyle)
gbf = QFrame()
gbl = QVBoxLayout()
gbl.addWidget(gbf)
groupbox.setLayout(gbl)
gbl = QHBoxLayout()
gbf.setLayout(gbl)
options_layout.addWidget(groupbox)
gbf.setVisible(False)
groupbox.toggled.connect(gbf.setVisible)
label = QLabel(_('Output &Format:'))
gbl.addWidget(label)
self.fileform = QComboBox(self)
self.fileform.addItem('epub')
self.fileform.addItem('mobi')
self.fileform.addItem('html')
self.fileform.addItem('txt')
self.fileform.setCurrentIndex(self.fileform.findText(prefs['fileform']))
self.fileform.setToolTip('Choose output format to create. May set default from plugin configuration.')
self.fileform.setToolTip(_('Choose output format to create. May set default from plugin configuration.'))
self.fileform.activated.connect(self.set_collisions)
label.setBuddy(self.fileform)
options_layout.addWidget(self.fileform)
gbl.addWidget(self.fileform)
label = QLabel('Update Mode:')
options_layout.addWidget(label)
label = QLabel(_('Update Mode:'))
gbl.addWidget(label)
self.collision = QComboBox(self)
self.collision.setToolTip("What sort of update to perform. May set default from plugin configuration.")
self.collision.setToolTip(_("What sort of update to perform. May set default from plugin configuration."))
# add collision options
self.set_collisions()
i = self.collision.findText(prefs['collision'])
i = self.collision.findText(save_collisions[prefs['collision']])
if i > -1:
self.collision.setCurrentIndex(i)
label.setBuddy(self.collision)
options_layout.addWidget(self.collision)
gbl.addWidget(self.collision)
self.updatemeta = QCheckBox('Update Calibre &Metadata?',self)
self.updatemeta.setToolTip("Update metadata for existing stories in Calibre from web site?\n(Columns set to 'New Only' in the column tabs will only be set for new books.)")
self.updatemeta = QCheckBox(_('Update Calibre &Metadata?'),self)
self.updatemeta.setToolTip(_("Update metadata for existing stories in Calibre from web site?\n(Columns set to 'New Only' in the column tabs will only be set for new books.)"))
self.updatemeta.setChecked(prefs['updatemeta'])
options_layout.addWidget(self.updatemeta)
gbl.addWidget(self.updatemeta)
self.updateepubcover = QCheckBox('Update EPUB Cover?',self)
self.updateepubcover.setToolTip('Update book cover image from site or defaults (if found) <i>inside</i> the EPUB when EPUB is updated.')
self.updateepubcover = QCheckBox(_('Update EPUB Cover?'),self)
self.updateepubcover.setToolTip(_('Update book cover image from site or defaults (if found) <i>inside</i> the EPUB when EPUB is updated.'))
self.updateepubcover.setChecked(prefs['updateepubcover'])
options_layout.addWidget(self.updateepubcover)
gbl.addWidget(self.updateepubcover)
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
button_box.accepted.connect(self.accept)
@@ -693,6 +777,7 @@ class UpdateExistingDialog(SizePersistedDialog):
'collision': unicode(self.collision.currentText()),
'updatemeta': self.updatemeta.isChecked(),
'updateepubcover': self.updateepubcover.isChecked(),
'smarten_punctuation':self.prefs['smarten_punctuation']
}
class StoryListTableWidget(QTableWidget):
@@ -705,7 +790,7 @@ class StoryListTableWidget(QTableWidget):
self.clear()
self.setAlternatingRowColors(True)
self.setRowCount(len(books))
header_labels = ['','Title', 'Author', 'URL', 'Comment']
header_labels = ['',_('Title'), _('Author'), 'URL', _('Comment')]
self.setColumnCount(len(header_labels))
self.setHorizontalHeaderLabels(header_labels)
self.horizontalHeader().setStretchLastSection(True)
@@ -773,9 +858,9 @@ class StoryListTableWidget(QTableWidget):
rows = self.selectionModel().selectedRows()
if len(rows) == 0:
return
message = '<p>Are you sure you want to remove this book from the list?'
message = '<p>'+_('Are you sure you want to remove this book from the list?')
if len(rows) > 1:
message = '<p>Are you sure you want to remove the selected %d books from the list?'%len(rows)
message = '<p>'+_('Are you sure you want to remove the selected %d books from the list?')%len(rows)
if not confirm(message,'fanfictiondownloader_delete_item', self):
return
first_sel_row = self.currentRow()
@@ -801,7 +886,7 @@ class RejectListTableWidget(QTableWidget):
self.clear()
self.setAlternatingRowColors(True)
self.setRowCount(len(reject_list))
header_labels = ['URL', 'Title', 'Author', 'Note']
header_labels = ['URL', _('Title'), _('Author'), _('Note')]
self.setColumnCount(len(header_labels))
self.setHorizontalHeaderLabels(header_labels)
self.horizontalHeader().setStretchLastSection(True)
@@ -828,7 +913,9 @@ class RejectListTableWidget(QTableWidget):
def populate_table_row(self, row, rej):
self.setItem(row, 0, ReadOnlyTableWidgetItem(rej.url))
url_cell = ReadOnlyTableWidgetItem(rej.url)
url_cell.setData(Qt.UserRole, QVariant(rej.book_id))
self.setItem(row, 0, url_cell)
self.setItem(row, 1, ReadOnlyTableWidgetItem(rej.title))
self.setItem(row, 2, ReadOnlyTableWidgetItem(rej.auth))
@@ -841,7 +928,7 @@ class RejectListTableWidget(QTableWidget):
note_cell.update_items_cache(items)
note_cell.show_initial_value(rej.note)
note_cell.set_separator(None)
note_cell.setToolTip('Select or Edit Reject Note.')
note_cell.setToolTip(_('Select or Edit Reject Note.'))
self.setCellWidget(row, 3, note_cell)
def remove_selected_rows(self):
@@ -849,9 +936,9 @@ class RejectListTableWidget(QTableWidget):
rows = self.selectionModel().selectedRows()
if len(rows) == 0:
return
message = '<p>Are you sure you want to remove this URL from the list?'
message = '<p>'+_('Are you sure you want to remove this URL from the list?')
if len(rows) > 1:
message = '<p>Are you sure you want to remove the %d selected URLs from the list?'%len(rows)
message = '<p>'+_('Are you sure you want to remove the %d selected URLs from the list?')%len(rows)
if not confirm(message,'ffdl_rejectlist_delete_item_again', self):
return
first_sel_row = self.currentRow()
@@ -869,7 +956,7 @@ class RejectListTableWidget(QTableWidget):
class RejectListDialog(SizePersistedDialog):
def __init__(self, gui, reject_list,
rejectreasons=[],
header="List of Books to Reject",
header=_("List of Books to Reject"),
icon='rotate-right.png',
show_delete=True,
show_all_reasons=True,
@@ -882,7 +969,7 @@ class RejectListDialog(SizePersistedDialog):
layout = QVBoxLayout(self)
self.setLayout(layout)
title_layout = ImageTitleLayout(self, icon, header,
'<i></i>FFDL will remember these URLs and display the note and offer to reject them if you try to download them again later.')
'<i></i>'+_('FFDL will remember these URLs and display the note and offer to reject them if you try to download them again later.'))
layout.addLayout(title_layout)
rejects_layout = QHBoxLayout()
layout.addLayout(rejects_layout)
@@ -896,7 +983,7 @@ class RejectListDialog(SizePersistedDialog):
button_layout.addItem(spacerItem)
self.remove_button = QtGui.QToolButton(self)
self.remove_button.setToolTip('Remove selected URL(s) from the list')
self.remove_button.setToolTip(_('Remove selected URL(s) from the list'))
self.remove_button.setIcon(get_icon('list_remove.png'))
self.remove_button.clicked.connect(self.remove_from_list)
button_layout.addWidget(self.remove_button)
@@ -914,11 +1001,11 @@ class RejectListDialog(SizePersistedDialog):
self.reason_edit.update_items_cache(items)
self.reason_edit.show_initial_value('')
self.reason_edit.set_separator(None)
self.reason_edit.setToolTip("This will be added to whatever note you've set for each URL above.")
self.reason_edit.setToolTip(_("This will be added to whatever note you've set for each URL above."))
horz = QHBoxLayout()
label = QLabel("Add this reason to all URLs added:")
label.setToolTip("This will be added to whatever note you've set for each URL above.")
label = QLabel(_("Add this reason to all URLs added:"))
label.setToolTip(_("This will be added to whatever note you've set for each URL above."))
horz.addWidget(label)
horz.addWidget(self.reason_edit)
horz.insertStretch(-1)
@@ -927,8 +1014,8 @@ class RejectListDialog(SizePersistedDialog):
options_layout = QHBoxLayout()
if show_delete:
self.deletebooks = QCheckBox('Delete Books (including books without FanFiction URLs)?',self)
self.deletebooks.setToolTip("Delete the selected books after adding them to the Rejected URLs list.")
self.deletebooks = QCheckBox(_('Delete Books (including books without FanFiction URLs)?'),self)
self.deletebooks.setToolTip(_("Delete the selected books after adding them to the Rejected URLs list."))
self.deletebooks.setChecked(True)
options_layout.addWidget(self.deletebooks)
@@ -950,10 +1037,19 @@ class RejectListDialog(SizePersistedDialog):
rejectrows = []
for row in range(self.rejects_table.rowCount()):
url = unicode(self.rejects_table.item(row, 0).text()).strip()
book_id = self.rejects_table.item(row, 0).data(Qt.UserRole).toPyObject()
title = unicode(self.rejects_table.item(row, 1).text()).strip()
auth = unicode(self.rejects_table.item(row, 2).text()).strip()
note = unicode(self.rejects_table.cellWidget(row, 3).currentText()).strip()
rejectrows.append(RejectUrlEntry(url,note,title,auth,self.get_reason_text()))
rejectrows.append(RejectUrlEntry(url,note,title,auth,self.get_reason_text(),book_id=book_id))
return rejectrows
def get_reject_list_ids(self):
rejectrows = []
for row in range(self.rejects_table.rowCount()):
book_id = self.rejects_table.item(row, 0).data(Qt.UserRole).toPyObject()
if book_id:
rejectrows.append(book_id)
return rejectrows
def get_reason_text(self):
File diff suppressed because it is too large Load Diff
+47 -25
View File
@@ -8,12 +8,16 @@ __copyright__ = '2012, Jim Miller'
__copyright__ = '2011, Grant Drake <grant.drake@gmail.com>'
__docformat__ = 'restructuredtext en'
import logging
logger = logging.getLogger(__name__)
import time, os, traceback
from StringIO import StringIO
from calibre.utils.ipc.server import Server
from calibre.utils.ipc.job import ParallelJob
from calibre.constants import numeric_version as calibre_version
from calibre_plugins.fanfictiondownloader_plugin.dialogs import (NotGoingToDownload,
OVERWRITE, OVERWRITEALWAYS, UPDATE, UPDATEALWAYS, ADDNEW, SKIP, CALIBREONLY)
@@ -37,19 +41,19 @@ def do_download_worker(book_list, options,
'''
server = Server(pool_size=cpus)
print(options['version'])
logger.info(options['version'])
total = 0
alreadybad = []
# Queue all the jobs
print("Adding jobs for URLs:")
logger.info("Adding jobs for URLs:")
for book in book_list:
print("%s"%book['url'])
logger.info("%s"%book['url'])
if book['good']:
total += 1
args = ['calibre_plugins.fanfictiondownloader_plugin.jobs',
'do_download_for_worker',
(book,options)]
job = ParallelJob('arbitrary',
job = ParallelJob('arbitrary_n',
"url:(%s) id:(%s)"%(book['url'],book['calibre_id']),
done=None,
args=args)
@@ -84,21 +88,18 @@ def do_download_worker(book_list, options,
book_id = job._book['calibre_id']
#title = job._title
count = count + 1
notification(float(count)/total, 'Downloaded Story')
notification(float(count)/total, '%d of %d stories finished downloading'%(count,total))
# Add this job's output to the current log
print('Logfile for book ID %s (%s)'%(book_id, job._book['title']))
print(job.details)
logger.info('Logfile for book ID %s (%s)'%(book_id, job._book['title']))
logger.info(job.details)
if count >= total:
# All done! Output some lists for convenience of some users.
print("Successfully downloaded:")
for book in book_list:
if book['good']:
print("%s %s"%(book['title'],book['url']))
print("\nUnsuccessful:")
for book in book_list:
if not book['good']:
print("%s %s"%(book['title'],book['url']))
logger.info("\nSuccessful:\n%s\n"%("\n".join([book['url'] for book in
filter(lambda x: x['good'], book_list) ] ) ) )
logger.info("\nUnsuccessful:\n%s\n"%("\n".join([book['url'] for book in
filter(lambda x: not x['good'], book_list) ] ) ) )
break
server.close()
@@ -106,7 +107,7 @@ def do_download_worker(book_list, options,
# return the book list as the job result
return book_list
def do_download_for_worker(book,options):
def do_download_for_worker(book,options,notification=lambda x,y:x):
'''
Child job, to extract isbn from formats for this specific book,
when run as a worker job
@@ -146,7 +147,7 @@ def do_download_for_worker(book,options):
## No need to download at all. Shouldn't ever get down here.
if options['collision'] in (CALIBREONLY):
print("Skipping CALIBREONLY 'update' down inside worker--this shouldn't be happening...")
logger.info("Skipping CALIBREONLY 'update' down inside worker--this shouldn't be happening...")
book['comment'] = 'Metadata collected.'
## checks were done earlier, it's new or not dup or newer--just write it.
@@ -169,7 +170,7 @@ def do_download_for_worker(book,options):
if adapter.logfile:
adapter.logfile = adapter.logfile.replace("span id","span notid")
print("write to %s"%outfile)
logger.info("write to %s"%outfile)
writer.writeStory(outfilename=outfile, forceOverwrite=True)
book['comment'] = 'Download %s completed, %s chapters.'%(options['fileform'],story.getMetadata("numChapters"))
@@ -178,7 +179,7 @@ def do_download_for_worker(book,options):
# update now handled by pre-populating the old images and
# chapters in the adapter rather than merging epubs.
urlchaptercount = int(story.getMetadata('numChapters'))
urlchaptercount = int(story.getMetadata('numChapters').replace(',',''))
(url,
chaptercount,
adapter.oldchapters,
@@ -197,15 +198,36 @@ def do_download_for_worker(book,options):
# dup handling from ffdl_plugin needed for anthology updates.
if chaptercount > urlchaptercount:
raise NotGoingToDownload("Existing epub contains %d chapters, web site only has %d. Use Overwrite to force update." % (chaptercount,urlchaptercount),'dialog_error.png')
print("Do update - epub(%d) vs url(%d)" % (chaptercount, urlchaptercount))
print("write to %s"%outfile)
if not (options['collision'] == UPDATEALWAYS and chaptercount == urlchaptercount) \
and adapter.getConfig("do_update_hook"):
chaptercount = adapter.hookForUpdates(chaptercount)
logger.info("Do update - epub(%d) vs url(%d)" % (chaptercount, urlchaptercount))
logger.info("write to %s"%outfile)
writer.writeStory(outfilename=outfile, forceOverwrite=True)
book['comment'] = 'Update %s completed, added %s chapters for %s total.'%\
(options['fileform'],(urlchaptercount-chaptercount),urlchaptercount)
if options['smarten_punctuation'] and options['fileform'] == "epub" \
and calibre_version >= (0, 9, 39):
# do smarten_punctuation from calibre's polish feature
from calibre.ebooks.oeb.polish.main import polish, ALL_OPTS
from calibre.utils.logging import Log
from collections import namedtuple
data = {'smarten_punctuation':True}
opts = ALL_OPTS.copy()
opts.update(data)
O = namedtuple('Options', ' '.join(ALL_OPTS.iterkeys()))
opts = O(**opts)
log = Log(level=Log.DEBUG)
# report = []
polish({outfile:outfile}, opts, log, logger.info) # report.append
except NotGoingToDownload as d:
book['good']=False
book['comment']=unicode(d)
@@ -216,8 +238,8 @@ def do_download_for_worker(book,options):
book['comment']=unicode(e)
book['icon']='dialog_error.png'
book['status'] = 'Error'
print("Exception: %s:%s"%(book,unicode(e)))
logger.info("Exception: %s:%s"%(book,unicode(e)))
traceback.print_exc()
#time.sleep(10)
return book
+7
View File
@@ -30,7 +30,11 @@ default_prefs['updatemeta'] = True
default_prefs['updatecover'] = False
default_prefs['updateepubcover'] = False
default_prefs['keeptags'] = False
default_prefs['suppressauthorsort'] = False
default_prefs['suppresstitlesort'] = False
default_prefs['mark'] = False
default_prefs['showmarked'] = False
default_prefs['autoconvert'] = False
default_prefs['urlsfromclip'] = True
default_prefs['updatedefault'] = True
default_prefs['fileform'] = 'epub'
@@ -40,7 +44,9 @@ default_prefs['adddialogstaysontop'] = False
default_prefs['includeimages'] = False
default_prefs['lookforurlinhtml'] = False
default_prefs['checkforseriesurlid'] = True
default_prefs['checkforurlchange'] = True
default_prefs['injectseries'] = False
default_prefs['smarten_punctuation'] = False
default_prefs['send_lists'] = ''
default_prefs['read_lists'] = ''
@@ -51,6 +57,7 @@ default_prefs['addtolistsonread'] = False
default_prefs['gcnewonly'] = False
default_prefs['gc_site_settings'] = {}
default_prefs['allow_gc_from_ini'] = True
default_prefs['gc_polish_cover'] = False
default_prefs['countpagesstats'] = []
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+505 -164
View File
File diff suppressed because it is too large Load Diff
+72 -20
View File
@@ -33,16 +33,24 @@ if sys.version_info >= (2, 7):
rootlogger.addHandler(loghandler)
try:
from fanficdownloader import adapters,writers,exceptions
from fanficdownloader.configurable import Configuration
from fanficdownloader.epubutils import get_dcsource_chaptercount, get_update_data
from fanficdownloader.geturls import get_urls_from_page
from calibre.constants import numeric_version as calibre_version
is_calibre = True
except:
is_calibre = False
# using try/except directly was masking errors during development.
if is_calibre:
# running under calibre
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters,writers,exceptions
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.configurable import Configuration
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.epubutils import get_dcsource_chaptercount, get_update_data
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader.geturls import get_urls_from_page
else:
from fanficdownloader import adapters,writers,exceptions
from fanficdownloader.configurable import Configuration
from fanficdownloader.epubutils import get_dcsource_chaptercount, get_update_data
from fanficdownloader.geturls import get_urls_from_page
if sys.version_info < (2, 5):
print "This program requires Python 2.5 or newer."
@@ -97,6 +105,9 @@ def main(argv,
parser.add_option("-n", "--normalize-list",
action="store_true", dest="normalize",default=False,
help="Get list of valid story URLs from page given, but normalized to standard forms.",)
parser.add_option("-s", "--sites-list",
action="store_true", dest="siteslist",default=False,
help="Get list of valid story URLs examples.",)
parser.add_option("-d", "--debug",
action="store_true", dest="debug",
help="Show debug output while downloading.",)
@@ -107,21 +118,45 @@ def main(argv,
logger = logging.getLogger("fanficdownloader")
logger.setLevel(logging.INFO)
if len(args) != 1:
if not options.siteslist and len(args) != 1:
parser.error("incorrect number of arguments")
if options.siteslist:
for (site,examples) in adapters.getSiteExamples():
print("\n====%s====\n\nExample URLs:"%site)
for u in examples:
print(" * %s"%u)
return
if options.update and options.format != 'epub':
parser.error("-u/--update-epub only works with epub")
## Attempt to update an existing epub.
chaptercount = None
output_filename = None
if options.update:
(url,chaptercount) = get_dcsource_chaptercount(args[0])
print "Updating %s, URL: %s" % (args[0],url)
output_filename = args[0]
try:
(url,chaptercount) = get_dcsource_chaptercount(args[0])
if not url:
print "No story URL found in epub to update."
return
print "Updating %s, URL: %s" % (args[0],url)
output_filename = args[0]
except:
# if there's an error reading the update file, maybe it's a URL?
# we'll look for an existing outputfile down below.
url = args[0]
else:
url = args[0]
configuration = Configuration(adapters.getConfigSectionFor(url),options.format)
try:
configuration = Configuration(adapters.getConfigSectionFor(url),options.format)
except exceptions.UnknownSite, e:
if options.list or options.normalize:
# list for page doesn't have to be a supported site.
configuration = Configuration("test1.com",options.format)
else:
raise e
conflist = []
homepath = join(expanduser("~"),".fanficdownloader")
@@ -156,8 +191,8 @@ def main(argv,
if options.force:
configuration.set("overrides","always_overwrite","true")
if options.update:
configuration.set("overrides","output_filename",args[0])
if options.update and chaptercount:
configuration.set("overrides","output_filename",output_filename)
if options.update and not options.updatecover:
configuration.set("overrides","never_make_cover","true")
@@ -174,14 +209,24 @@ def main(argv,
if options.list or options.normalize:
retlist = get_urls_from_page(args[0], configuration, normalize=options.normalize)
print "\n".join(retlist)
print "\n".join(retlist)
return
try:
adapter = adapters.getAdapter(configuration,url)
adapter.setChaptersRange(options.begin,options.end)
# check for updating from URL (vs from file)
if options.update and not chaptercount:
try:
writer = writers.getWriter("epub",configuration,adapter)
output_filename=writer.getOutputFileName()
(noturl,chaptercount) = get_dcsource_chaptercount(output_filename)
print "Updating %s, URL: %s" % (output_filename,url)
except:
options.update = False
pass
## Check for include_images and absence of PIL, give warning.
if adapter.getConfig('include_images'):
try:
@@ -217,13 +262,14 @@ def main(argv,
if options.update and not options.force:
urlchaptercount = int(adapter.getStoryMetadataOnly().getMetadata('numChapters'))
if chaptercount == urlchaptercount and not options.metaonly:
print "%s already contains %d chapters." % (args[0],chaptercount)
print "%s already contains %d chapters." % (output_filename,chaptercount)
elif chaptercount > urlchaptercount:
print "%s contains %d chapters, more than source: %d." % (args[0],chaptercount,urlchaptercount)
print "%s contains %d chapters, more than source: %d." % (output_filename,chaptercount,urlchaptercount)
elif chaptercount == 0:
print "%s doesn't contain any recognizable chapters, probably from a different source. Not updating." % (output_filename)
else:
print "Do update - epub(%d) vs url(%d)" % (chaptercount, urlchaptercount)
if not options.metaonly:
# update now handled by pre-populating the old
@@ -235,7 +281,13 @@ def main(argv,
adapter.oldimgs,
adapter.oldcover,
adapter.calibrebookmark,
adapter.logfile) = get_update_data(args[0])
adapter.logfile) = get_update_data(output_filename)
print "Do update - epub(%d) vs url(%d)" % (chaptercount, urlchaptercount)
if not (options.update and chaptercount == urlchaptercount) \
and adapter.getConfig("do_update_hook"):
chaptercount = adapter.hookForUpdates(chaptercount)
writeStory(configuration,adapter,"epub")
Binary file not shown.
+57
View File
@@ -0,0 +1,57 @@
# coding: utf-8
import re
import codecs
stack = []
def get_end_tag(tag):
if len(tag) > 0 and tag.find(u'<') > -1 and tag.rfind(u'>') > -1:
return re.sub(r'.*<([^\ >]+).*', r'</\1>', tag)
return u''
def get_tag_name(tag):
if len(tag) > 0 and tag.find(u'<') > -1 and tag.rfind(u'>') > -1:
return re.sub(r'</*([^\ >]+).*', r'\1', tag)
return u''
def push(tag):
if len(tag) > 0 and tag.find(u'<') > -1 and tag.rfind(u'>') > -1:
stack.append(tag)
def pop():
if len(stack) > 0:
return stack.pop()
return u''
def pop_end_tag():
return unicode(get_end_tag(pop()))
def spool_end():
html = u''
for tag in reversed(stack):
html += get_end_tag(tag)
return html
def spool_start():
html = u''
for item in stack:
html += item
return html
def has_elements():
return len(stack) > 0
def get_last():
# t = pop()
# push(t)
# return t
if len(stack) > 0:
return stack[len(stack)-1]
return u''
def flush():
del stack[:]
def get_stack():
return stack
+12 -9
View File
@@ -4,13 +4,16 @@ try:
# just a way to switch between web service and CLI/PI
import google.appengine.api
except:
import sys
if sys.version_info >= (2, 7):
import logging
logger = logging.getLogger(__name__)
loghandler=logging.StreamHandler()
loghandler.setFormatter(logging.Formatter("FFDL:%(levelname)s:%(filename)s(%(lineno)d):%(message)s"))
logger.addHandler(loghandler)
loghandler.setLevel(logging.DEBUG)
logger.setLevel(logging.DEBUG)
try: # just a way to switch between CLI and PI
import calibre.constants
except:
import sys
if sys.version_info >= (2, 7):
import logging
logger = logging.getLogger(__name__)
loghandler=logging.StreamHandler()
loghandler.setFormatter(logging.Formatter("FFDL:%(levelname)s:%(filename)s(%(lineno)d):%(message)s"))
logger.addHandler(loghandler)
loghandler.setLevel(logging.DEBUG)
logger.setLevel(logging.DEBUG)
+43 -13
View File
@@ -71,7 +71,6 @@ import adapter_thehexfilesnet
import adapter_dokugacom
import adapter_iketernalnet
import adapter_onedirectionfanfictioncom
import adapter_prisonbreakficnet
import adapter_storiesofardacom
import adapter_samdeanarchivenu
import adapter_destinysgatewaycom
@@ -88,7 +87,6 @@ import adapter_pretendercentrecom
import adapter_darksolaceorg
import adapter_finestoriescom
import adapter_hpfanficarchivecom
import adapter_svufictioncom
import adapter_twilightarchivescom
import adapter_wizardtalesnet
import adapter_nhamagicalworldsus
@@ -103,7 +101,6 @@ import adapter_merlinficdtwinscouk
import adapter_thehookupzonenet
import adapter_bloodtiesfancom
import adapter_indeathnet
import adapter_jlaunlimitedcom
import adapter_qafficcom
import adapter_efpfanficnet
import adapter_potterficscom
@@ -115,6 +112,17 @@ import adapter_imagineeficcom
import adapter_buffynfaithnet
import adapter_psychficcom
import adapter_hennethannunnet
import adapter_tokrafandomnetcom
import adapter_netraptororg
import adapter_asr3slashzoneorg
import adapter_nickandgregnet
import adapter_potterheadsanonymouscom
import adapter_simplyundeniablecom
import adapter_scarheadnet
import adapter_fictionpadcom
import adapter_storiesonlinenet
import adapter_trekiverseorg
import adapter_literotica
## This bit of complexity allows adapters to be added by just adding
## importing. It eliminates the long if/else clauses we used to need
@@ -122,6 +130,7 @@ import adapter_hennethannunnet
## List of registered site adapters.
__class_list = []
__domain_map = {}
def imports():
for name, val in globals().items():
@@ -131,9 +140,19 @@ def imports():
for x in imports():
if "fanficdownloader.adapters.adapter_" in x:
#print x
__class_list.append(sys.modules[x].getClass())
cls = sys.modules[x].getClass()
__class_list.append(cls)
for site in cls.getAcceptDomains():
__domain_map[site]=cls
def getNormalStoryURL(url):
r = getNormalStoryURLSite(url)
if r:
return r[0]
else:
return None
def getNormalStoryURLSite(url):
if not getNormalStoryURL.__dummyconfig:
getNormalStoryURL.__dummyconfig = Configuration("test1.com","EPUB")
# pulling up an adapter is pretty low over-head. If
@@ -141,20 +160,23 @@ def getNormalStoryURL(url):
try:
adapter = getAdapter(getNormalStoryURL.__dummyconfig,url)
url = adapter.url
site = adapter.getSiteDomain()
del adapter
return url
return (url,site)
except:
return None;
return None
# kludgey function static/singleton
getNormalStoryURL.__dummyconfig = None
def getAdapter(config,url):
def getAdapter(config,url,anyurl=False):
#logger.debug("trying url:"+url)
(cls,fixedurl) = getClassFor(url)
#logger.debug("fixedurl:"+fixedurl)
if cls:
if anyurl:
fixedurl = cls.getSiteExampleURLs().split()[0]
adapter = cls(config,fixedurl) # raises InvalidStoryURL
return adapter
# No adapter found.
@@ -163,6 +185,12 @@ def getAdapter(config,url):
def getConfigSections():
return [cls.getConfigSection() for cls in __class_list]
def getSiteExamples():
l=[]
for cls in sorted(__class_list, key=lambda x : x.getConfigSection()):
l.append((cls.getConfigSection(),cls.getSiteExampleURLs().split()))
return l
def getConfigSectionFor(url):
(cls,fixedurl) = getClassFor(url)
if cls:
@@ -173,7 +201,7 @@ def getConfigSectionFor(url):
def getClassFor(url):
## fix up leading protocol.
fixedurl = re.sub(r"(?i)^[htps]+[:/]+","http://",url.strip())
fixedurl = re.sub(r"(?i)^[htp]+(s?)[:/]+",r"http\1://",url.strip())
if not fixedurl.startswith("http"):
fixedurl = "http://%s"%url
## remove any trailing '#' locations.
@@ -194,12 +222,14 @@ def getClassFor(url):
#logger.debug("trying site:www."+domain)
cls = getClassFromList("www."+domain)
fixedurl = fixedurl.replace("http://","http://www.")
fixedurl = cls.stripURLParameters(fixedurl)
if cls:
fixedurl = cls.stripURLParameters(fixedurl)
return (cls,fixedurl)
def getClassFromList(domain):
for cls in __class_list:
if cls.matchesSite(domain):
return cls
try:
return __domain_map[domain]
except KeyError:
pass # return none.
@@ -42,7 +42,7 @@ class AdAstraFanficComSiteAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -52,6 +52,7 @@ class AdAstraFanficComSiteAdapter(BaseSiteAdapter):
def getSiteDomain():
return 'www.adastrafanfic.com'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -89,7 +90,7 @@ class AdAstraFanficComSiteAdapter(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
self.story.setMetadata('title',stripHTML(a))
# Find authorid and URL from... author url.
a = soup.find('a', href=re.compile(r"viewuser.php"))
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
# Copyright 2011 Fanficdownloader team
#
@@ -51,13 +51,13 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# get storyId from url--url validation guarantees query correct
m = re.match(self.getSiteURLPattern(),url)
if m:
self.story.setMetadata('storyId',m.group('id'))
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/works/'+self.story.getMetadata('storyId'))
else:
@@ -77,13 +77,14 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'archiveofourown.org'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/works/123456 http://"+self.getSiteDomain()+"/collections/Some_Archive/works/123456"
return "http://"+self.getSiteDomain()+"/works/123456 http://"+self.getSiteDomain()+"/collections/Some_Archive/works/123456 http://"+self.getSiteDomain()+"/works/123456/chapters/78901"
def getSiteURLPattern(self):
# http://archiveofourown.org/collections/Smallville_Slash_Archive/works/159770
# Discard leading zeros from story ID numbers--AO3 doesn't use them in it's own chapter URLs.
return re.escape("http://")+re.escape(self.getSiteDomain())+r"(/collections/[^/]+)?/works/0*(?P<id>\d+)"
return r"https?://"+re.escape(self.getSiteDomain())+r"(/collections/[^/]+)?/works/0*(?P<id>\d+)"
## Login
def needToLoginCheck(self, data):
@@ -162,7 +163,7 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile(r"^/works/\d+$"))
self.story.setMetadata('title',a.string)
self.story.setMetadata('title',stripHTML(a))
# Find authorid and URL from... author url.
alist = soup.findAll('a', href=re.compile(r"^/users/\w+/pseuds/\w+"))
@@ -176,19 +177,28 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
self.story.addToList('authorUrl','http://'+self.host+a['href'])
self.story.addToList('author',a.text)
newestChapter = None
self.newestChapterNum = None # save for comparing during update.
# Scan all chapters to find the oldest and newest, on AO3 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.
# Find the chapters:
chapters=soup.findAll('a', href=re.compile(r'/works/'+self.story.getMetadata('storyId')+"/chapters/\d+$"))
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 <i> in chapter titles.
chapter=chapters[x]
if len(chapters)==1:
self.chapterUrls.append((self.story.getMetadata('title'),'http://'+self.host+chapter['href']+addurl))
else:
if len(chapters)==1:
self.chapterUrls.append((self.story.getMetadata('title'),'http://'+self.host+chapters[0]['href']+addurl))
else:
for index, chapter in enumerate(chapters):
# strip just in case there's tags, like <i> in chapter titles.
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+chapter['href']+addurl))
# (2013-09-21)
date = stripHTML(chapter.findNext('span'))[1:-1]
chapterDate = makeDate(date,self.dateformat)
if newestChapter == None or chapterDate > newestChapter:
newestChapter = chapterDate
self.newestChapterNum = index
a = metasoup.find('blockquote',{'class':'userstuff'})
if a != None:
@@ -209,10 +219,7 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
if a != None:
warnings = a.findAll('a',{'class':"tag"})
for warning in warnings:
if warning.string == "Author Chose Not To Use Archive Warnings":
warning.string = "No Archive Warnings Apply"
if warning.string != "No Archive Warnings Apply":
self.story.addToList('warnings',warning.string)
self.story.addToList('warnings',warning.string)
a = metasoup.find('dd',{'class':"freeform tags"})
if a != None:
@@ -287,25 +294,31 @@ class ArchiveOfOurOwnOrgAdapter(BaseSiteAdapter):
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), self.dateformat))
try:
# Find Series name from series URL.
a = metasoup.find('dd',{'class':"series"})
b = a.find('a', href=re.compile(r"/series/\d+"))
series_name = b.string
series_url = 'http://'+self.host+'/fanfic/'+b['href']
series_index = int(a.text.split(' ')[1])
self.setSeries(series_name, series_index)
self.story.setMetadata('seriesUrl',series_url)
except:
# I find it hard to care if the series parsing fails
pass
# Find Series name from series URL.
ddseries = metasoup.find('dd',{'class':"series"})
if ddseries:
for i, a in enumerate(ddseries.findAll('a', href=re.compile(r"/series/\d+"))):
series_name = stripHTML(a)
series_url = 'http://'+self.host+a['href']
series_index = int(stripHTML(a.previousSibling).replace(', ','').split(' ')[1]) # "Part # of" or ", Part #"
self.story.setMetadata('series%02d'%i,"%s [%s]"%(series_name,series_index))
self.story.setMetadata('series%02dUrl'%i,series_url)
if i == 0:
self.setSeries(series_name, series_index)
self.story.setMetadata('seriesUrl',series_url)
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)
# grab the text for an individual chapter.
def getChapterText(self, url):
logger.debug('Getting chapter text from: %s' % url)
chapter=bs.BeautifulSoup('<div class="story"></div>')
chapter=bs.BeautifulSoup('<div class="story"></div>').find('div')
data = self._fetchUrl(url)
soup = bs.BeautifulSoup(data,selfClosingTags=('br','hr'))
@@ -49,7 +49,7 @@ class ArchiveSkyeHawkeComAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/story.php?no='+self.story.getMetadata('storyId'))
@@ -70,6 +70,7 @@ class ArchiveSkyeHawkeComAdapter(BaseSiteAdapter):
def getAcceptDomains(cls):
return ['archive.skyehawke.com','www.skyehawke.com']
@classmethod
def getSiteExampleURLs(self):
return "http://archive.skyehawke.com/story.php?no=1234 http://www.skyehawke.com/archive/story.php?no=1234 http://skyehawke.com/archive/story.php?no=1234"
@@ -100,7 +101,7 @@ class ArchiveSkyeHawkeComAdapter(BaseSiteAdapter):
## Title
a = soup.find('div', {'class':"story border"}).find('span',{'class':'left'})
title=a.text.split('"')[1]
title=stripHTML(a).split('"')[1]
self.story.setMetadata('title',title)
# Find authorid and URL from... author url.
@@ -48,7 +48,7 @@ class AshwinderSycophantHexComAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
@@ -66,6 +66,7 @@ class AshwinderSycophantHexComAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'ashwinder.sycophanthex.com'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -156,7 +157,7 @@ class AshwinderSycophantHexComAdapter(BaseSiteAdapter):
for info in asoup.findAll('table', {'width' : '100%', 'bordercolor' : re.compile(r'#')}):
a = info.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
if a != None:
self.story.setMetadata('title',a.string)
self.story.setMetadata('title',stripHTML(a))
break
@@ -0,0 +1,226 @@
# -*- 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 Asr3SlashzoneOrgAdapter
class Asr3SlashzoneOrgAdapter(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','asr3')
# 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 'asr3.slashzone.org'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.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 = "&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+((?:&amp;ageconsent=ok)?&amp;warning=\d+)'",data)
if m != None:
if self.is_adult or self.getConfig("is_adult"):
# We tried the default and still got a warning, so
# let's pull the warning number from the 'continue'
# link and reload data.
addurl = m.group(1)
# correct stupid &amp; error in url.
addurl = addurl.replace("&amp;","&")
url = self.url+'&index=1'+addurl
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)
# 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+'/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.
metadiv = soup.find('div',{'class':'content'})
smalldiv = metadiv.find('div',{'class':'small'})
categorys = smalldiv.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
for category in categorys:
self.story.addToList('category',category.string)
chars = smalldiv.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
for char in chars:
self.story.addToList('characters',char.string)
ships = smalldiv.parent.findAll('a',href=re.compile(r'browse\.php\?type=class&type_id=2&classid=1'))
for ship in ships:
self.story.addToList('ships',ship.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+'/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
# 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', {'id' : 'story'})
if None == div:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
return self.utf8FromSoup(url,div)
@@ -70,7 +70,7 @@ class BloodTiesFansComAdapter(BaseSiteAdapter): # XXX
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
# XXX Most sites don't have the /fanfic part. Replace all to remove it usually.
@@ -88,6 +88,7 @@ class BloodTiesFansComAdapter(BaseSiteAdapter): # XXX
# The site domain. Does have www here, if it uses it.
return 'bloodties-fans.com' # XXX
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/fiction/viewstory.php?sid=1234"
@@ -206,12 +207,12 @@ class BloodTiesFansComAdapter(BaseSiteAdapter): # XXX
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
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('authorUrl','http://'+self.host+'/fiction/'+a['href'])
self.story.setMetadata('author',a.string)
# Find the chapters:
@@ -58,7 +58,7 @@ class BuffyNFaithNetAdapter(BaseSiteAdapter):
m = re.match(self.getSiteURLPattern(),url)
if m:
self.story.setMetadata('storyId',m.group('id'))
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL. gets rid of chapter if there, left with ch 1 URL on this site
nurl = "http://"+self.getSiteDomain()+"/fanfictions/index.php?act=vie&id="+self.story.getMetadata('storyId')
self._setURL(nurl)
@@ -69,7 +69,7 @@ class BuffyNFaithNetAdapter(BaseSiteAdapter):
raise exceptions.InvalidStoryURL(url,
self.getSiteDomain(),
self.getSiteExampleURLs())
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','bnfnet')
@@ -87,10 +87,11 @@ class BuffyNFaithNetAdapter(BaseSiteAdapter):
def setHeader(self):
"buffynfaith.net wants a Referer for images. Used both above and below(after cookieproc added)"
self.opener.addheaders = [('Referer', 'http://'+self.getSiteDomain()+'/')]
self.opener.addheaders.append(('Referer', 'http://'+self.getSiteDomain()+'/'))
@classmethod
def getSiteExampleURLs(self):
return "http://buffynfaith.net/fanfictions/index.php?act=vie&id=963 http://buffynfaith.net/fanfictions/index.php?act=vie&id=949 http://buffynfaith.net/fanfictions/index.php?act=vie&id=949&ch=2"
return "http://"+self.getSiteDomain()+"/fanfictions/index.php?act=vie&id=1234 http://buffynfaith.net/fanfictions/index.php?act=ovr&id=1234 http://buffynfaith.net/fanfictions/index.php?act=vie&id=1234&ch=2"
def getSiteURLPattern(self):
#http://buffynfaith.net/fanfictions/index.php?act=vie&id=963
@@ -72,7 +72,7 @@ class CastleFansOrgAdapter(BaseSiteAdapter): # XXX
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
# XXX Most sites don't have the /fanfic part. Replace all to remove it usually.
@@ -90,6 +90,7 @@ class CastleFansOrgAdapter(BaseSiteAdapter): # XXX
# The site domain. Does have www here, if it uses it.
return 'castlefans.org' # XXX
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/fanfic/viewstory.php?sid=1234"
@@ -179,12 +180,12 @@ class CastleFansOrgAdapter(BaseSiteAdapter): # XXX
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',a.string)
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('authorUrl','http://'+self.host+'/fanfic/'+a['href'])
self.story.setMetadata('author',a.string)
# Find the chapters:
@@ -48,7 +48,7 @@ class ChaosSycophantHexComAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
@@ -66,6 +66,7 @@ class ChaosSycophantHexComAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'chaos.sycophanthex.com'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -115,7 +116,7 @@ class ChaosSycophantHexComAdapter(BaseSiteAdapter):
## 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',a.text)
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+"))
@@ -48,7 +48,7 @@ class CheckmatedComAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
self._setURL('http://' + self.getSiteDomain() + '/story.php?story='+self.story.getMetadata('storyId'))
@@ -64,6 +64,7 @@ class CheckmatedComAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'www.checkmated.com'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/story.php?story=1234"
@@ -142,11 +143,11 @@ class CheckmatedComAdapter(BaseSiteAdapter):
# Now go hunting for all the meta data and the chapter list.
## Title
a = soup.findAll('span', {'class' : 'storytitle'})
self.story.setMetadata('title',a[0].string)
a = soup.find('span', {'class' : 'storytitle'})
self.story.setMetadata('title',stripHTML(a))
# Find authorid and URL from... author url.
a = a[1].find('a', href=re.compile(r"authors.php\?name\=\w+"))
a = a.parent.find('a', href=re.compile(r"authors.php\?name\=\w+"))
self.story.setMetadata('authorId',a['href'].split('=')[1])
self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href'])
self.story.setMetadata('author',a.string)
@@ -48,8 +48,7 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/elysian/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -59,7 +58,7 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%d %B %Y"
self.dateformat = "%B %d, %Y"
@staticmethod # must be @staticmethod, don't remove it.
def getSiteDomain():
@@ -70,6 +69,7 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
def getAcceptDomains(cls):
return ['www.dark-solace.org','dark-solace.org']
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/elysian/viewstory.php?sid=1234"
@@ -79,7 +79,8 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
## Login seems to be reasonably standard across eFiction sites.
def needToLoginCheck(self, data):
if 'This story contains adult content not suitable for children' in data \
or "That password doesn't match the one in our database" in data:
or "That password doesn't match the one in our database" in data \
or "Registered Users Only" in data:
return True
else:
return False
@@ -93,18 +94,16 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
else:
params['penname'] = self.getConfig("username")
params['password'] = self.getConfig("password")
params['rememberme'] = '1'
params['sid'] = ''
params['intent'] = ''
params['action'] = 'login'
params['submit'] = 'Submit'
loginUrl = 'http://' + self.getSiteDomain() + '/elysian/user.php'
loginUrl = 'http://www.' + self.getSiteDomain() + '/elysian/user.php'
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
params['penname']))
d = self._fetchUrl(loginUrl, params)
d = self._postUrl(loginUrl, params)
if "User Account Page" not in d : #Member Account
if "Member Account" not in d : #User Account Page
logger.info("Failed to login to URL %s as %s, or have no authorization to access the story" % (loginUrl, params['penname']))
raise exceptions.FailedToLogin(url,params['penname'])
return False
@@ -113,9 +112,19 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
## 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
url = self.url+'&index=1'+addurl
logger.debug("URL: "+url)
try:
@@ -128,9 +137,30 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
if self.needToLoginCheck(data):
# need to log in for this one.
addurl="&ageconsent=ok"
self.performLogin(url)
data = self._fetchUrl(url+addurl)
data = self._fetchUrl(url)
m = re.search(r"'viewstory.php\?sid=\d+((?:&amp;ageconsent=ok)?&amp;warning=\d+)'",data)
if m != None:
if self.is_adult or self.getConfig("is_adult"):
# We tried the default and still got a warning, so
# let's pull the warning number from the 'continue'
# link and reload data.
addurl = m.group(1)
# correct stupid &amp; error in url.
addurl = addurl.replace("&amp;","&")
url = self.url+'&index=1'+addurl
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.")
@@ -142,34 +172,42 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
# Now go hunting for all the meta data and the chapter list.
## Title and author
a = soup.find('div', {'id' : 'pagetitle'})
div = soup.find('div', {'id' : 'pagetitle'})
aut = a.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
aut = div.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+'/elysian/'+aut['href'])
self.story.setMetadata('author',aut.string)
aut.extract()
# first a tag in pagetitle is title
self.story.setMetadata('title',stripHTML(div.find('a')))
for chapa in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+
self.story.getMetadata('storyId')+'&chapter=\d+')):
self.chapterUrls.append((stripHTML(chapa),'http://'+self.host+'/elysian/'+chapa['href']))
self.story.setMetadata('title',a.string[:(len(a.string)-3)])
# Find the chapters:
chapters=soup.find('select', {'name' : 'chapter'})
if chapters != None:
for chapter in chapters.findAll('option'):
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/elysian/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='))
if a != None:
if 'viewstory.php?sid='+self.story.getMetadata('storyId') in a['href']:
storylink = asoup.find('a', href=re.compile(r'viewstory.php\?sid='+
self.story.getMetadata('storyId')+'($|[^\d])'))
# author's story list is paginated if there's a pagelinks div.
# Only need to look in it if the story wasn't on the first page.
pagelinks = asoup.find('div',{'id':'pagelinks'})
if pagelinks and storylink==None:
authpageslist = pagelinks.findAll('a',href=re.compile(r'action=storiesby'))
for page in authpageslist[1:]: # skip first, already checked above.
asoup = bs.BeautifulSoup(self._fetchUrl('http://'+self.host+'/elysian/'+page['href']))
storylink = asoup.find('a', href=re.compile(r'viewstory.php\?sid='+
self.story.getMetadata('storyId')+'($|[^\d])'))
if storylink:
break
if not storylink:
raise exceptions.FailedToDownload("Unable to find story metadata on author's page(s)")
metalist = storylink.parent.parent
# eFiction sites don't help us out a lot with their meta data
# formating, so it's a little ugly.
@@ -182,7 +220,7 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
# <span class="label">Rated:</span> NC-17<br /> etc
labels = list.findAll('span', {'class' : 'classification'})
labels = metalist.findAll('span', {'class' : 'label'})
for labelspan in labels:
label = labelspan.text
value = labelspan.nextSibling
@@ -190,7 +228,7 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
if 'Summary' in label:
## Everything until the next span class='label'
svalue = ""
while not (defaultGetattr(value,'class') == 'classification' or "Chapters: " in stripHTML(value)):
while value and not (defaultGetattr(value,'class') == 'label' or "Chapters: " in stripHTML(value)):
svalue += str(value)
value = value.nextSibling
self.setDescription(url,svalue)
@@ -238,35 +276,54 @@ class DarkSolaceOrgAdapter(BaseSiteAdapter):
try:
# Find Series name from series URL.
a = list.find('a', href=re.compile(r"series.php\?seriesid=\d+"))
a = metalist.find('a', href=re.compile(r"series.php\?seriesid=\d+"))
series_name = a.string
series_url = 'http://'+self.host+'/elysian/'+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)
storylink = seriessoup.find('a', href=re.compile(r'viewstory.php\?sid='+
self.story.getMetadata('storyId')+'($|[^\d])'))
if storylink and storylink.parent and storylink.parent['class'] != 'title': # in case of links inside story summaries.
storylink = None
offset = 0
# series story list is paginated if there's a pagelinks div.
# Only need to look in it if the story wasn't on the first page.
pagelinks = seriessoup.find('div',{'id':'pagelinks'})
if pagelinks and storylink==None:
authpageslist = pagelinks.findAll('a',href=re.compile(r'offset='))
for page in authpageslist[1:]: # skip first, already checked above.
seriessoup = bs.BeautifulSoup(self._fetchUrl('http://'+self.host+'/elysian/'+page['href']))
storylink = seriessoup.find('a', href=re.compile(r'viewstory.php\?sid='+
self.story.getMetadata('storyId')+'($|[^\d])'))
if storylink and storylink.parent and storylink.parent['class'] != 'title': # in case of links inside story summaries.
storylink = None
if storylink:
offset = int(page['href'].split('=')[-1]) # offset is last.
break
i+=1
# for reasons I don't understand, searching for story
# links by regex wasn't working reliably. It was missing
# the javascript links sometimes. This is cleaner anyway.
for i, div in enumerate(seriessoup.findAll('div', {'class':'title'})):
a = div.find('a') # first a is story link.
# skip 'report this' and 'TOC' links
if a == storylink:
self.setSeries(series_name, 1+i+offset)
self.story.setMetadata('seriesUrl',series_url)
break
except:
# I find it hard to care if the series parsing fails
pass
except Exception, e:
print("Series parsing failed: %s"%e)
# 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.
soup = bs.BeautifulSoup(self._fetchUrl(url))
div = soup.find('div', {'id' : 'story'})
@@ -48,7 +48,7 @@ class DestinysGatewayComAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -65,6 +65,7 @@ class DestinysGatewayComAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'www.destinysgateway.com'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -130,7 +131,7 @@ class DestinysGatewayComAdapter(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
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+"))
@@ -48,7 +48,7 @@ class DokugaComAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[3])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# www.dokuga.com has two 'sections', shown in URL as
# 'fanfiction' and 'spark' that change how things should be
@@ -75,6 +75,7 @@ class DokugaComAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'www.dokuga.com'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/fanfiction/story/1234/1 http://"+self.getSiteDomain()+"/spark/story/1234/1"
@@ -163,7 +164,7 @@ class DokugaComAdapter(BaseSiteAdapter):
aut.extract()
a = a.string[:(len(a.string)-4)]
self.story.setMetadata('title',a)
self.story.setMetadata('title',stripHTML(a))
# Find the chapters:
chapters = soup.find('select').findAll('option')
@@ -204,7 +205,6 @@ class DokugaComAdapter(BaseSiteAdapter):
a=div.text.split('Category: ')
if len(a) == 2: self.story.addToList('category', a[1].split('&')[0])
self.story.addToList('category', 'Fanfiction')
a=div.text.split('Created: ')
if len(a) == 2: self.story.setMetadata('datePublished', makeDate(stripHTML(a[1].split('&')[0]), self.dateformat))
@@ -48,7 +48,7 @@ class DotMoonNetAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL. www.dotmoon.net/library_view.php?storyid=3
self._setURL('http://' + self.getSiteDomain() + '/library_view.php?storyid='+self.story.getMetadata('storyId'))
@@ -65,6 +65,7 @@ class DotMoonNetAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'www.dotmoon.net'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/library_view.php?storyid=1234"
@@ -140,7 +141,7 @@ class DotMoonNetAdapter(BaseSiteAdapter):
## Title
a = body.find('b')
self.story.setMetadata('title',a.string)
self.story.setMetadata('title',stripHTML(a))
# Find authorid and URL from... author url. http://www.dotmoon.net/board/index.php?action=profile;u=1'
a = body.find('a', href=re.compile(r"index.php\?action=profile;u=\d+"))
@@ -48,7 +48,7 @@ class DracoAndGinnyComAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -65,6 +65,7 @@ class DracoAndGinnyComAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'www.dracoandginny.com'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -169,7 +170,7 @@ class DracoAndGinnyComAdapter(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
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+"))
@@ -48,7 +48,7 @@ class DramioneOrgAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -65,6 +65,7 @@ class DramioneOrgAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'dramione.org'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -153,7 +154,7 @@ class DramioneOrgAdapter(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
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+"))
@@ -237,6 +238,9 @@ class DramioneOrgAdapter(BaseSiteAdapter):
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:
@@ -282,6 +286,14 @@ class DramioneOrgAdapter(BaseSiteAdapter):
# I find it hard to care if the series parsing fails
pass
try:
self.story.setMetadata('reviews',
stripHTML(soup.find('h2',{'id':'pagetitle'}).
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):
@@ -48,7 +48,7 @@ class EfictionEstelielDeAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -65,6 +65,7 @@ class EfictionEstelielDeAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'efiction.esteliel.de'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -103,7 +104,7 @@ class EfictionEstelielDeAdapter(BaseSiteAdapter):
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',a.string)
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+"))
@@ -48,7 +48,7 @@ class EFPFanFicNet(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -65,6 +65,7 @@ class EFPFanFicNet(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'www.efpfanfic.net'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -165,7 +166,7 @@ class EFPFanFicNet(BaseSiteAdapter):
logger.debug("Normalizing to URL: "+url)
self._setURL(url)
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# eFiction sites don't help us out a lot with their meta data
# formating, so it's a little ugly.
@@ -48,7 +48,7 @@ class ErosnSapphoSycophantHexComAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
@@ -66,6 +66,7 @@ class ErosnSapphoSycophantHexComAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'erosnsappho.sycophanthex.com'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -131,7 +132,7 @@ class ErosnSapphoSycophantHexComAdapter(BaseSiteAdapter):
## 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',a.text)
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+"))
@@ -16,6 +16,7 @@
#
import time
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
import re
@@ -43,7 +44,7 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
# normalized story URL.
self._setURL("http://"+self.getSiteDomain()\
self._setURL("https://"+self.getSiteDomain()\
+"/s/"+self.story.getMetadata('storyId')+"/1/")
# ffnet update emails have the latest chapter URL.
@@ -52,9 +53,11 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
# chapter list doesn't get the latest. So save and use the
# original URL given to pull chapter list & metadata.
self.origurl = url
if "http://m." in self.origurl:
if "https://m." in self.origurl:
## accept m(mobile)url, but use www.
self.origurl = self.origurl.replace("http://m.","http://www.")
self.origurl = self.origurl.replace("https://m.","https://www.")
self.opener.addheaders.append(('Referer',self.origurl))
@staticmethod
def getSiteDomain():
@@ -64,11 +67,19 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
def getAcceptDomains(cls):
return ['www.fanfiction.net','m.fanfiction.net']
@classmethod
def getSiteExampleURLs(self):
return "http://www.fanfiction.net/s/1234/1/ http://www.fanfiction.net/s/1234/12/ http://www.fanfiction.net/s/1234/1/Story_Title"
return "https://www.fanfiction.net/s/1234/1/ https://www.fanfiction.net/s/1234/12/ http://www.fanfiction.net/s/1234/1/Story_Title http://m.fanfiction.net/s/1234/1/"
def getSiteURLPattern(self):
return r"http://(www|m)?\.fanfiction\.net/s/\d+(/\d+)?(/|/[^/]+)?/?$"
return r"https?://(www|m)?\.fanfiction\.net/s/\d+(/\d+)?(/|/[^/]+)?/?$"
def _fetchUrl(self,url):
time.sleep(1.0) ## ffnet(and, I assume, fpcom) tends to fail
## more if hit too fast. This is in
## additional to what ever the
## slow_down_sleep_time setting is.
return BaseSiteAdapter._fetchUrl(self,url)
def extractChapterUrlsAndMetadata(self):
@@ -96,32 +107,35 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
if "not found. Please check to see you are not using an outdated url." in data:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! 'Chapter not found. Please check to see you are not using an outdated url.'" % url)
try:
# rather nasty way to check for a newer chapter. ffnet has a
# tendency to send out update notices in email before all
# their servers are showing the update on the first chapter.
if self.getConfig('check_next_chapter'):
try:
chapcount = len(soup.find('select', { 'name' : 'chapter' } ).findAll('option'))
# get chapter part of url.
## ffnet used to have a tendency to send out update
## notices in email before all their servers were
## showing the update on the first chapter. It
## generates another server request and doesn't seem
## to be needed lately, so now default it to off.
try:
chapcount = len(soup.find('select', { 'name' : 'chapter' } ).findAll('option'))
# get chapter part of url.
except:
chapcount = 1
chapter = url.split('/',)[5]
tryurl = "https://%s/s/%s/%d/"%(self.getSiteDomain(),
self.story.getMetadata('storyId'),
chapcount+1)
logger.debug('=Trying newer chapter: %s' % tryurl)
newdata = self._fetchUrl(tryurl)
if "not found. Please check to see you are not using an outdated url." \
not in newdata:
logger.debug('=======Found newer chapter: %s' % tryurl)
soup = bs.BeautifulSoup(newdata)
except:
chapcount = 1
chapter = url.split('/',)[5]
tryurl = "http://%s/s/%s/%d/"%(self.getSiteDomain(),
self.story.getMetadata('storyId'),
chapcount+1)
logger.debug('=Trying newer chapter: %s' % tryurl)
newdata = self._fetchUrl(tryurl)
if "not found. Please check to see you are not using an outdated url." \
not in newdata:
logger.debug('=======Found newer chapter: %s' % tryurl)
soup = bs.BeautifulSoup(newdata)
except:
pass
pass
# Find authorid and URL from... author url.
a = soup.find('a', href=re.compile(r"^/u/\d+"))
self.story.setMetadata('authorId',a['href'].split('/')[2])
self.story.setMetadata('authorUrl','http://'+self.host+a['href'])
self.story.setMetadata('authorUrl','https://'+self.host+a['href'])
self.story.setMetadata('author',a.string)
## Pull some additional data from html.
@@ -135,14 +149,27 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
categories = soup.find('div',{'id':'pre_story_links'}).findAll('a',{'class':'xcontrast_txt'})
#print("xcontrast_txt a:%s"%categories)
if len(categories) > 1:
# Strangely, the ones with *two* links are the
# non-crossover categories. Each is in a category itself
# of Book, Movie, etc.
self.story.addToList('category',stripHTML(categories[1]))
elif 'Crossover' in categories[0]['href']:
caturl = "http://%s%s"%(self.getSiteDomain(),categories[0]['href'])
caturl = "https://%s%s"%(self.getSiteDomain(),categories[0]['href'])
catsoup = bs.BeautifulSoup(self._fetchUrl(caturl))
for a in catsoup.findAll('a',href=re.compile(r"^/crossovers/")):
for a in catsoup.findAll('a',href=re.compile(r"^/crossovers/.+?/\d+/")):
self.story.addToList('category',stripHTML(a))
else:
# Fall back. I ran across a story with a Crossver
# category link to a broken page once.
# http://www.fanfiction.net/s/2622060/1/
# Naruto + Harry Potter Crossover
logger.info("Fall back category collection")
for c in stripHTML(categories[0]).replace(" Crossover","").split(' + '):
self.story.addToList('category',c)
a = soup.find('a', href='http://www.fictionratings.com/')
a = soup.find('a', href=re.compile(r'https?://www\.fictionratings\.com/'))
rating = a.string
if 'Fiction' in rating: # if rating has 'Fiction ', strip that out for consistency with past.
rating = rating[8:]
@@ -151,7 +178,7 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
# after Rating, the same bit of text containing id:123456 contains
# Complete--if completed.
gui_table1i = soup.find('table',{'cellpadding':'5'})
gui_table1i = soup.find('div',{'id':'content_wrapper_inner'})
self.story.setMetadata('title', stripHTML(gui_table1i.find('b'))) # title appears to be only(or at least first) bold tag in gui_table1i
@@ -185,15 +212,23 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
for g in genrelist:
#logger.debug("g:(%s)"%g)
if g.strip() not in ffnetgenres:
logger.info("g not in ffnetgenres")
#logger.info("g not in ffnetgenres")
goodgenres=False
if goodgenres:
self.story.extendList('genre',genrelist)
metalist=metalist[1:]
# Updated: <span data-xutime='1368059198'>5/8</span> - Published: <span data-xutime='1278984264'>7/12/2010</span>
# Published: <span data-xutime='1384358726'>8m ago</span>
dates = soup.findAll('span',{'data-xutime':re.compile(r'^\d+$')})
if len(dates) > 1 :
# updated get set to the same as published upstream if not found.
self.story.setMetadata('dateUpdated',datetime.fromtimestamp(float(dates[0]['data-xutime'])))
self.story.setMetadata('datePublished',datetime.fromtimestamp(float(dates[-1]['data-xutime'])))
donechars = False
while len(metalist) > 0:
if metalist[0].startswith('Chapters') or metalist[0].startswith('Status') or metalist[0].startswith('id:'):
if metalist[0].startswith('Chapters') or metalist[0].startswith('Status') or metalist[0].startswith('id:') or metalist[0].startswith('Updated:') or metalist[0].startswith('Published:'):
pass
elif metalist[0].startswith('Reviews'):
self.story.setMetadata('reviews',metalist[0].split(':')[1].strip())
@@ -201,21 +236,21 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
self.story.setMetadata('favs',metalist[0].split(':')[1].strip())
elif metalist[0].startswith('Follows:'):
self.story.setMetadata('follows',metalist[0].split(':')[1].strip())
elif metalist[0].startswith('Updated'):
self.story.setMetadata('dateUpdated',makeDate(metalist[0].split(':')[1].strip(), '%m-%d-%y'))
elif metalist[0].startswith('Published'):
self.story.setMetadata('datePublished',makeDate(metalist[0].split(':')[1].strip(), '%m-%d-%y'))
elif metalist[0].startswith('Words'):
self.story.setMetadata('numWords',metalist[0].split(':')[1].strip())
elif not donechars:
self.story.extendList('characters',metalist[0].split('&'))
# with 'pairing' support, pairings are bracketed w/o comma after
# [Caspian X, Lucy Pevensie] Edmund Pevensie, Peter Pevensie
self.story.extendList('characters',metalist[0].replace('[','').replace(']',',').split(','))
l = metalist[0]
while '[' in l:
self.story.addToList('ships',l[l.index('[')+1:l.index(']')].replace(', ','/'))
l = l[l.index(']')+1:]
donechars = True
metalist=metalist[1:]
# next might be characters, otherwise Reviews, Updated, Published, Words
# if not ( metalist[0].startswith('Reviews') or metalist[0].startswith('Updated') or metalist[0].startswith('Published') or metalist[0].startswith('Words') or metalist[0].startswith('Chapters') ):
# self.story.extendList('characters',metalist[0].split('&'))
if 'Status: Complete' in metatext:
self.story.setMetadata('status', 'Completed')
else:
@@ -239,9 +274,9 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
else:
allOptions = select.findAll('option')
for o in allOptions:
url = u'http://%s/s/%s/%s/' % ( self.getSiteDomain(),
self.story.getMetadata('storyId'),
o['value'])
url = u'https://%s/s/%s/%s/' % ( self.getSiteDomain(),
self.story.getMetadata('storyId'),
o['value'])
# just in case there's tags, like <i> in chapter titles.
title = u"%s" % o
title = re.sub(r'<[^>]+>','',title)
@@ -252,19 +287,26 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
return
def getChapterText(self, url):
logger.debug('Getting chapter text from: %s' % url)
time.sleep(0.5) ## ffnet(and, I assume, fpcom) tends to fail
time.sleep(4.0) ## ffnet(and, I assume, fpcom) tends to fail
## more if hit too fast. This is in
## additional to what ever the
## slow_down_sleep_time setting is.
logger.debug('Getting chapter text from: %s' % url)
data = self._fetchUrl(url)
if "Please email this error message in full to <a href='mailto:support@fanfiction.com'>support@fanfiction.com</a>" in data:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! FanFiction.net Site Error!" % url)
# some ancient stories have body tags inside them that cause
# soup parsing to discard the content. For story text we
# don't care about anything before "<div class='storytextp"
# (there's a space after storytextp, so no close quote(')) and
# this kills any body tags.
data = data[data.index("<div class='storytextp"):]
divstr = "<div role='main'"
if divstr not in data:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
else:
data = data[data.index(divstr):]
data.replace("<body","<notbody").replace("<BODY","<NOTBODY")
soup = bs.BeautifulSoup(data)
@@ -50,7 +50,7 @@ class FanFiktionDeAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/s/'+self.story.getMetadata('storyId') + '/1')
@@ -67,11 +67,12 @@ class FanFiktionDeAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'www.fanfiktion.de'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/s/46ccbef30000616306614050"
return "http://"+self.getSiteDomain()+"/s/46ccbef30000616306614050 http://"+self.getSiteDomain()+"/s/46ccbef30000616306614050/1 http://"+self.getSiteDomain()+"/s/46ccbef30000616306614050/1/story-name"
def getSiteURLPattern(self):
return re.escape("http://"+self.getSiteDomain()+"/s/")+r"\w+(/\d+)?$"
return re.escape("http://"+self.getSiteDomain()+"/s/")+r"\w+(/\d+)?"
## Login seems to be reasonably standard across eFiction sites.
def needToLoginCheck(self, data):
@@ -137,15 +138,15 @@ class FanFiktionDeAdapter(BaseSiteAdapter):
# 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',a.string)
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.
head = soup.find('div', {'class' : 'story-metadata-left-top'})
a = head.find('a')
self.story.setMetadata('authorId',a['href'].split('/')[2])
self.story.setMetadata('authorUrl','http://'+self.host+'/'+a['href'])
self.story.setMetadata('author',a.string)
self.story.setMetadata('author',stripHTML(a))
# Find the chapters:
for chapter in soup.find('select').findAll('option'):
@@ -172,14 +173,14 @@ class FanFiktionDeAdapter(BaseSiteAdapter):
tr=asoup.findAll('tr')
for i in range(1,len(tr)):
a = tr[i].find('a')
if a['href'] == '/s/'+self.story.getMetadata('storyId'):
if '/s/'+self.story.getMetadata('storyId')+'/1/' in a['href']:
break
self.setDescription(url,a['onmouseover'].split("', '")[1])
td = tr[i].findAll('td')
self.story.addToList('category',stripHTML(td[1]))
self.story.setMetadata('rating', td[4].string)
self.story.setMetadata('numWords', td[5].string)
self.story.setMetadata('rating', stripHTML(td[4]))
self.story.setMetadata('numWords', stripHTML(td[5]))
# grab the text for an individual chapter.
@@ -52,7 +52,7 @@ class FicBookNetAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/readfic/'+self.story.getMetadata('storyId'))
@@ -69,8 +69,9 @@ class FicBookNetAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'www.ficbook.net'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/readfic/12345"
return "http://"+self.getSiteDomain()+"/readfic/12345 http://"+self.getSiteDomain()+"/readfic/93626/246417#part_content"
def getSiteURLPattern(self):
return re.escape("http://"+self.getSiteDomain()+"/readfic/")+r"\d+"
@@ -45,7 +45,7 @@ class FictionAlleyOrgSiteAdapter(BaseSiteAdapter):
if m:
self.story.setMetadata('authorId',m.group('auth'))
self.story.setMetadata('storyId',m.group('id'))
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL(url)
else:
@@ -57,6 +57,7 @@ class FictionAlleyOrgSiteAdapter(BaseSiteAdapter):
def getSiteDomain():
return 'www.fictionalley.org'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/authors/drt/DA.html http://"+self.getSiteDomain()+"/authors/drt/JOTP01a.html"
@@ -110,7 +111,7 @@ class FictionAlleyOrgSiteAdapter(BaseSiteAdapter):
url=self.url
logger.debug("Normalizing to URL: "+url)
## title's right there...
self.story.setMetadata('title',storya.string)
self.story.setMetadata('title',stripHTML(storya))
data = self._fetchUrl(url)
soup = bs.BeautifulSoup(data)
chapterlinklist = soup.findAll('a',{'class':'chapterlink'})
@@ -120,7 +121,7 @@ class FictionAlleyOrgSiteAdapter(BaseSiteAdapter):
## same way.
chapsoup = bs.BeautifulSoup(chapterdata)
storya = chapsoup.find('div',{'class':'breadcrumbs'}).findAll('a')[1]
self.story.setMetadata('title',storya.string)
self.story.setMetadata('title',stripHTML(storya))
del chapsoup
del chapterdata
@@ -0,0 +1,192 @@
# -*- coding: utf-8 -*-
# Copyright 2013 Fanficdownloader team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import time
import logging
logger = logging.getLogger(__name__)
import re
import urllib2
import time
import json
from .. import BeautifulSoup as bs
#from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, makeDate
class FictionPadSiteAdapter(BaseSiteAdapter):
def __init__(self, config, url):
BaseSiteAdapter.__init__(self, config, url)
self.story.setMetadata('siteabbrev','fpad')
self.dateformat = "%Y-%m-%dT%H:%M:%SZ"
self.is_adult=False
self.username = None
self.password = None
# get storyId from url--url validation guarantees query correct
m = re.match(self.getSiteURLPattern(),url)
if m:
self.story.setMetadata('storyId',m.group('id'))
# normalized story URL.
self._setURL("https://"+self.getSiteDomain()
+"/author/"+m.group('author')
+"/stories/"+self.story.getMetadata('storyId'))
else:
raise exceptions.InvalidStoryURL(url,
self.getSiteDomain(),
self.getSiteExampleURLs())
@staticmethod
def getSiteDomain():
return 'fictionpad.com'
@classmethod
def getSiteExampleURLs(self):
return "https://fictionpad.com/author/Author/stories/1234/Some-Title"
def getSiteURLPattern(self):
# http://fictionpad.com/author/Serdd/stories/4275
return r"http(s)?://(www\.)?fictionpad\.com/author/(?P<author>[^/]+)/stories/(?P<id>\d+)"
# <form method="post" action="/signin">
# <input name="authenticity_token" type="hidden" value="u+cfdXh46dRnwVnSlmE2B2BFmHgu760paqgBG6KQeos=" />
# <input type="hidden" name="remember" value="1">
# <strong class="help-start text-center">or with FictionPad</strong>
# <label class="control-label hidden-placeholder">Pseudonym or Email Address</label>
# <input name="login" class="input-block-level" type="text" placeholder="Pseudonym or Email Address" maxlength="50" required autofocus>
# <label class="control-label hidden-placeholder">Password</label>
# <input name="password" class="input-block-level" type="password" placeholder="Password" minlength="6" required>
# <button type="submit" class="btn btn-primary btn-block">Sign In</button>
# <p class="help-end">
# <a href="/passwordreset">Forgot your password?</a>
# </p>
# </form>
def performLogin(self):
params = {}
if self.password:
params['login'] = self.username
params['password'] = self.password
else:
params['login'] = self.getConfig("username")
params['password'] = self.getConfig("password")
params['remember'] = '1'
loginUrl = 'http://' + self.getSiteDomain() + '/signin'
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
params['login']))
## need to pull empty login page first to get authenticity_token
soup = bs.BeautifulSoup(self._fetchUrl(loginUrl))
params['authenticity_token']=soup.find('input', {'name':'authenticity_token'})['value']
data = self._postUrl(loginUrl, params)
if "Invalid email/pseudonym and password combination." in data:
logger.info("Failed to login to URL %s as %s" % (loginUrl,
params['login']))
raise exceptions.FailedToLogin(loginUrl,params['login'])
def extractChapterUrlsAndMetadata(self):
# fetch the chapter. From that we will get almost all the
# metadata and chapter list
url=self.url
logger.debug("URL: "+url)
try:
data = self._fetchUrl(url)
if "This is a mature story. Please sign in to read it." in data:
self.performLogin()
data = self._fetchUrl(url)
find = "wordyarn.config.page = "
data = data[data.index(find)+len(find):]
data = data[:data.index("</script>")]
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]
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.
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"<em>This chapter has no text.</em>"
else:
data = self._fetchUrl(url)
soup = bs.BeautifulSoup(u"<div id='story'>"+data+u"</div>")
return self.utf8FromSoup(url,soup)
def getClass():
return FictionPadSiteAdapter
@@ -39,8 +39,9 @@ class FictionPressComSiteAdapter(FanFictionNetSiteAdapter):
def getAcceptDomains(cls):
return ['www.fictionpress.com','m.fictionpress.com']
@classmethod
def getSiteExampleURLs(self):
return "http://www.fictionpress.com/s/1234/1/ http://www.fictionpress.com/s/1234/12/ http://www.fictionpress.com/s/1234/1/Story_Title"
return "http://www.fictionpress.com/s/1234/1/ http://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"http://(www|m)?\.fictionpress\.com/s/\d+(/\d+)?(/|/[a-zA-Z0-9_-]+)?/?$"
@@ -45,8 +45,9 @@ class FicwadComSiteAdapter(BaseSiteAdapter):
def getSiteDomain():
return 'ficwad.com'
@classmethod
def getSiteExampleURLs(self):
return "http://ficwad.com/story/137169"
return "http://ficwad.com/story/1234"
def getSiteURLPattern(self):
return re.escape(r"http://"+self.getSiteDomain())+"/story/\d+?$"
@@ -84,7 +85,11 @@ class FicwadComSiteAdapter(BaseSiteAdapter):
# use BeautifulSoup HTML parser to make everything easier to find.
try:
soup = bs.BeautifulSoup(self._fetchUrl(url))
data = self._fetchUrl(url)
# non-existent/removed story urls get thrown to the front page.
if "<h2>Welcome to FicWad</h2>" in data:
raise exceptions.StoryDoesNotExist(self.url)
soup = bs.BeautifulSoup(data)
except urllib2.HTTPError, e:
if e.code == 404:
raise exceptions.StoryDoesNotExist(self.url)
@@ -115,7 +120,7 @@ class FicwadComSiteAdapter(BaseSiteAdapter):
# title - first h4 tag will be title.
titleh4 = soup.find('h4')
self.story.setMetadata('title', titleh4.a.string)
self.story.setMetadata('title', stripHTML(titleh4.a))
# Find authorid and URL from... author url.
a = soup.find('a', href=re.compile(r"^/author/\d+"))
@@ -21,7 +21,6 @@ logger = logging.getLogger(__name__)
import re
import urllib2
import cookielib as cl
from datetime import datetime
import json
from .. import BeautifulSoup as bs
@@ -42,6 +41,10 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
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'
@@ -51,6 +54,7 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
# 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(self):
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"
@@ -92,10 +96,12 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
if "Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result resource" in data:
raise exceptions.StoryDoesNotExist(self.url)
if "/images/missing_story.png" in data:
raise exceptions.StoryDoesNotExist(self.url)
# Can cause problems if a missing story is referenced in a comment.
# Shouldn't be needed anyway.
# if "/images/missing_story.png" in data:
# raise exceptions.StoryDoesNotExist(self.url)
if "This story has been marked as having adult content." in data:
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:
@@ -116,7 +122,7 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile(r'^/story/'+self.story.getMetadata('storyId')))
self.story.setMetadata('title',a.string)
self.story.setMetadata('title',stripHTML(a))
# self.story.setMetadata("title", storyMetadata["title"])
# if not storyMetadata["title"]:
@@ -184,14 +190,42 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
hrstr="<hr />"
descdivstr = '<div class="description">'+descdivstr[descdivstr.index(hrstr)+len(hrstr):]
self.setDescription(self.url,descdivstr)
# Can't trust dates from API anymore I'm told.
# Dates are in Unix time
# Take the publish date from the first chapter posted
rawDatePublished = storyMetadata["chapters"][0]["date_modified"]
self.story.setMetadata("datePublished", datetime.fromtimestamp(rawDatePublished))
rawDateUpdated = storyMetadata["date_modified"]
self.story.setMetadata("dateUpdated", datetime.fromtimestamp(rawDateUpdated))
# rawDatePublished = storyMetadata["chapters"][0]["date_modified"]
# self.story.setMetadata("datePublished", datetime.fromtimestamp(rawDatePublished))
# rawDateUpdated = storyMetadata["date_modified"]
# self.story.setMetadata("dateUpdated", datetime.fromtimestamp(rawDateUpdated))
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(soup.findAll('span', {'class':'date'})):
date=re.sub(r"(\d+)(st|nd|rd|th)",r"\1",chapterDate.contents[1].strip())
chapterDate = makeDate(date,self.dateformat)
if oldestChapter == None or chapterDate < oldestChapter:
oldestChapter = chapterDate
if newestChapter == None or chapterDate > newestChapter:
newestChapter = chapterDate
self.newestChapterNum = index
self.story.setMetadata("dateUpdated", newestChapter)
pubdatetag = soup.find('span', {'class':'date_approved'})
if pubdatetag is None:
self.story.setMetadata("datePublished", oldestChapter)
else:
pubdateraw = pubdatetag('span')[1].text
datestripped=re.sub(r"(\d+)(st|nd|rd|th)",r"\1",pubdateraw.strip())
pubDate = makeDate(datestripped,self.dateformat)
self.story.setMetadata("datePublished", pubDate)
chars = soup.find("div", {"class":"inner_data"})
# fimfic stopped putting the char name on or around the char
# icon now for some reason. Pull it from the image name with
@@ -214,10 +248,46 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
if not isinstance(value,basestring):
value = unicode(value)
self.story.setMetadata(metakey, value)
## Groups and sequels code from FaceDeer
allGroupLists = soup.findAll('ul', {'id':'story_group_list'})
for groupList in allGroupLists:
for groupName in groupList.findAll('a', {'href':re.compile('^/group/')}):
self.story.addToList("groupsUrl", 'http://'+self.host+groupName["href"])
self.story.addToList("groups",stripHTML(groupName).replace(',', ';'))
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 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 getChapterText(self, url):
logger.debug('Getting chapter text from: %s' % url)
soup = bs.BeautifulSoup(self._fetchUrl(url),selfClosingTags=('br','hr')).find('div', {'class' : 'chapter_content'})
if soup == None:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2012 Fanficdownloader team
# 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.
@@ -37,11 +37,6 @@ class FineStoriesComAdapter(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
@@ -50,7 +45,6 @@ class FineStoriesComAdapter(BaseSiteAdapter):
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])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/s/storyInfo.php?id='+self.story.getMetadata('storyId'))
@@ -67,8 +61,9 @@ class FineStoriesComAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'finestories.com'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/s/10537 http://"+self.getSiteDomain()+"/s/10537:4010 http://"+self.getSiteDomain()+"/library/storyInfo.php?id=10537"
return "http://"+self.getSiteDomain()+"/s/1234 http://"+self.getSiteDomain()+"/s/1234:4010 http://"+self.getSiteDomain()+"/library/storyInfo.php?id=1234"
def getSiteURLPattern(self):
return re.escape("http://"+self.getSiteDomain())+r"/(s|library)?/(storyInfo.php\?id=)?\d+(:\d+)?(;\d+)?$"
@@ -92,7 +87,7 @@ class FineStoriesComAdapter(BaseSiteAdapter):
params['theusername'] = self.getConfig("username")
params['thepassword'] = self.getConfig("password")
params['rememberMe'] = '1'
params['page'] = 'http://finestories.com/'
params['page'] = 'http://'+self.getSiteDomain()+'/'
params['submit'] = 'Login'
loginUrl = 'http://' + self.getSiteDomain() + '/login.php'
@@ -141,7 +136,7 @@ class FineStoriesComAdapter(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile(r'/s/'+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.text)
self.story.setMetadata('title',stripHTML(a))
# Find authorid and URL from... author url.
a = soup.find('a', href=re.compile(r"/a/\w+"))
@@ -261,8 +256,6 @@ class FineStoriesComAdapter(BaseSiteAdapter):
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()
@@ -48,7 +48,7 @@ class GrangerEnchantedCom(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
self.section=self.parsedUrl.path.split('/',)[1]
# normalized story URL.
@@ -74,6 +74,7 @@ class GrangerEnchantedCom(BaseSiteAdapter):
def getAcceptDomains(cls):
return ['grangerenchanted.com','malfoymanor.grangerenchanted.com']
@classmethod
def getSiteExampleURLs(self):
return "http://grangerenchanted.com/enchant/viewstory.php?sid=1234 http://malfoymanor.grangerenchanted.com/themanor/viewstory.php?sid=1234"
@@ -181,7 +182,7 @@ class GrangerEnchantedCom(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
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+"))
@@ -227,6 +228,9 @@ class GrangerEnchantedCom(BaseSiteAdapter):
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:
@@ -278,7 +282,14 @@ class GrangerEnchantedCom(BaseSiteAdapter):
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
@@ -42,7 +42,7 @@ class HarryPotterFanFictionComSiteAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only psid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?psid='+self.story.getMetadata('storyId'))
@@ -56,8 +56,9 @@ class HarryPotterFanFictionComSiteAdapter(BaseSiteAdapter):
def getAcceptDomains(cls):
return ['www.harrypotterfanfiction.com','harrypotterfanfiction.com']
@classmethod
def getSiteExampleURLs(self):
return "http://www.harrypotterfanfiction.com/viewstory.php?psid=1234 http://harrypotterfanfiction.com/viewstory.php?psid=5678"
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+$"
@@ -91,7 +92,7 @@ class HarryPotterFanFictionComSiteAdapter(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile(r'\?psid='+self.story.getMetadata('storyId')))
self.story.setMetadata('title',a.string)
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)
@@ -130,55 +131,55 @@ class HarryPotterFanFictionComSiteAdapter(BaseSiteAdapter):
## 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 = center.find('table',{'bgcolor':None})
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
#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".*?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".*?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"))
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. <shrug>
m = re.match(r".*?Last Updated: ([0-9\.]+).*?",metastr)
if m:
self.story.setMetadata('dateUpdated',makeDate(m.group(1), "%Y.%m.%d"))
# Updated can have more than one space after it. <shrug>
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):
@@ -48,7 +48,7 @@ class HennethAnnunNetAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/stories/chapter.cfm?stid='+self.story.getMetadata('storyId'))
@@ -65,6 +65,7 @@ class HennethAnnunNetAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'www.henneth-annun.net'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/stories/chapter.cfm?stid=1234"
@@ -100,7 +101,7 @@ class HennethAnnunNetAdapter(BaseSiteAdapter):
## Title
a = soup.find('h2', {'id':'page_heading'})
self.story.setMetadata('title',a.string)
self.story.setMetadata('title',stripHTML(a))
# Find the chapters: chapter_view.cfm?stid=6663&amp;spordinal=1"
for chapter in soup.findAll('a', href=re.compile(r'chapter_view.cfm\?stid='+self.story.getMetadata('storyId')+"&spordinal=\d+$")):
@@ -48,7 +48,7 @@ class HLFictionNetAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -65,6 +65,7 @@ class HLFictionNetAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'hlfiction.net'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -105,7 +106,7 @@ class HLFictionNetAdapter(BaseSiteAdapter):
self.story.setMetadata('author',aut.string)
aut.extract()
self.story.setMetadata('title',a.string[:(len(a.string)-3)])
self.story.setMetadata('title',stripHTML(a)[:(len(a.string)-3)])
# Find the chapters:
chapters=soup.find('select')
@@ -52,7 +52,7 @@ class HPFandomNetAdapterAdapter(BaseSiteAdapter): # XXX
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
# XXX Most sites don't have the /eff part. Replace all to remove it usually.
@@ -70,6 +70,7 @@ class HPFandomNetAdapterAdapter(BaseSiteAdapter): # XXX
# The site domain. Does have www here, if it uses it.
return 'www.hpfandom.net' # XXX
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/eff/viewstory.php?sid=1234"
@@ -48,7 +48,7 @@ class HPFanficArchiveComAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/stories/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -65,6 +65,7 @@ class HPFanficArchiveComAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'www.hpfanficarchive.com'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/stories/viewstory.php?sid=1234"
@@ -99,7 +100,7 @@ class HPFanficArchiveComAdapter(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
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+"))
@@ -127,23 +128,22 @@ class HPFanficArchiveComAdapter(BaseSiteAdapter):
# <span class="label">Rated:</span> NC-17<br /> etc
labels = soup.findAll('span',{'class':'label'})
for labelspan in labels:
value = labelspan.nextSibling
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:
## 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))
self.setDescription(url,value)
if 'Rated' in label:
self.story.setMetadata('rating', value)
self.story.setMetadata('rating', stripHTML(value))
if 'Word count' in label:
self.story.setMetadata('numWords', value)
self.story.setMetadata('numWords', stripHTML(value))
if 'Categories' in label:
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
@@ -166,7 +166,7 @@ class HPFanficArchiveComAdapter(BaseSiteAdapter):
self.story.addToList('warnings',warning.string)
if 'Completed' in label:
if 'Yes' in value:
if 'Yes' in stripHTML(value):
self.story.setMetadata('status', 'Completed')
else:
self.story.setMetadata('status', 'In-Progress')
@@ -48,7 +48,7 @@ class IkEternalNetAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -65,6 +65,7 @@ class IkEternalNetAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'www.ik-eternal.net'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -182,7 +183,7 @@ class IkEternalNetAdapter(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
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+"))
@@ -48,7 +48,7 @@ class ImagineEFicComAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -65,6 +65,7 @@ class ImagineEFicComAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'imagine.e-fic.com'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -169,7 +170,7 @@ class ImagineEFicComAdapter(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
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+"))
+44 -23
View File
@@ -46,12 +46,12 @@ class InDeathNetAdapter(BaseSiteAdapter):
self.password = ""
self.is_adult=False
# get storyId from url--url validation guarantees query correct
# get storyId from url--url validation guarantees query correct
m = re.match(self.getSiteURLPattern(),url)
if m:
self.story.setMetadata('storyId',m.group('id'))
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://www.' + self.getSiteDomain() + '/blog/archive/'+self.story.getMetadata('storyId')+'-'+m.group('name')+'/')
else:
@@ -72,6 +72,7 @@ class InDeathNetAdapter(BaseSiteAdapter):
return 'indeath.net'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/blog/archive/123-story-in-death/"
@@ -86,6 +87,34 @@ class InDeathNetAdapter(BaseSiteAdapter):
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):
@@ -108,21 +137,14 @@ class InDeathNetAdapter(BaseSiteAdapter):
## Title
h = soup.find('a', id="blog_title")
t = h.find('span')
self.story.setMetadata('title',t.contents[0].string.strip())
self.story.setMetadata('title',stripHTML(t.contents[0]).strip())
s = t.find('div')
if s != None:
self.setDescription(url,s)
# Find authorid and URL from first link in Recent Entries (don't yet reference 'recent entries' - let's see if that is required)
a = soup.find('a', href=re.compile(r"http://www.indeath.net/user/\d+\-[a-z0-9]+/$")) #http://www.indeath.net/user/9083-cyrex/
m = re.search('http://www.indeath.net/user/(?P<id>\d+)\-(?P<name>[a-z0-9]*)/$',a['href'])
self.story.setMetadata('authorId',m.group('id'))
self.story.setMetadata('authorUrl',a['href'])
self.story.setMetadata('author',m.group('name'))
# 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+)\-([^/]*)/$"))
@@ -151,21 +173,20 @@ class InDeathNetAdapter(BaseSiteAdapter):
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<newtitle>.*)$",ct)
if tnew:
chaptertitle = tnew.group('newtitle')
else:
chaptertitle = ct
ct = stripHTML(chapter)
tnew = re.match("(?i)"+self.story.getMetadata('title')+r" - (?P<newtitle>.*)$",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('<div class="story"></div>')
data = self._fetchUrl(url)
soup = bs.BeautifulSoup(data,selfClosingTags=('br','hr','span','center'))
@@ -52,7 +52,7 @@ class KSArchiveComAdapter(BaseSiteAdapter): # XXX
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
# XXX Most sites don't have the /fanfic part. Replace all to remove it usually.
@@ -74,6 +74,7 @@ class KSArchiveComAdapter(BaseSiteAdapter): # XXX
# The site domain. Does have www here, if it uses it.
return 'ksarchive.com' # XXX
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -49,7 +49,7 @@ class LibraryOfMoriaComAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/a/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -66,6 +66,7 @@ class LibraryOfMoriaComAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'www.libraryofmoria.com'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/a/viewstory.php?sid=1234"
@@ -128,12 +129,12 @@ class LibraryOfMoriaComAdapter(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
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('authorUrl','http://'+self.host+'/a/'+a['href'])
self.story.setMetadata('author',a.string)
# Find the chapters:
@@ -0,0 +1,237 @@
# -*- 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')
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
self.origurl = url
if "http://www.i." in self.origurl:
## accept m(mobile)url, but use www.
self.origurl = self.origurl.replace("http://www.i.","http://www.")
# normalized story URL.
self._setURL("http://"+self.getSiteDomain()\
+"/s/"+self.story.getMetadata('storyId'))
# 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 'www.literotica.com'
@classmethod
def getAcceptDomains(cls):
return ['www.literotica.com', 'www.i.literotica.com']
@classmethod
def getSiteExampleURLs(self):
#return "http://www.literotica.com/s/story-title http://www.literotica.com/stories/showstory.php?id=1234 http://www.i.literotica.com/stories/showstory.php?id=1234"
return "http://www.literotica.com/s/story-title"
def getSiteURLPattern(self):
return r"http://www(\.i)?\.literotica\.com/s/([a-zA-Z0-9_-]+)"
def extractChapterUrlsAndMetadata(self):
if not (self.is_adult or self.getConfig("is_adult")):
raise exceptions.AdultCheckRequired(self.url)
url1 = self.origurl
logger.debug("first page URL: "+url1)
try:
data1 = self._fetchUrl(url1)
soup1 = bs.BeautifulSoup(data1)
except urllib2.HTTPError, e:
if e.code == 404:
raise exceptions.StoryDoesNotExist(url1)
else:
raise e
#strip comments from soup
[comment.extract() for comment in soup1.findAll(text=lambda text:isinstance(text, bs.Comment))]
# author
a = soup1.find("span", "b-story-user-y")
self.story.setMetadata('authorId', urlparse.parse_qs(a.a['href'].split('?')[1])['uid'])
self.story.setMetadata('authorUrl', a.a['href'])
self.story.setMetadata('author', a.text)
# get the author page
try:
dataAuth = self._fetchUrl(a.a['href'])
soupAuth = bs.BeautifulSoup(dataAuth)
except urllib2.HTTPError, e:
if e.code == 404:
raise exceptions.StoryDoesNotExist(a.a['href'])
else:
raise e
storyLink = soupAuth.find('a', href=url1)
if storyLink is not None:
# pull the published date from the author page
# default values from single link. Updated below if multiple chapter.
date = storyLink.parent.parent.findAll('td')[-1].text
self.story.setMetadata('datePublished', makeDate(date, self.dateformat))
self.story.setMetadata('dateUpdated',makeDate(date, self.dateformat))
# find num of pages
# find a "3 Pages:" string on the page and parse it
pgs = soup1.find("span", "b-pager-caption-t r-d45").string.split(' ')[0]
# If there are multiple pages, find and request the last page
if "1" != pgs:
logger.debug("last page number: "+pgs)
try:
data2 = self._fetchUrl(url1, {'page': pgs})
soup2 = bs.BeautifulSoup(data2)
[comment.extract() for comment in soup2.findAll(text=lambda text:isinstance(text, bs.Comment))]
except urllib2.HTTPError, e:
if e.code == 404:
# TODO: Probably should reformat this
raise exceptions.StoryDoesNotExist(url1, {'page': pgs})
else:
raise e
else:
#If we're already on the last page, copy the soup
soup2 = soup1
# parse out the list of chapters
chaps = soup2.find('div', id='b-series')
if chaps: # may be one post only
#self.chapterUrls = [(ch.a.text, ch.a['href']) for ch in chaps.findAll('li')]
# if there are chapters, lets pull them and title from the
# author page because *this* chapter is omitted from the
# list on the last page.
row = storyLink.parent.parent.previousSibling
while row['class'] != 'ser-ttl':
row = row.previousSibling
seriesTitle = stripHTML(row)
if seriesTitle:
# this regex is deliberately greedy. We want to get the biggest match before a ':'
self.story.setMetadata('title', re.match('(.*):[^:]*$', seriesTitle).group(1))
else:
self.story.setMetadata('title', soup1.h1.string)
# now chapter list. Assumed oldest to newest.
self.chapterUrls = []
row = row.nextSibling
self.story.setMetadata('datePublished',makeDate(stripHTML(row.find('td',{'class':'dt'})), self.dateformat))
while row['class'] == 'sl':
# pages include full URLs.
self.chapterUrls.append((row.a.string,row.a['href']))
if not row.nextSibling:
break
row = row.nextSibling
row = row.previousSibling
self.story.setMetadata('dateUpdated',makeDate(stripHTML(row.find('td',{'class':'dt'})), self.dateformat))
else: # if one post only
self.chapterUrls = [(soup1.h1.string, url1)]
self.story.setMetadata('title', soup1.h1.string)
# normalize on first chapter URL.
self._setURL(self.chapterUrls[0][1])
# reset storyId to first chapter.
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
self.story.setMetadata('numChapters', len(self.chapterUrls))
self.story.setMetadata('category', soup1.find('div', 'b-breadcrumbs').findAll('a')[1].string)
self.story.setMetadata('description', soup1.find('meta', {'name': 'description'})['content'])
return
def getChapterText(self, url):
logger.debug('Getting chapter text from: %s' % url)
time.sleep(0.5)
data1 = self._fetchUrl(url)
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').p
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})
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').p
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
@@ -48,7 +48,7 @@ class LumosSycophantHexComAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
@@ -66,6 +66,7 @@ class LumosSycophantHexComAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'lumos.sycophanthex.com'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -115,7 +116,7 @@ class LumosSycophantHexComAdapter(BaseSiteAdapter):
## 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',a.text)
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+"))
@@ -43,7 +43,7 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
m = re.match(self.getSiteURLPattern(),url)
if m:
self.story.setMetadata('storyId',m.group('id'))
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/fanfic/view_st.php/'+self.story.getMetadata('storyId'))
else:
@@ -55,6 +55,7 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
def getSiteDomain():
return 'www.mediaminer.org'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/fanfic/view_st.php/123456 http://"+self.getSiteDomain()+"/fanfic/view_ch.php/1234123/123444#fic_c"
@@ -106,10 +107,10 @@ class MediaMinerOrgSiteAdapter(BaseSiteAdapter):
for font in title.findAll('font'):
font.extract() # removes 'font' tags from inside the td.
if title.has_key('colspan'):
titlet = title.text
titlet = stripHTML(title)
else:
## No colspan, it's part chapter title--even if it's a one-shot.
titlet = ':'.join(title.text.split(':')[:-1]) # strip trailing 'Chapter X' or chapter title
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
@@ -48,7 +48,7 @@ class MerlinFicDtwinsCoUk(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -65,6 +65,7 @@ class MerlinFicDtwinsCoUk(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'merlinfic.dtwins.co.uk'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -169,7 +170,7 @@ class MerlinFicDtwinsCoUk(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
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+"))
@@ -52,7 +52,7 @@ class MidnightwhispersCaAdapter(BaseSiteAdapter): # XXX
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
# XXX Most sites don't have the /fanfic part. Replace all to remove it usually.
@@ -70,6 +70,7 @@ class MidnightwhispersCaAdapter(BaseSiteAdapter): # XXX
# The site domain. Does have www here, if it uses it.
return 'www.midnightwhispers.ca' # XXX
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -52,7 +52,7 @@ class MuggleNetComAdapter(BaseSiteAdapter): # XXX
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -66,14 +66,19 @@ class MuggleNetComAdapter(BaseSiteAdapter): # XXX
@staticmethod # must be @staticmethod, don't remove it.
def getSiteDomain():
# The site domain. Does have www here, if it uses it.
return 'fanfiction.mugglenet.com' # XXX
# The site domain.
return 'fanfiction.mugglenet.com'
@classmethod
def getAcceptDomains(cls):
return ['fanfiction.mugglenet.com','fanfic.mugglenet.com']
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
def getSiteURLPattern(self):
return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$"
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):
@@ -188,7 +193,7 @@ class MuggleNetComAdapter(BaseSiteAdapter): # XXX
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
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+"))
@@ -48,7 +48,7 @@ class NationalLibraryNetAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only storyid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?storyid='+self.story.getMetadata('storyId'))
@@ -68,8 +68,10 @@ class NationalLibraryNetAdapter(BaseSiteAdapter):
def getAcceptDomains(cls):
return ['www.national-library.net','national-library.net']
@classmethod
def getSiteExampleURLs(self):
return "ONLY the stories archived on or after June 17, 2006 and that are hosted on the website: http://"+self.getSiteDomain()+"/viewstory.php?storyid=1234"
# ONLY the stories archived on or after June 17, 2006 and that are hosted on the website:
return "http://"+self.getSiteDomain()+"/viewstory.php?storyid=1234"
def getSiteURLPattern(self):
return re.escape("http://")+"(www\.)?"+re.escape(self.getSiteDomain()+"/viewstory.php?storyid=")+r"\d+$"
@@ -102,7 +104,7 @@ class NationalLibraryNetAdapter(BaseSiteAdapter):
## Title
a = soup.find('h1')
self.story.setMetadata('title',a.string)
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+"))
@@ -48,7 +48,7 @@ class NCISFicComAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only storyid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?storyid='+self.story.getMetadata('storyId'))
@@ -68,6 +68,7 @@ class NCISFicComAdapter(BaseSiteAdapter):
def getAcceptDomains(cls):
return ['www.ncisfic.com','ncisfic.com']
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?storyid=1234"
@@ -102,7 +103,7 @@ class NCISFicComAdapter(BaseSiteAdapter):
## Title
a = soup.find('h1')
self.story.setMetadata('title',a.string)
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+"))
@@ -37,11 +37,6 @@ class NCISFictionNetAdapter(BaseSiteAdapter):
def __init__(self, config, url):
BaseSiteAdapter.__init__(self, config, url)
# ncisfiction.net blocks IPs the default user-agent. However,
# when asked, they said it was just general anti-spam, not
# targeted at us. That lets me do this in good conscience:
self.opener.addheaders = [('User-agent', 'FFDL/1.6')]
self.decode = ["iso-8859-1",
"Windows-1252"] # 1252 is a superset of iso-8859-1.
# Most sites that claim to be
@@ -53,7 +48,7 @@ class NCISFictionNetAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL("http://"+self.getSiteDomain()\
@@ -79,6 +74,7 @@ class NCISFictionNetAdapter(BaseSiteAdapter):
def getAcceptDomains(cls):
return ['www.ncisfiction.net','www.ncisfiction.com']
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/story.php?stid=01234 http://"+self.getSiteDomain()+"/chapters.php?stid=1234"
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2012 Fanficdownloader team
# 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.
@@ -28,11 +28,9 @@ from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, makeDate
def getClass():
return PrisonBreakFicNetAdapter
return NetRaptorOrgAdapter
# Class name has to be unique. Our convention is camel case the
# sitename with Adapter at the end. www is skipped.
class PrisonBreakFicNetAdapter(BaseSiteAdapter):
class NetRaptorOrgAdapter(BaseSiteAdapter):
def __init__(self, config, url):
BaseSiteAdapter.__init__(self, config, url)
@@ -48,34 +46,33 @@ class PrisonBreakFicNetAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
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','pbf')
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 = "%B %d, %Y"
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.prisonbreakfic.net'
return 'netraptor.org'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
return "http://"+self.getSiteDomain()+"/fanfiction/viewstory.php?sid=1234"
def getSiteURLPattern(self):
return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$"
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):
# 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)
@@ -86,6 +83,10 @@ class PrisonBreakFicNetAdapter(BaseSiteAdapter):
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)
@@ -94,19 +95,20 @@ class PrisonBreakFicNetAdapter(BaseSiteAdapter):
# Now go hunting for all the meta data and the chapter list.
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
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 = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
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('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+'/'+chapter['href']))
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/fanfiction/'+chapter['href']))
self.story.setMetadata('numChapters',len(self.chapterUrls))
@@ -119,10 +121,7 @@ class PrisonBreakFicNetAdapter(BaseSiteAdapter):
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
@@ -145,21 +144,18 @@ class PrisonBreakFicNetAdapter(BaseSiteAdapter):
if 'Categories' in label:
cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories'))
for cat in cats:
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'))
for char in chars:
charstext = [char.string for char in chars]
for char in charstext:
self.story.addToList('characters',char.string)
if 'Pairing' in label:
ships = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=3')) # XXX
for ship in ships:
self.story.addToList('ships',ship.string.replace(" and ","/"))
if 'Genre' in label:
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1')) # XXX
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)
@@ -184,7 +180,7 @@ class PrisonBreakFicNetAdapter(BaseSiteAdapter):
# 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']
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))
@@ -206,8 +202,7 @@ class PrisonBreakFicNetAdapter(BaseSiteAdapter):
logger.debug('Getting chapter text from: %s' % url)
soup = bs.BeautifulStoneSoup(self._fetchUrl(url),
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
soup = bs.BeautifulSoup(self._fetchUrl(url))
div = soup.find('div', {'id' : 'story'})
@@ -52,7 +52,7 @@ class NfaCommunityComAdapter(BaseSiteAdapter): # XXX
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
# XXX Most sites don't have the /fanfic part. Replace all to remove it usually.
@@ -74,6 +74,7 @@ class NfaCommunityComAdapter(BaseSiteAdapter): # XXX
# The site domain. Does have www here, if it uses it.
return 'nfacommunity.com' # XXX
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -155,7 +156,7 @@ class NfaCommunityComAdapter(BaseSiteAdapter): # XXX
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
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+"))
@@ -48,7 +48,7 @@ class NHAMagicalWorldsUsAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
@@ -66,6 +66,7 @@ class NHAMagicalWorldsUsAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'nha.magical-worlds.us'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -112,8 +113,9 @@ class NHAMagicalWorldsUsAdapter(BaseSiteAdapter):
for info in asoup.findAll('table', {'width' : '100%', 'bordercolor' : re.compile(r'#')}):
a = info.find('a')
if ('viewstory.php?sid='+self.story.getMetadata('storyId')) in a['href']:
self.story.setMetadata('title',a.string)
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
@@ -142,14 +144,14 @@ class NHAMagicalWorldsUsAdapter(BaseSiteAdapter):
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')))
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 += str(val)
svalue += unicode(val)
val = val.nextSibling
self.setDescription(url,svalue)
@@ -0,0 +1,176 @@
# -*- 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 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(self):
return "http://"+self.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)
@@ -48,7 +48,7 @@ class OcclumencySycophantHexComAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
@@ -66,6 +66,7 @@ class OcclumencySycophantHexComAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'occlumency.sycophanthex.com'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -157,7 +158,7 @@ class OcclumencySycophantHexComAdapter(BaseSiteAdapter):
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',a.text)
self.story.setMetadata('title',stripHTML(a))
break
@@ -48,7 +48,7 @@ class OneDirectionFanfictionComAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -70,6 +70,7 @@ class OneDirectionFanfictionComAdapter(BaseSiteAdapter):
return ['www.onedirectionfanfiction.com','onedirectionfanfiction.com']
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -157,7 +158,7 @@ class OneDirectionFanfictionComAdapter(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
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+"))
@@ -48,7 +48,7 @@ class PhoenixSongNetAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[3])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/fanfiction/story/' +self.story.getMetadata('storyId')+'/')
@@ -65,6 +65,7 @@ class PhoenixSongNetAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'www.phoenixsong.net'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/fanfiction/story/1234/"
@@ -112,6 +113,8 @@ class PhoenixSongNetAdapter(BaseSiteAdapter):
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:
@@ -123,7 +126,6 @@ class PhoenixSongNetAdapter(BaseSiteAdapter):
# 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)
@@ -134,7 +136,7 @@ class PhoenixSongNetAdapter(BaseSiteAdapter):
## 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',a.string)
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+"))
@@ -48,7 +48,7 @@ class PommeDeSangComAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# pommedesang.com has two 'sections', shown in URL as
# 'efiction' and 'sds' that change how things should be
@@ -75,6 +75,7 @@ class PommeDeSangComAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'pommedesang.com'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/efiction/viewstory.php?sid=1234 http://"+self.getSiteDomain()+"/sds/viewstory.php?sid=1234"
@@ -179,7 +180,7 @@ class PommeDeSangComAdapter(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile('viewstory.php\?sid=\d+'))
self.story.setMetadata('title',a.string)
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+"))
@@ -48,8 +48,7 @@ class PonyFictionArchiveNetAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
logger.info(self.parsedUrl.netloc)
# normalized story URL.
if "explicit" in self.parsedUrl.netloc:
self._setURL('http://explicit.' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -70,8 +69,9 @@ class PonyFictionArchiveNetAdapter(BaseSiteAdapter):
def getAcceptDomains(cls):
return ['www.ponyfictionarchive.net','ponyfictionarchive.net','explicit.ponyfictionarchive.net']
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234 http://explicit."+self.getSiteDomain()+"/viewstory.php?sid=1234"
def getSiteURLPattern(self):
return re.escape("http://")+"(www\.|explicit\.)?"+re.escape(self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$"
@@ -136,7 +136,7 @@ class PonyFictionArchiveNetAdapter(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
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+"))
@@ -55,7 +55,7 @@ class PortkeyOrgAdapter(BaseSiteAdapter): # XXX
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.path.split('/',)[2])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/story/'+self.story.getMetadata('storyId'))
@@ -72,6 +72,7 @@ class PortkeyOrgAdapter(BaseSiteAdapter): # XXX
# The site domain. Does have www here, if it uses it.
return 'fanfiction.portkey.org' # XXX
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/story/1234"
@@ -250,8 +251,10 @@ class PortkeyOrgAdapter(BaseSiteAdapter): # XXX
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.
data = data.replace("HTML>","div>")
soup = bs.BeautifulSoup(data)
#print("soup:%s"%soup)
tag = soup.find('td', {'class' : 'story'})
@@ -41,7 +41,7 @@ class PotionsAndSnitchesNetSiteAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/fanfiction/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -55,8 +55,9 @@ class PotionsAndSnitchesNetSiteAdapter(BaseSiteAdapter):
def getAcceptDomains(cls):
return ['www.potionsandsnitches.net','potionsandsnitches.net']
@classmethod
def getSiteExampleURLs(self):
return "http://www.potionsandsnitches.net/fanfiction/viewstory.php?sid=1234 http://potionsandsnitches.net/fanfiction/viewstory.php?sid=5678"
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+$"
@@ -82,7 +83,7 @@ class PotionsAndSnitchesNetSiteAdapter(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
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+"))
@@ -162,12 +163,11 @@ class PotionsAndSnitchesNetSiteAdapter(BaseSiteAdapter):
self.story.setMetadata('status', 'In-Progress')
if 'Published' in label:
self.story.setMetadata('datePublished', makeDate(stripHTML(value), "%d %b %Y"))
# limit date values, there's some extra chars.
self.story.setMetadata('datePublished', makeDate(stripHTML(value[:12]), "%d %b %Y"))
if 'Updated' in label:
# there's a stray [ at the end.
#value = value[0:-1]
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value), "%d %b %Y"))
self.story.setMetadata('dateUpdated', makeDate(stripHTML(value[:12]), "%d %b %Y"))
try:
# Find Series name from series URL.
@@ -54,7 +54,7 @@ class PotterFicsComAdapter(BaseSiteAdapter):
m = re.match(self.getSiteURLPattern(),url)
if m:
self.story.setMetadata('storyId',m.group('id'))
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# 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)
@@ -62,7 +62,7 @@ class PotterFicsComAdapter(BaseSiteAdapter):
raise exceptions.InvalidStoryURL(url,
self.getSiteDomain(),
self.getSiteExampleURLs())
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','potficscom')
@@ -72,13 +72,9 @@ class PotterFicsComAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'www.potterfics.com'
@classmethod
def getSiteExampleURLs(self):
return \
"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 "\
"http://www.potterfics.com/historias/111194"
return "http://www.potterfics.com/historias/12345 http://www.potterfics.com/historias/12345/capitulo-1 "
def getSiteURLPattern(self):
#http://www.potterfics.com/historias/127583
@@ -151,7 +147,7 @@ class PotterFicsComAdapter(BaseSiteAdapter):
#find first metadata block
mb = cell.div.findNextSibling('div')
#Get meta...
self.story.setMetadata('title', mb.b.string)
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:
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2011 Fanficdownloader team
# 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.
@@ -27,12 +27,12 @@ from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, makeDate
def getClass():
return JLAUnlimitedComAdapter
return PotterHeadsAnonymousComAdapter
class JLAUnlimitedComAdapter(BaseSiteAdapter):
# 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)
@@ -42,36 +42,70 @@ class JLAUnlimitedComAdapter(BaseSiteAdapter):
# Most sites that claim to be
# iso-8859-1 (and some that claim to be
# utf8) are really windows-1252.
self.username = "" # if left empty, site doesn't return any message at all.
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])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
self._setURL('http://' + self.getSiteDomain() + '/eFiction1.1/viewstory.php?sid='+self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','jla')
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 = "%m/%d/%y"
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.jlaunlimited.com'
return 'fanfic.potterheadsanonymous.com'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/eFiction1.1/viewstory.php?sid=1234"
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
def getSiteURLPattern(self):
return re.escape("http://"+self.getSiteDomain()+"/eFiction1.1/viewstory.php?sid=")+r"\d+$"
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):
@@ -81,7 +115,7 @@ class JLAUnlimitedComAdapter(BaseSiteAdapter):
# 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
addurl = "&ageconsent=ok&warning=4"
else:
addurl=""
@@ -98,8 +132,21 @@ class JLAUnlimitedComAdapter(BaseSiteAdapter):
else:
raise e
# Assume that if there is a url with 'warning=#' in the page then it is a 'check' page
m = re.search(r"'viewstory.php\?sid=\d+((?:&amp;ageconsent=ok)?&amp;warning=\d+)",data)
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
# &amp;warning= -- actually, so do other sites. Must be an
# eFiction book.
# viewstory.php?sid=1882&amp;warning=4
# viewstory.php?sid=1654&amp;ageconsent=ok&amp;warning=5
#print data
#m = re.search(r"'viewstory.php\?sid=1882(&amp;warning=4)'",data)
m = re.search(r"'viewstory.php\?sid=\d+((?:&amp;ageconsent=ok)?&amp;warning=\d+)'",data)
if m != None:
if self.is_adult or self.getConfig("is_adult"):
# We tried the default and still got a warning, so
@@ -120,27 +167,32 @@ class JLAUnlimitedComAdapter(BaseSiteAdapter):
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 = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')))
self.story.setMetadata('title',a.string)
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 = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
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+")):
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+'/eFiction1.1/'+chapter['href']+addurl))
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href']+addurl))
self.story.setMetadata('numChapters',len(self.chapterUrls))
@@ -177,34 +229,22 @@ class JLAUnlimitedComAdapter(BaseSiteAdapter):
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:
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'))
charstext = [char.string for char in chars]
for char in charstext:
for char in chars:
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:
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)
## 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:
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:
@@ -225,7 +265,7 @@ class JLAUnlimitedComAdapter(BaseSiteAdapter):
# 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+'/fanfic/'+a['href']
series_url = 'http://'+self.host+'/'+a['href']
# use BeautifulSoup HTML parser to make everything easier to find.
seriessoup = bs.BeautifulSoup(self._fetchUrl(series_url))
@@ -241,20 +281,18 @@ class JLAUnlimitedComAdapter(BaseSiteAdapter):
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),
soup = bs.BeautifulStoneSoup(self._fetchUrl(url),
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
div = soup.find('div', {'id' : 'story'})
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)
@@ -48,7 +48,7 @@ class PretenderCenterComAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/missingpieces/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -69,6 +69,7 @@ class PretenderCenterComAdapter(BaseSiteAdapter):
def getAcceptDomains(cls):
return ['www.pretendercentre.com','pretendercentre.com']
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/missingpieces/viewstory.php?sid=1234"
@@ -133,7 +134,7 @@ class PretenderCenterComAdapter(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.text)
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+"))
@@ -48,7 +48,7 @@ class PsychFicComAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -65,6 +65,7 @@ class PsychFicComAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'www.psychfic.com'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -129,7 +130,7 @@ class PsychFicComAdapter(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
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+"))
@@ -48,7 +48,7 @@ class QafFicComAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/atp/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -65,6 +65,7 @@ class QafFicComAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'www.qaf-fic.com'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/atp/viewstory.php?sid=1234"
@@ -137,7 +138,7 @@ class QafFicComAdapter(BaseSiteAdapter):
self.story.setMetadata('author',aut.string)
aut.extract()
self.story.setMetadata('title',a.string[:(len(a.string)-3)])
self.story.setMetadata('title',stripHTML(a)[:(len(a.string)-3)])
# Find the chapters:
chapters=soup.find('select')
@@ -22,7 +22,6 @@ import re
import urllib2
import cookielib as cl
from datetime import datetime
import json
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
@@ -52,10 +51,9 @@ class RestrictedSectionOrgSiteAdapter(BaseSiteAdapter):
m = re.match(self.getSiteURLPattern(),url)
if m:
self.story.setMetadata('storyId',m.group('id'))
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/' + m.group('filestory') + '.php?' + m.group('filestory') + '=' + self.story.getMetadata('storyId'))
logger.debug("storyUrl: (%s)"%self.story.getMetadata('storyUrl'))
else:
raise exceptions.InvalidStoryURL(url,
self.getSiteDomain(),
@@ -70,6 +68,7 @@ class RestrictedSectionOrgSiteAdapter(BaseSiteAdapter):
def getSiteDomain():
return 'www.restrictedsection.org'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/story.php?story=1234 http://"+self.getSiteDomain()+"/file.php?file=1234"
@@ -126,10 +125,12 @@ class RestrictedSectionOrgSiteAdapter(BaseSiteAdapter):
self.story.setMetadata('authorId',ahref.split('=')[1])
self.story.setMetadata('authorUrl','http://'+self.host+'/'+ahref)
self.story.setMetadata('author',a.string)
self.story.setMetadata('author',stripHTML(a))
# title, remove byauthorname.
self.story.setMetadata('title',h2.text[:h2.text.index("by"+a.string)])
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
@@ -48,7 +48,7 @@ class SamDeanArchiveNuAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -69,6 +69,7 @@ class SamDeanArchiveNuAdapter(BaseSiteAdapter):
def getAcceptDomains(cls):
return ['www.samdean.archive.nu','samdean.archive.nu']
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -109,7 +110,7 @@ class SamDeanArchiveNuAdapter(BaseSiteAdapter):
self.story.setMetadata('author',aut.string)
aut.extract()
self.story.setMetadata('title',a.string[:(len(a.string)-3)])
self.story.setMetadata('title',stripHTML(a)[:(len(stripHTML(a))-3)])
# Find the chapters:
chapters=soup.find('select')
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2012 Fanficdownloader team
# 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.
@@ -28,11 +28,11 @@ from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, makeDate
def getClass():
return SVUFictionComAdapter
return ScarHeadNetAdapter
# Class name has to be unique. Our convention is camel case the
# sitename with Adapter at the end. www is skipped.
class SVUFictionComAdapter(BaseSiteAdapter):
class ScarHeadNetAdapter(BaseSiteAdapter):
def __init__(self, config, url):
BaseSiteAdapter.__init__(self, config, url)
@@ -48,23 +48,24 @@ class SVUFictionComAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','svuf')
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 = "%b %d, %Y"
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 'svufiction.com'
return 'scarhead.net'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -108,13 +109,13 @@ class SVUFictionComAdapter(BaseSiteAdapter):
## 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=6"
addurl = "&ageconsent=ok&warning=5"
else:
addurl=""
@@ -130,12 +131,21 @@ class SVUFictionComAdapter(BaseSiteAdapter):
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
# &amp;warning= -- actually, so do other sites. Must be an
# eFiction book.
# viewstory.php?sid=1882&amp;warning=4
# viewstory.php?sid=1654&amp;ageconsent=ok&amp;warning=5
#print data
#m = re.search(r"'viewstory.php\?sid=1882(&amp;warning=4)'",data)
m = re.search(r"'viewstory.php\?sid=\d+((?:&amp;ageconsent=ok)?&amp;warning=\d+)'",data)
if m != None:
if self.is_adult or self.getConfig("is_adult"):
@@ -166,24 +176,19 @@ class SVUFictionComAdapter(BaseSiteAdapter):
# print data
# Now go hunting for all the meta data and the chapter list.
pagetitle = soup.find('tr',{'valign':'top'})
## Title
pt = soup.find('div', {'class' : 'title'})
a = pt.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
if a.text != "":
self.story.setMetadata('title',a.text)
else:
self.story.setMetadata('title','Banner')
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 = pt.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
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)
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.
@@ -193,46 +198,70 @@ class SVUFictionComAdapter(BaseSiteAdapter):
# 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 ""
bottom=soup.find('div', {'class' : 'bottom'}).text
self.story.setMetadata('numWords', bottom.split('Word count: ')[1].split(' Read:')[0])
self.story.setMetadata('datePublished', makeDate(bottom.split('Published: ')[1].split(' Updated:')[0], self.dateformat))
self.story.setMetadata('dateUpdated', makeDate(bottom.split('Updated: ')[1], self.dateformat))
content=soup.find('div', {'class' : 'content'})
value=content.find('h1').nextSibling
svalue = ""
while not defaultGetattr(value,'class') == 'smaller':
svalue += str(value)
value = value.nextSibling
self.setDescription(url,svalue)
status = content.text.split('Completed: ')[1]
if 'Yes' in status:
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)
cats = content.findAll('a',href=re.compile(r'browse.php\?type=categories'))
for cat in cats:
self.story.addToList('category',cat.string)
chars = content.findAll('a',href=re.compile(r'browse.php\?type=characters'))
for char in chars:
self.story.addToList('characters',char.string)
genres = content.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1'))
for genre in genres:
self.story.addToList('genre',genre.string)
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+"))
@@ -241,17 +270,14 @@ class SVUFictionComAdapter(BaseSiteAdapter):
# 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+'))
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
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
@@ -262,7 +288,7 @@ class SVUFictionComAdapter(BaseSiteAdapter):
logger.debug('Getting chapter text from: %s' % url)
soup = bs.BeautifulStoneSoup(self._fetchUrl(url),
soup = bs.BeautifulSoup(self._fetchUrl(url),
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
div = soup.find('div', {'id' : 'story'})
@@ -48,7 +48,7 @@ class ScarvesAndCoffeeNetAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -65,6 +65,7 @@ class ScarvesAndCoffeeNetAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'www.scarvesandcoffee.net'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -138,10 +139,10 @@ class ScarvesAndCoffeeNetAdapter(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
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+"))
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)
@@ -48,7 +48,7 @@ class SG1HeliopolisComAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
self.section=self.parsedUrl.path.split('/',)[1]
# normalized story URL.
@@ -74,6 +74,7 @@ class SG1HeliopolisComAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'sg1-heliopolis.com'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/archive/viewstory.php?sid=1234 http://"+self.getSiteDomain()+"/adult/viewstory.php?sid=1234 http://"+self.getSiteDomain()+"/atlantis/viewstory.php?sid=1234"
@@ -139,7 +140,7 @@ class SG1HeliopolisComAdapter(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
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+"))
@@ -0,0 +1,219 @@
# -*- 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 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(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
def getSiteURLPattern(self):
return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$"
## Login seems to be reasonably standard across eFiction sites.
def needToLoginCheck(self, data):
if '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)
@@ -48,7 +48,7 @@ class SinfulDesireOrgAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/archive/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -65,6 +65,7 @@ class SinfulDesireOrgAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'www.sinful-desire.org'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/archive/viewstory.php?sid=1234"
@@ -129,7 +130,7 @@ class SinfulDesireOrgAdapter(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
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+"))
@@ -49,7 +49,7 @@ class SiyeCoUkAdapter(BaseSiteAdapter): # XXX
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/siye/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -70,6 +70,7 @@ class SiyeCoUkAdapter(BaseSiteAdapter): # XXX
def getAcceptDomains(cls):
return ['www.siye.co.uk','siye.co.uk']
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/siye/viewstory.php?sid=1234"
@@ -110,7 +111,7 @@ class SiyeCoUkAdapter(BaseSiteAdapter): # XXX
## Title
titlea = authsoup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',titlea.string)
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+$")):
@@ -240,5 +241,6 @@ class SiyeCoUkAdapter(BaseSiteAdapter): # XXX
if None == story:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
story.name='div'
return self.utf8FromSoup(url,story)
@@ -59,7 +59,7 @@ class SquidgeOrgPejaAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/peja/cgi-bin/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -81,6 +81,7 @@ class SquidgeOrgPejaAdapter(BaseSiteAdapter):
# The config section name. Only override if != site domain.
return cls.getSiteDomain()+'/peja'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/peja/cgi-bin/viewstory.php?sid=1234"
@@ -102,6 +103,9 @@ class SquidgeOrgPejaAdapter(BaseSiteAdapter):
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)
@@ -121,7 +125,7 @@ class SquidgeOrgPejaAdapter(BaseSiteAdapter):
# formating, so it's a little ugly.
title = authorSoup.find('a',{'href':'viewstory.php?sid='+self.story.getMetadata('storyId')})
self.story.setMetadata('title',title.string)
self.story.setMetadata('title',stripHTML(title))
titleblock=title.parent.parent
chapterselect=soup.find('select',{'name':'chapter'})
@@ -48,7 +48,7 @@ class StargateAtlantisOrgAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/fanfics/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -65,6 +65,7 @@ class StargateAtlantisOrgAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'stargate-atlantis.org'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/fanfics/viewstory.php?sid=1234"
@@ -105,7 +106,7 @@ class StargateAtlantisOrgAdapter(BaseSiteAdapter):
self.story.setMetadata('author',aut.string)
aut.extract()
self.story.setMetadata('title',a.string[:(len(a.string)-3)])
self.story.setMetadata('title',stripHTML(a)[:(len(stripHTML(a))-3)])
# Find the chapters:
chapters=soup.findAll('div', {'class' : 'content'})
@@ -48,7 +48,7 @@ class StoriesOfArdaComAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/chapterlistview.asp?SID='+self.story.getMetadata('storyId'))
@@ -65,6 +65,7 @@ class StoriesOfArdaComAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'www.storiesofarda.com'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/chapterlistview.asp?SID=1234"
@@ -103,7 +104,7 @@ class StoriesOfArdaComAdapter(BaseSiteAdapter):
asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')))
a.find('em').extract()
self.story.setMetadata('title',a.text)
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+$"))
@@ -136,8 +137,20 @@ class StoriesOfArdaComAdapter(BaseSiteAdapter):
logger.debug('Getting chapter text from: %s' % url)
soup = bs.BeautifulStoneSoup(self._fetchUrl(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'
@@ -0,0 +1,401 @@
# -*- 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(self):
return "http://"+self.getSiteDomain()+"/s/1234 http://"+self.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 '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:
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+":i")
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+":i")
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.
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',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
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('genre',cat.text)
# in lieu of word count.
self.story.setMetadata('size', lc2.findNext('td', {'class' : 'num'}).text)
lc4 = lc2.findNext('td', {'class' : 'lc4'})
desc = lc4.contents[0]
try:
a = lc4.find('a', href=re.compile(r"/library/show_series.php\?id=\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
universes_soup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl') + "&type=uni"))
# logger.debug("Universe page=", universes_soup)
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' : '/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
except:
pass
try:
a = lc4.find('a', href=re.compile(r"/library/universe.php\?id=\d+"))
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")
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('span', {'id' : 'ptitle'}).text.partition(' &mdash;')[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)
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 'Codes' in label:
for code in value.split(' '):
self.story.addToList('codes',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)
@@ -44,7 +44,7 @@ class TenhawkPresentsComSiteAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -55,6 +55,7 @@ class TenhawkPresentsComSiteAdapter(BaseSiteAdapter):
def getSiteDomain():
return 'fanfiction.tenhawkpresents.com'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -132,7 +133,7 @@ class TenhawkPresentsComSiteAdapter(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')))
self.story.setMetadata('title',a.string)
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+"))
+116 -26
View File
@@ -40,6 +40,7 @@ class TestSiteAdapter(BaseSiteAdapter):
def getSiteDomain():
return 'test1.com'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"?sid=1234"
@@ -47,31 +48,64 @@ class TestSiteAdapter(BaseSiteAdapter):
return BaseSiteAdapter.getSiteURLPattern(self)+r'/?\?sid=\d+$'
def extractChapterUrlsAndMetadata(self):
idstr = self.story.getMetadata('storyId')
idnum = int(idstr)
self.do_sleep()
if self.story.getMetadata('storyId') == '665' and not (self.is_adult or self.getConfig("is_adult")):
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 self.story.getMetadata('storyId') == '666':
if idstr == '666':
raise exceptions.StoryDoesNotExist(self.url)
if self.story.getMetadata('storyId').startswith('670'):
if idstr.startswith('670'):
time.sleep(1.0)
if self.story.getMetadata('storyId').startswith('671'):
if idstr.startswith('671'):
time.sleep(1.0)
if self.getConfig("username"):
self.username = self.getConfig("username")
if self.story.getMetadata('storyId') == '668' and self.username != "Me" :
if idstr == '668' and self.username != "Me" :
raise exceptions.FailedToLogin(self.url,self.username)
if self.story.getMetadata('storyId') == '664':
self.story.setMetadata(u'title',"Test Story Title "+self.story.getMetadata('storyId')+self.crazystring)
if idstr == '664':
self.story.setMetadata(u'title',"Test Story Title "+idstr+self.crazystring)
self.story.setMetadata('author','Test Author aa bare amp(&) quote(&#39;) amp(&amp;)')
else:
self.story.setMetadata(u'title',"Test Story Title "+self.story.getMetadata('storyId'))
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
@@ -79,13 +113,12 @@ class TestSiteAdapter(BaseSiteAdapter):
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 self.story.getMetadata('storyId') == '669':
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')
idnum = int(self.story.getMetadata('storyId'))
if idnum % 2 == 1:
self.story.setMetadata('status','In-Progress')
else:
@@ -108,15 +141,67 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
self.story.setMetadata('rating','Tweenie')
if self.story.getMetadata('storyId') == '673':
self.story.addToList('author','Author From List')
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')
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')
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')
@@ -128,18 +213,19 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
self.story.addToList('warnings','Swearing')
self.story.addToList('warnings','Violence')
if self.story.getMetadata('storyId') == '80':
self.story.addToList('category',u'Rizzoli & Isles')
if idstr == '80':
self.story.addToList('category',u'Rizzoli &amp; Isles')
self.story.addToList('characters','J. Rizzoli')
elif self.story.getMetadata('storyId') == '81':
elif idstr == '81':
self.story.addToList('category',u'Pitch Perfect')
self.story.addToList('characters','Chloe B.')
elif self.story.getMetadata('storyId') == '83':
self.story.addToList('category',u'Rizzoli & Isles')
elif idstr == '83':
self.story.addToList('category',u'Rizzoli &amp; Isles')
self.story.addToList('characters','J. Rizzoli')
self.story.addToList('category',u'Pitch Perfect')
self.story.addToList('characters','Chloe B.')
elif self.story.getMetadata('storyId') == '82':
self.story.addToList('ships','Chloe B. &amp; J. Rizzoli')
elif idstr == '82':
self.story.addToList('characters','Henry (Once Upon a Time)')
self.story.addToList('category',u'Once Upon a Time (TV)')
else:
@@ -152,6 +238,9 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
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 &amp; Hermione Granger')
self.story.addToList('genre','Fantasy')
self.story.addToList('genre','Comedy')
@@ -208,6 +297,7 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
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)
@@ -217,6 +307,9 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
<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&gt;=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>
@@ -224,10 +317,6 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
<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>
@@ -236,7 +325,7 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
</div>
'''
elif self.story.getMetadata('storyId') == '0':
text=u'''
text=u'''<div>
<h3>45. Pronglet Returns to Hogwarts: Chapter 7</h3>
<br />
eyes but Im not convinced we should automatically<br />
@@ -245,6 +334,7 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
<br /><br />
Sure, invite her along. Does she have children?<br />
<br />
</div>
'''
else:
if self.story.getMetadata('storyId') == '667':
@@ -263,7 +353,7 @@ Don't&#8212e;ver&#8212d;o&#8212;that&#8212a;gain, &#27861; &#xE9;
<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>
<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>
'''
@@ -48,7 +48,7 @@ class TheAlphaGateComAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -65,6 +65,7 @@ class TheAlphaGateComAdapter(BaseSiteAdapter):
# The site domain. Does have www here, if it uses it.
return 'www.thealphagate.com'
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -98,7 +99,7 @@ class TheAlphaGateComAdapter(BaseSiteAdapter):
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
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+"))
@@ -48,7 +48,7 @@ class TheHexFilesNetAdapter(BaseSiteAdapter):
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/viewstory.php?sid='+self.story.getMetadata('storyId'))
@@ -69,6 +69,7 @@ class TheHexFilesNetAdapter(BaseSiteAdapter):
def getAcceptDomains(cls):
return ['www.thehexfiles.net','thehexfiles.net']
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
@@ -102,7 +103,7 @@ class TheHexFilesNetAdapter(BaseSiteAdapter):
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)
self.story.setMetadata('author',stripHTML(a))
asoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')))
try:
@@ -117,7 +118,7 @@ class TheHexFilesNetAdapter(BaseSiteAdapter):
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',a.string)
self.story.setMetadata('title',stripHTML(a))
break
@@ -72,7 +72,7 @@ class TheHookupZoneNetAdapter(BaseSiteAdapter): # XXX
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logger.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
# XXX Most sites don't have the /fanfic part. Replace all to remove it usually.
@@ -90,6 +90,7 @@ class TheHookupZoneNetAdapter(BaseSiteAdapter): # XXX
# The site domain. Does have www here, if it uses it.
return 'thehookupzone.net' # XXX
@classmethod
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/CriminalMinds/viewstory.php?sid=1234"
@@ -178,12 +179,12 @@ class TheHookupZoneNetAdapter(BaseSiteAdapter): # XXX
## Title
a = soup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',a.string)
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('authorUrl','http://'+self.host+'/CriminalMinds/'+a['href'])
self.story.setMetadata('author',a.string)
# Find the chapters:

Some files were not shown because too many files have changed in this diff Show More