Compare commits

...
Author SHA1 Message Date
Jim Miller 7529a24521 Plugin - Fix for issue with sorting tz aware vs naive dates. Look ahead for
ffnet for 1 chapter stories.
2012-01-22 22:00:21 -06:00
Jim Miller 78abc1ae74 Added tag calibre-plugin-1.2.2 for changeset be22fb9c7ec7 2012-01-19 14:07:44 -06:00
Jim Miller fb8ba4147c Fix to handle extratags correctly, kludge for ffnet for update emails that come
before the site is completely updated, fix plugin keeptags.
2012-01-19 14:07:33 -06:00
Jim Miller 62e0d41a7b Added tag fanficdownloader-4.2.1 for changeset bf79887dfdad 2012-01-17 19:03:40 -06:00
Jim Miller 8b0cef709f Added tag calibre-plugin-1.2.1 for changeset bf79887dfdad 2012-01-17 19:03:25 -06:00
Jim Miller 5681d5e2bd Month in English fix works. Plugin-Fixes to date proc. CLI-make personal.ini
from ~/.fan... take priority over pwd/defaults.ini.
2012-01-17 19:03:13 -06:00
Jim Miller 5162cb4ddb Kludge around for month names when running with non-English locale. Hoping to
fix reported problems with mediaminer and fictionalley.
"time data 'July 15, 2005' does not match format %B %d, %Y"
2012-01-16 23:37:16 -06:00
Jim Miller 45382ad424 Plugin--Fix for Update Always w/fewer chapters on site. Don't update Date
column when doing Calibre MetaData Only or Update Always with no new chapters.
Set Language to English if it's not already set.  Add some additional logging.
Support other URL form for TTH.
2012-01-16 23:34:10 -06:00
Jim Miller 37fce63735 Added tag fanficdownloader-4.2.1 for changeset f31de6f46e1f 2012-01-16 00:00:29 -06:00
Jim Miller 927ea0298c Bump webapp to 4.2.1, add announcements. 2012-01-16 00:00:15 -06:00
Jim Miller 2079952737 Disable rather than hide Reading List config tab so people see it's a feature. 2012-01-15 23:53:44 -06:00
Jim Miller dd53a33551 Added tag calibre-plugin-1.2.0 for changeset 4b67fdeac4ec 2012-01-15 23:15:09 -06:00
Jim Miller 2a86c3c9ab Adding 'characters' parsing to other existing sites. Similar 'fix' as twiwrite
for plugin for some stories across several sites.  Suspect different SGMLParser
in calibre stumbling on something common to many eFiction sites.
2012-01-15 23:14:54 -06:00
Jim Miller ce379a0700 Options for Reading List "Send to Device" and "To Read" lists. Fix config per
library.  Plugin specific example.ini.  Button to keyboard shortcut config.
More duplicate URL removal to avoid odd race conditions.
2012-01-15 22:02:46 -06:00
Jim Miller ac6b790b7d Saving intermediate dev version. Finds URL in dc:source for older/tth epubs,
saves config per library, tweaks def inis, add get URLs List feature, initial,
partially hard coded integration with Reading List plugin.
2012-01-14 22:50:42 -06:00
Jim Miller f587731797 Added tag calibre-plugin-1.1.5 for changeset 19d17085a579 2012-01-13 17:13:59 -06:00
Jim Miller afff98a9af Plugin 1.1.5. 2012-01-13 17:13:40 -06:00
Jim Miller 8dcb178ad0 Parse and save Characters on ffnet, fix genres on fictionpress. 2012-01-13 17:12:53 -06:00
Jim Miller 237e95933c Added tag calibre-plugin-1.1.4 for changeset 562e1e61688c 2012-01-12 21:39:46 -06:00
Jim Miller f7b5feede6 Plugin 1.1.4, fix Category title, keep calibre_bookmarks.txt on epub update. 2012-01-12 21:39:32 -06:00
Jim Miller 2375e0b6b0 Added tag calibre-plugin-1.1.3 for changeset c5cc35f2c3a8 2012-01-12 21:39:13 -06:00
Jim Miller ee7bb5775b Plugin-Add option to keep existing tags on updatemeta, fix Last Update tags.
Existing Completed, In-Progress and Last Update tags still removed.
Add plugin-defaults.ini to remove unusable options and default lastupdate off.
2012-01-10 20:25:26 -06:00
Jim Miller fdcf1ecf3a Added tag calibre-plugin-1.1.2 for changeset a49aee1d2562 2012-01-10 11:10:12 -06:00
Jim Miller 52ff63dbc3 Added tag fanficdownloader-4.2.0 for changeset a49aee1d2562 2012-01-10 11:09:57 -06:00
Jim Miller 092a0bcea0 Fixes for fimfiction.net site changes. 2012-01-09 22:34:01 -06:00
Jim Miller 6cbc009666 Added tag calibre-plugin-1.1.2 for changeset 11e15317b3aa 2012-01-09 18:10:26 -06:00
Jim Miller a8a049df79 Fix for plugin's 'add new book tags' issue. Bump plugin to 1.1.2 and release. 2012-01-09 18:10:19 -06:00
Jim Miller d04380092b Added tag calibre-plugin-1.1.1 for changeset 41da310867ef 2012-01-08 18:40:44 -06:00
Jim Miller 7bd141ba59 Plugin couldn't hangle user/pass AND is_adult well either. 2012-01-08 18:39:41 -06:00
Jim Miller f76c0ac823 Added tag calibre-plugin-1.1.1 for changeset a6033a2ed3a7 2012-01-08 17:25:49 -06:00
Jim Miller 35c4f0c609 Plugin-Need to overwrite existing file and ignore always_overwrite. 2012-01-08 15:54:38 -06:00
Jim Miller 413c24d149 Added tag fanficdownloader-4.2.0 for changeset f911c7feb03e 2012-01-08 14:31:34 -06:00
Jim Miller ce630d98bf Web/CLI 4.2.0, updated index.html. 2012-01-08 14:31:09 -06:00
Jim Miller 537af7e4c4 Added tag calibre-plugin-1.1.1 for changeset e32a9909f662 2012-01-08 13:42:42 -06:00
Jim Miller 36eb6c2e2c Plugin 1.1.1, only allow update when epub, restruct add/update code. 2012-01-08 13:42:27 -06:00
Jim Miller 6ee4be0236 CLI didn't handle user/pass AND is_adult well. 2012-01-08 11:02:28 -06:00
Jim Miller f7c321a36e twiwrite adult stories broke Soup in Plugin only. Suspect different SGMLParsers.
Removing all the headers first works around it.
2012-01-08 10:59:29 -06:00
Jim Miller fbcc582541 fanfic.castletv.net changed to castlefans.org/fanfic. 2012-01-08 10:57:57 -06:00
Jim Miller d561a9a2e6 Added tag calibre-plugin-1.1.0 for changeset cc3fc9de3120 2012-01-05 23:09:05 -06:00
Jim Miller ae103ff128 Couple more small fixes to plugin. 2012-01-05 23:08:51 -06:00
Jim Miller 102cc3498f Added tag calibre-plugin-1.1.0 for changeset d940020052e7 2012-01-05 22:59:22 -06:00
Jim Miller 0f6f24123b Clean up, better screen refreshing after update. 2012-01-05 22:58:46 -06:00
Jim Miller ee2db5549d Plugin 1.1.0--BG process (not thread) for reliability, true updates, menu. 2012-01-05 22:08:25 -06:00
Jim Miller 445ff17981 Fix for param to write to filename. Oops. 2012-01-04 20:52:27 -06:00
Jim Miller 9850fc0805 Allow List metadata in output_filename, param to write to filename,
tidy defaults.ini a little, pull u/p/is_adult up to base_adapter.py for plugin.
2012-01-04 16:39:16 -06:00
Jim Miller 8bcf2e53db Fix siye.co.uk for stories with <p> instead of <br/>. 2012-01-02 10:40:19 -06:00
Jim Miller c944d837fb Merge calibre-plugin branch back to default trunk. 2012-01-01 09:26:17 -06:00
Jim Miller 82efb462aa Add siye.co.uk. Plugin-add 'view defaults.ini', fix delete other forms, 1.0.4. 2011-12-31 21:51:03 -06:00
Jim Miller d455a97e3b Added tag calibre-plugin-1.0.3 for changeset db74b56aa2f0 2011-12-30 14:24:43 -06:00
Jim Miller 4ac42526bd Add 'delete other formats' option, bump to 1.0.3. 2011-12-30 14:24:30 -06:00
Jim Miller 81e6134139 Make plugin name consistent. 2011-12-30 14:12:58 -06:00
Jim Miller af1f69facc Once again accidently checked in code I'm still working on. Sorry. :-( 2011-12-30 14:07:44 -06:00
Jim Miller db513cb62a Need makezip.py for makeplugin.py. 2011-12-30 14:04:56 -06:00
Jim Miller f9c173f12f Added tag calibre-plugin-1.0.2 for changeset 14b351e079c3 2011-12-28 13:57:58 -06:00
Jim Miller 102a55131d Plugin wasn't including extratags in calibre tags. 2011-12-28 13:54:52 -06:00
Jim Miller 851d3df751 Pull minimum calibre version back down to 0.8.30 rather than latest/greatest. 2011-12-28 11:00:21 -06:00
Jim Miller 307ff464f1 Added tag calibre-plugin-1.0.2 for changeset ff8743cdd18b 2011-12-28 10:53:36 -06:00
Jim Miller aa2b1961e2 Update version numbers for plugin. 2011-12-28 10:53:26 -06:00
Jim Miller e3ceeb937f Make clickable link and HTML formatting in About box. 2011-12-25 17:36:36 -06:00
Jim Miller 74ac6fb348 Add options to disable/prioritize taking URLs from selected stories & clipboard. 2011-12-25 15:27:25 -06:00
Jim Miller 6e52a5b096 Fix for 'Add New' collision. 2011-12-24 09:40:23 -06:00
Jim Miller 3aaf9ec91d Added tag calibre-plugin-1.0.1 for changeset c97a71bd7dc9 2011-12-22 19:22:13 -06:00
Jim Miller d4280e84d1 Add epub update, option to only overwrite when newer, option to update meta only 2011-12-22 19:21:10 -06:00
Jim Miller 6bbe59c76b Prefill URLbox from selected books. Filter that and clipboard for good URL.
Save icon gimp file, python script to create calibre-plugin zip.
2011-12-21 23:46:12 -06:00
Jim Miller 2c6b0f9aa7 Added tag calibre-plugin-1.0.0 for changeset 42f22186b2dd 2011-12-21 23:44:22 -06:00
Jim Miller 8fb62499cd Remove 'aa' from plugin name, create a new icon. 2011-12-21 18:08:36 -06:00
Jim Miller dbda2f96a6 Plugin improvements: options, copy clipboard as url, save options, etc. 2011-12-21 15:01:18 -06:00
Jim Miller c9a69ff3c8 TTH won't send you future updates if you aren't 'caught up' on the story.
Login isn't required for F21, but logging in will mark stories you've
downloaded as 'read' on tth.
2011-12-20 14:40:05 -06:00
Jim Miller 56ac912706 Rename some files, further improvements--just about usable for new stories only. 2011-12-19 21:18:55 -06:00
Jim Miller b75c9d4946 Refactoring and cleaning up. 2011-12-19 18:04:47 -06:00
Jim Miller cbc8cadd91 First calibre-plugin version with working background downloading. 2011-12-19 14:59:20 -06:00
Jim Miller f634629b98 Check for existing duplicate, other minor improvements. 2011-12-18 19:09:08 -06:00
Jim Miller c0ea637aef Fill calibre metadata directly(vs read from epub), allow other output formats. 2011-12-17 14:56:46 -06:00
Jim Miller 5511b4fd28 Store/use personal.ini, is_adult and user/pass dialogs when needed. 2011-12-16 23:01:18 -06:00
Jim Miller a9fa06fd4d Collect URL, first 'useful' version. Borrowing from GPL v3 code, so not Apache License. 2011-12-16 11:29:36 -06:00
Jim Miller d5c38927fb Add initial version of plugin code--proof of concept stage. 2011-12-16 10:57:40 -06:00
Jim Miller 97e257d22a Begin on a calibre plugin of FFDL while still sharing code with CLI & web. 2011-12-16 10:55:08 -06:00
Jim Miller 6c2f025ca1 Correct post_process_cmd to use shell so it can parse args. 2011-12-16 10:23:17 -06:00
Jim Miller 71981c157f Remove 'use new version' texts. Automerge didn't do what I intended. 2011-12-15 17:50:45 -06:00
Jim Miller 68c9e3cb8e Added tag last_python25 for changeset f41bce05c6ba 2011-12-15 16:58:23 -06:00
Jim Miller dc0a4c2b25 Merging python27 branch back into default(trunk) branch. 2011-12-15 16:56:59 -06:00
Jim Miller 33b15fe7c1 Fix for (past)contrib author in tthfanficorg. Tweak meaning of --force in CLI. 2011-12-15 16:48:24 -06:00
Jim Miller 63faea59d8 Fix for fimfiction.net 'too many values to unpack' error 2011-12-14 15:27:46 -06:00
Jim Miller 8905a08862 Added tag fanficdownloader-4.1.0 for changeset ac4f9f370ee4 2011-12-14 12:49:52 -06:00
Jim Miller 1d1e0899ee Added tag fanficdownloader-4.1.1 for changeset 2d69c1a21b37 2011-12-14 12:49:40 -06:00
Jim Miller 18f26de557 Add post_process_cmd feature for CLI only. 2011-12-14 12:47:03 -06:00
Jim Miller 32bae4c47b Add tthfanfic.org. They have their own ebook downloads, but lack metadata. 2011-12-14 12:07:13 -06:00
Jim Miller 6e41e5d59a Add background_color for html/epub, change notice, bump to 4-1-1. 2011-12-14 11:54:28 -06:00
Jim Miller f9f7c6ea2a Twisting the Hellmouth epubs are .html, not .xhtml. Bump stored version to 4.1. 2011-12-14 11:08:26 -06:00
Jim Miller 9b98cb8cce Warning that AppEngine Python2.7 has a bug fetching large reqs. 2011-11-21 14:07:49 -06:00
Jim Miller 5ba3a36518 Tweak to index now that we have new new URL. 2011-11-15 17:33:09 -06:00
Jim Miller 672cc09b0d Disable r3m0v3rOrphans cron. There's a Python 2.7 runtime bug effecting it. 2011-11-15 11:52:09 -06:00
Jim Miller fed75f6684 Couple tweaks to multithreaded. 2011-11-15 11:28:56 -06:00
Jim Miller be645e012b First attempt at multithreaded python 2.7 version of downloader. 2011-11-09 17:41:15 -06:00
Jim Miller 1cf87eae3a Reduce story save time, increase status check time, add notice about quotas. 2011-11-08 12:50:45 -06:00
althaine 2cd89b2e55 Brought fimfiction.net published date parsing into line with new site layout 2011-11-06 18:59:33 +11:00
Jim Miller a3402170ad whoficcom only - viewstory.php url can appear descs, look for reviews.php. 2011-10-31 11:08:25 -05:00
Jim Miller 141ed90d74 Work around for poorly formatted desc on adastra. Saw error in logs. 2011-10-27 15:00:33 -05:00
Jim Miller 33e520fef1 Added tag fanficdownloader-4.0.6 for changeset c6f4e3562a86 2011-09-25 16:34:57 -05:00
Jim Miller 7e6963bbaf Bump version, add notice about fimfiction.net. 2011-09-25 16:32:56 -05:00
althaine 4a4ec3863d Brought fimfiction.net fic status in line with other adapters 2011-09-26 07:18:06 +10:00
althaine 070096e9cb Added fimfiction.net to index.html
Added fimfiction.net to defaults.ini
Add mature fic handling to fimfiction.net
More robust handling of story descriptions from fimfiction.net
Added 'characters' list
2011-09-25 15:23:06 +10:00
althaine 88bfd4ec58 Added fimfiction.net 2011-09-25 10:39:58 +10:00
Jim Miller c96381a0b4 Added tag fanficdownloader-4.0.5 for changeset ead54adcc041 2011-09-19 17:04:39 -05:00
Jim Miller f9466ef491 Improve CLI config file specification, CLI only. Issue 19. 2011-09-19 17:04:20 -05:00
Jim Miller 8e02856b55 Added tag fanficdownloader-4.0.5 for changeset 61d72dfc9f63 2011-09-15 14:50:06 -05:00
Jim Miller 8d0a29270a Add website_encodings option to change encoding list, add 'auto' as encoding type.
'auto' uses Universal Encoding Detector(http://chardet.feedparser.org/) to
derive encoding.  It spots utf-8 fairly well, but not iso8859-1/windows-1252,
so we require 90+% confidence before using it.
2011-09-15 14:43:12 -05:00
Jim Miller 06920119db Change hpcom to BeautifulSoup vs BeautifulStoneSoup to allow nested div tags. 2011-08-14 19:42:37 -05:00
Jim Miller 75fb648ace Annotate adapter_fanficcastletvnet.py for ease of copying for new sites. 2011-08-12 17:22:29 -05:00
Jim Miller e4ad7edb48 Added tag fanficdownloader-4.0.4 for changeset 8e70741ae507 2011-07-24 11:10:19 -05:00
Jim Miller ca0082797c AppEngine Version 4-0-4. 2011-07-24 11:10:05 -05:00
Jim Miller fe684926b1 Added tag fanficdownloader-4.0.4 for changeset ddec88fac578 2011-07-24 11:08:25 -05:00
Jim Miller f920f505c6 Fix for mediaminer.org genre. 2011-07-24 11:08:04 -05:00
Jim Miller 3ca4447279 Some tweaks for CLI only update feature. Doesn't force file extension anymore. 2011-07-22 17:14:25 -05:00
Jim Miller ac11984c97 Change abbrev for fanfic.castletv.net. 2011-07-14 19:54:27 -05:00
Jim Miller 09f3f62792 Add fanfic.castletv.net to index.html. 2011-07-14 19:41:48 -05:00
Jim Miller eefdaf8518 Add fanfic.castletv.net. 2011-07-14 18:13:24 -05:00
Jim Miller 0e7a211ce9 Running at 94% of available space used. Decrease save time to 6 days. 2011-07-04 12:20:02 -05:00
Jim Miller 9863616eae Improve ffnet error checking. 2011-06-20 14:24:21 -05:00
Jim Miller 5afd7b8c06 Check for and report errors in User Configuration. 2011-06-15 13:07:48 -05:00
Jim Miller 0e7ef6d804 Different Age Consent text in tenhawkpresents.com 2011-06-14 15:15:56 -05:00
Jim Miller 2a154c13ac Add fanfiction.tenhawkpresents.com. 2011-06-14 15:11:00 -05:00
Jim Miller 24120a73e7 Added tag fanficdownloader-4.0.3 for changeset efa536795df8 2011-06-12 16:26:22 -05:00
Jim Miller fddd146b39 Refactorings/tweaks from Update Feature work that are worthwhile even without it. 2011-06-12 16:26:05 -05:00
Jim Miller 8bf22729fe CLI downloader can update existing epubs, pull only metadata or chapter range.
epubmerge provides a lot of the grunt work for updates.
2011-06-09 19:07:43 -05:00
sigizmund e202105248 Adding Google +1 button to see how it goes. Minor static fixes. 2011-06-08 22:18:25 +01:00
Jim Miller 4f0ce05006 Added tag fanficdownloader-4.0.2 for changeset fbf33a024a38 2011-06-07 20:12:32 -05:00
Jim Miller 57534c26c5 My fix for ffnet latest update chapters broke m. mobile support. Fixed. 2011-06-07 20:12:11 -05:00
Jim Miller df3b95aa6f Added tag fanficdownloader-4.0.2 for changeset 2d6980d864a9 2011-06-07 19:50:01 -05:00
Jim Miller fd2d4a935d Bump to 4-0-2 primarily for new download version for ffnet/fpcom fix. 2011-06-07 19:49:49 -05:00
Jim Miller 71ae63e1ea Update index, remove new version notice, put ffnet/fpcom notice. 2011-06-07 14:26:31 -05:00
Jim Miller 2a353a74f9 Make ffnet/fpcom use BeautifulSoup (not Stone) for nested divs. 2011-06-07 14:19:25 -05:00
Jim Miller aa629c2c39 Merge ffnet/fpcom, make both use URL given to pull meta/chap list to avoid
missing latest chapter due to out of date servers.
2011-06-07 13:00:39 -05:00
Jim Miller 520a295f88 Try both utf8 and windows-1252 (order depends on adapter), then strip non-ascii
characters entirely if both fail.
2011-06-05 16:49:35 -05:00
Jim Miller 951bc3b030 Put orphan search in cron--it works now. Not that there were any. 2011-06-03 22:38:30 -05:00
Jim Miller 09d37c4426 Use cursor saved in memcache to search for orphan chunks. Smaller, ordered
search doesn't work because ids are not increasing.
2011-06-03 22:09:59 -05:00
Jim Miller b76e812eaa Orphan search exceeds queued task DB timeout as well. Use smaller search
like the regular cleanup and leave manual instead of cron.
2011-06-03 21:07:02 -05:00
Jim Miller ecabbd3af2 Use task queue for orphan chunk clean up. 2011-06-03 20:40:40 -05:00
Jim Miller 152ee61ed4 Now I remember why cleanup was limited--task timeout. Basic reverted to 100
each time, but still by time not day, orphan search will have to become a queue
task--unscheduled for now.
2011-06-03 20:12:04 -05:00
Jim Miller f2283a2052 Improve download cleanup, add orphan chunk cleanup, add recent list clear,
improve mediaminer no such story detect.
2011-06-03 20:03:14 -05:00
Jim Miller fb5740c549 Install hook to make urlfetch deadline 10s, add limited retry on fetch error. 2011-06-02 16:10:19 -05:00
Jim Miller 7133189601 Added tag fanficdownloader-4.0.1 for changeset 265d84461442 2011-06-02 12:35:37 -05:00
Jim Miller 6d24caee1e Tweak status.html and test1.com. 2011-06-02 12:35:13 -05:00
Jim Miller 583275f037 Added tag fanficdownloader-4.0.1 for changeset 01d49b13cc3c 2011-05-30 23:02:16 -05:00
Jim Miller 50b80b6d68 4-0-1 Add mobi back in, changes to recent&status appearance, more reliability. 2011-05-30 23:02:00 -05:00
Jim Miller f7857ad6dc Suppress warnings about Django version. 2011-05-30 15:48:35 -05:00
Jim Miller a3f75cb3eb Improve error handling/reporting, make redownloads appear at top of recent list. 2011-05-30 15:05:00 -05:00
Jim Miller 94063d478c Pass normalized URL to queue task, not original url. 2011-05-30 09:59:37 -05:00
Jim Miller f25eae83d3 Save corrected URL in DownloadMeta record so status & recent have good URL. 2011-05-29 21:21:02 -05:00
Jim Miller 433e371b74 Enable (www|m). for fictionpress.com, like ff.net. 2011-05-29 17:03:02 -05:00
Jim Miller def2937327 Save appengine version in DownloadMeta to aid troubleshooting. 2011-05-29 13:07:12 -05:00
Jim Miller e99adf3fa9 Allow trailing / after /Story_Title on ff.net, fp.com urls. 2011-05-29 10:44:46 -05:00
Jim Miller a05bf24ed3 Fix typo that prevented some proper fictionpress.com urls from working. 2011-05-28 21:28:54 -05:00
Jim Miller aa58f1c1ee Make much more forgiving of inexact URLs--that's most of our errors now. 2011-05-28 16:09:34 -05:00
Jim Miller 35aeae59f2 Tweaking logging slightly. 2011-05-27 20:43:36 -05:00
Jim Miller 6d5785a5ac Added tag fanficdownloader-4.0.0 for changeset 3dbb4938aead 2011-05-23 18:33:56 -05:00
Jim Miller 07fbb8c5d5 Bring 4.0.0 branch into trunk. 2011-05-23 18:26:24 -05:00
Jim Miller ac74918005 Make this default appengine version, downloader.py honors -f, remove extratags from default title page. 2011-05-23 17:36:56 -05:00
Jim Miller b85a5ab95b Include announcement of new version. 2011-05-23 17:35:24 -05:00
Jim Miller 215552af00 Strip html tags and entities from epub chapter titles. 2011-05-20 09:56:53 -05:00
Jim Miller 7f0c989319 Reduce logging level of normal system exceptions. 2011-05-19 20:36:19 -05:00
Jim Miller d38360be6b Fix zip output file name. 2011-05-19 17:30:41 -05:00
Jim Miller bf52020876 Bump html output title size back up to h1. 2011-05-19 10:35:41 -05:00
Jim Miller 58b95af0da Fixes for various problems found during testing, remove old files. Beta ready. 2011-05-16 12:38:42 -05:00
Jim Miller df9e877865 Fixes to various from first round testing and some code clean up. 2011-05-13 21:34:58 -05:00
Jim Miller a88d4cac50 Support for fictionalley.org, add feature to not overwrite files newer than last
update, fix to config section precedence, consolidate date parsing a bit.
2011-05-11 22:44:18 -05:00
Jim Miller 756b6944fc Support for mediaminer.org, update copyrights. 2011-05-11 12:29:59 -05:00
Jim Miller 6d2f657729 Support for thewriterscoffeeshop.com, allow both login/pass and Are Adult in one site. 2011-05-10 13:26:34 -05:00
Jim Miller 9b2450a609 Support for twiwrite.net, potionsandsnitches.com & harrypotterfanfiction.com. 2011-05-09 21:20:06 -05:00
Jim Miller 36d4b9afcf Add adastrafanfic.com support and support to confirm adult w/o login/pass. 2011-05-09 14:46:05 -05:00
Jim Miller d90c9ebced Support for ficwad.com. Further tweaks/improvments, especially to user config.
(I'm getting a bit carried away with making things configurable, honestly.)
2011-05-08 21:53:06 -05:00
Jim Miller ecd1690b70 Add fictionpress.com adapter, some more tweak/fixes. 2011-05-07 15:14:46 -05:00
Jim Miller 87a57cc286 Add (partial) CLI options, fixes to site editconfig. 2011-05-07 12:51:03 -05:00
Jim Miller d91f2e74fd Only ask for Login/Password when required. Save and use user configuration. 2011-05-06 20:33:54 -05:00
Jim Miller d3b1ddcda9 Added tag fanficdownloader-3.0.2 for changeset 564ada569d46 2011-05-06 19:59:04 -05:00
Jim Miller a7b80388dd Merge changes from trunk to 4-0 branch. 2011-05-06 13:00:32 -05:00
Jim Miller 9515ff00be Tweak output a bit, allow any 'entry' as epub subject. 2011-05-06 12:42:20 -05:00
sigizmund 605c4c40fc Mis-aligned ad block 2011-05-06 12:00:50 +01:00
sigizmund 3e1f0dd53b Fixing screwed Google Analytics tracking code. I don't know how, but it nearly disappeared and our impressions went down from 250+ a day to around 30. 2011-05-06 11:24:27 +01:00
Jim Miller 0e28d86a7b Fix for zip files, status in epub. 2011-05-05 20:47:39 -05:00
Jim Miller 94669a2179 Fix up exception handling, first working appengine(SDK) version. 2011-05-04 17:54:36 -05:00
Jim Miller 547411666d Move CLI up a level to match appengine, change module packaging to suit.
Change mechanism for loading adapters to avoid problems with module init.
Move adapter exceptions to own file.
2011-05-04 14:29:24 -05:00
Jim Miller 150316f460 Commit first version of reorg/rewrite. Currently CLI only. 2011-05-03 11:27:58 -05:00
Jim Miller f75abaad72 3-0-2 Add convert link feature. Remove title page table(for convert). Make favicon.ico work. 2011-05-02 13:54:48 -05:00
Jim Miller bdaea79875 Previous change to elim newlines in debug sometimes output breaks download.
Cheap fix, comment out debugs.
2011-05-02 13:12:16 -05:00
Jim Miller 7420d27652 More log clean up, better handling of bad URL in ficwad, missing story in ffnet. 2011-04-26 17:22:51 -05:00
Jim Miller 1426626d0a Strip leading newline so it doesn't (falsely) show up in appengine logs as error. 2011-04-25 19:57:40 -05:00
Jim Miller 666e9666cd Attempting to help with ffnet failures. Increase fetch deadline, add retries with backoff, 1/2 sleep.
Also remove <> from error message--HTML eats it.
2011-04-22 17:03:43 -05:00
Jim Miller 1c2c1a5d62 Add support for twiwrite.net. Requires User/Pass like twilighted.net. 2011-04-20 22:34:59 -05:00
Jim Miller 7c5aaa36d5 Save recent downloads longer(5 days, was 2). Fix some redirects.
Reduces log errors and makes auto-re-login more transparent.
2011-04-20 17:32:30 -05:00
sigizmund 13b96946d1 Improving logging, can dump the data which was downloaded if download fails. 2011-04-19 12:49:37 +01:00
sigizmund 1119ba52a0 Replaced ad block 2011-04-18 11:07:56 +01:00
Jim Miller b058477b97 Fix so recent, but pre-compress mobis will still download. 2011-04-03 16:55:51 -05:00
Jim Miller 1f303013bd Fix to allow retrying, compress mobi chunks, Working/Finished/Failed in status.html title, keep 3 days worth. 2011-04-03 16:47:08 -05:00
Jim Miller 302a8f12a3 Removing storage for old version. Fixes for whofic meta data. 2011-04-01 22:14:05 -05:00
retiefjimm 3e381dd33f Still tweaking the clean up cron. 2011-03-24 22:09:11 -05:00
retiefjimm bd82311d51 Mobi improvements: Mark TOC so reader can find it, don't let reader eat every 4096th char, don't prettify. Remove debug outputs from whofic. Increase fetch size of remover. 2011-03-24 12:57:38 -05:00
retiefjimm efb521c829 Added tag fanficdownloader-0.5 for changeset 0f08ff79de17 2011-03-22 19:45:47 -05:00
retiefjimm 925324bd13 Improved (but not perfected) mobi file output. Reduce cron clean up time to 2 days. 2011-03-22 19:17:41 -05:00
retiefjimm 1d1ed1ef71 Add support for www.whofic.com. 2011-03-22 19:11:17 -05:00
retiefjimm 389fc010f2 After merging QueueProc changes, tweaks clean up cron times and change appspot instance name. 2011-03-22 12:59:12 -05:00
Jim Miller 5ab5991d6e Merging QueueProc changes to trunk. 2011-03-22 12:48:17 -05:00
retiefjimm 8c606e45fd Remove 2 sec sleep before making live version. 2011-03-17 17:03:25 -05:00
retiefjimm e645c054d8 adastrafanfic can have chapters with tags (<i>) in them. Allow get as well as post. 2011-03-15 14:49:16 -05:00
retiefjimm e6ebd956f8 Datastore max size is 1,000,000, not 1M(=2^20). 2011-03-10 14:30:42 -06:00
retiefjimm ef5e7700b9 Back out some changes I didn't actually mean to commit. Just some custom stuff. 2011-03-10 13:14:51 -06:00
retiefjimm 53e2a0f204 First version with background queue processing and support for stories >1M. 2011-03-10 13:00:11 -06:00
retiefjimm 0e9d992e13 Make Branch for conversion to Queue Processing. 2011-03-04 11:58:53 -06:00
retiefjimm 790aca39da Add epubmerge.py, a standalone CLI program to merge multiple epubs together into one. 2011-02-24 18:02:49 -06:00
retiefjimm 7064873b4e Put title, author and publisher into mobi files properly as meta data. Also strips non-ascii chars from t,a,p--caused problems with mobi gen.
Calibre's book reader cannot read the mobi files produced from fanficdownloader, either before or after these changes.
2011-01-23 13:37:29 -06:00
retiefjimm 95e7ba636d Add commas to word counts. 2011-01-15 10:41:28 -06:00
sigizmund fe557bbce5 Fixed downloading mobi from fdown 2011-01-03 20:35:15 +00:00
retiefjimm 783a614345 Enable collection of user's own user/password again. Only used by twilighted.net. 2010-12-27 11:29:12 -06:00
retiefjimm a32ccf7dac Work around an SGMLParser/BeautifulStoneSoup entity bug--incorrectly inserts (;) when it shouldn't, "AT&T" becomes "AT&T;". Also update to latest BeautifulSoup: has one minor, unrelated change. 2010-12-19 12:21:55 -06:00
retiefjimm d3abfbf5d6 Add Star Trek fanfic site adastrafanfic.com, plus add filename(linenum) to logger output for CLI. 2010-12-13 23:31:35 -06:00
retiefjimm d95c72f447 Split info line in ffnet by ' - ' instead of just '-' so Sci-Fi doesn't become just 'Sci'. fpcom already does. 2010-12-08 17:40:10 -06:00
retiefjimm e05749678e Move status->subject into output.py as per Bill's suggestion. 2010-12-06 15:03:07 -06:00
retiefjimm f7d94105ee Correct example URL. 2010-12-05 21:40:04 -06:00
retiefjimm 50b0573033 Add status (In-Progress or Completed) to metadata as a Subject tag. 2010-12-05 21:38:16 -06:00
wsuetholz 48b882c745 2 second sleep was in Mediaminer as well. 2010-12-05 21:21:44 -06:00
wsuetholz 3fdd079b97 Merge with 8d1772310e5c4ad1772a92b1f0408952b0b370e2 2010-12-05 20:58:38 -06:00
wsuetholz e674f7c7df There was still a 2 second sleep between chapters for the fictonpress adapter.. Keep in mind that if you do a lot of downloading from fanfiction.net or fictionpress.com it is possible that your IP address could be locked without the sleep in place. 2010-12-05 20:52:13 -06:00
retiefjimm 8f58164dde Added tag fanficdownloader-0.4 for changeset f23fd0e4cbc3 2010-12-02 17:36:52 -06:00
wsuetholz 4c99fdbcc1 Removed some leftover functions that were left from using ffnet.py as a base for fpcom.py and mediaminer.py. 2010-11-30 15:17:00 -06:00
wsuetholz c6b79cb57b Changed the "Title Page" reference in the TOC and CONTENT epub files to be title_page instead. 2010-11-30 15:11:20 -06:00
wsuetholz f114a2438e This one might be controversial..
Since none of the EBook readers are really for update-able stories I added tags/subjects in for Last Update Year/Month and Last Update, which at least in FBReader I can order by, and see all stories updated for a certain month, or a certain date.  This will let you know to reread the story.
2010-11-30 14:06:12 -06:00
wsuetholz 2f0bb31e24 Take out adding the id: field as a subject. 2010-11-30 13:56:18 -06:00
wsuetholz 30455bfda2 Updated the processing for fictionpress.com of the information line to correctly extract the Updated and Published dates, and the Completion status 2010-11-30 12:13:00 -06:00
wsuetholz cfb218f21e Moved the check for a completed story higher in the code, this makes the continue's that are in that portion of the if statement not skip that check. 2010-11-29 18:45:47 -06:00
retiefjimm 617dc21eb9 Kludge fix for uniode in story descs in appengine. 2010-11-27 17:32:31 -06:00
retiefjimm 2d7f6c2e5c Correct story status for ficwad, change story status for fictionalley to 'Unknown' (site doesn't tell us status), change mobi file output mode to 'wb'(binary) so it works on windows. 2010-11-26 19:36:53 -06:00
sigizmund 719aed32a9 Tiny static changes - preparing for doing some SEO 2010-11-26 15:55:04 +00:00
retiefjimm ccef466090 Change no-class div tags to p tags in mediaminer stories to get paragraph breaks. 2010-11-24 12:36:38 -06:00
sigizmund 708f750bab Added Mobipocket support to the Web UI 2010-11-24 09:37:49 +00:00
retiefjimm 3b0a2639e7 Merge because Hg is insistent. 2010-11-23 12:53:27 -06:00
retiefjimm edb4f5a3bd Change a couple of the example story URLs. 2010-11-23 12:49:57 -06:00
retiefjimm f3571959df Change a couple of the example story URLs. 2010-11-23 12:42:33 -06:00
sigizmund ebcce7e42d Adding Mobi format and making final configuration changes before uploading a beta version. 2010-11-23 07:15:18 +00:00
retiefjimm cf0d1bf09b Change output names for web version, make html & text output work for CLI version. 2010-11-22 20:05:58 -06:00
retiefjimm ecdeef4b3b Add new adapters to main.py, update verbiage on index, replace a couple prints with logging.debug. 2010-11-22 15:00:00 -06:00
retiefjimm dd483db783 Move books directory down into sub dir next to CLI. 2010-11-22 13:24:50 -06:00
sigizmund 348786f073 cvreating a special tag so we can quickly restore to previous version 2010-11-22 07:06:29 +00:00
sigizmund 5549786946 Adding AppEngine files under the source control, moving downloaded code one directory deeper. 2010-11-22 07:04:17 +00:00
retiefjimm f584b3f1e8 Change calls for str() to unicode(). Default CL type to epub if not given. 2010-11-21 11:52:11 -06:00
retiefjimm df6b85b723 Fix desc escaping. <sigh> 2010-11-18 23:34:25 -06:00
wsuetholz 26f6ddabd9 The with call is not an automatic in python 2.5, you need to import with_statement for it to work.
the from __future__ import with_statement needs to be the first import statment as well.
2010-11-18 20:08:55 -06:00
wsuetholz 22d0989d0a Raise exceptions instead of an exit when errors are detected.
Added static functions to the writer classes that contain the type name and type extension to use when writing out the destination file.
Added a function to the zipdir that will check the date the fic was last updated in the current archive of a fic against the current last updated date and return whether the fic is newer then the archive file.  This is not useful at this time for when this is running under appEngine.
Added a flag to indicate if the program is running from appEngine or not.
2010-11-18 16:07:58 -06:00
retiefjimm 9b6fdb4540 Fix numeric entities--correctly this time. Reduce margins slightly, tweak description escaping slightly. 2010-11-18 14:26:16 -06:00
wsuetholz e3b4769bfb Misspelled the return variable from getOutputFileName. 2010-11-16 09:09:26 -06:00
wsuetholz 5618d8036b MediaMiner was not sending the date with the correct timezone info, so strip it off. We don't need the time anyways. 2010-11-13 23:30:47 -06:00
wsuetholz dbec6c95b2 Changed to deal with somebody putting in the mobile url for fanfiction.net... ie <http://m.fanfiction.net/s/11> will get changed to <http://www.fanfiction.net/s/11> 2010-11-12 21:51:46 -06:00
wsuetholz c77498e6ea Some changes in order to allow the refactoring of functions to work from within the appEngine. 2010-11-12 21:28:25 -06:00
wsuetholz 99ba1b48aa The getSubjects function was returning the author. 2010-11-12 20:27:17 -06:00
wsuetholz bdf429b41c Added in mediaminer.org adapter. Still has problems becaue mm seems to use the &#8820; type character encoding, and I couldn't manage to replace them. 2010-11-12 20:25:55 -06:00
wsuetholz 981a922d00 Deal with being passed in the first chapter as the url. 2010-11-11 13:53:48 -06:00
wsuetholz b51dd549f5 self.numChapters was one too many. 2010-11-11 13:22:30 -06:00
wsuetholz 35065593af Refactored source code to remove redundant code.. Moved most of the adapter get and set functions for variables into the adapter.py. Define all the adapter variables in adapter.py. Remove the unneeded functions from the various adapter sources.
Fix a couple of problems with fpcom.py.  Rearrange the Genre and Category for some of the adapters.
2010-11-11 13:14:15 -06:00
wsuetholz 696223a456 Added in an adapter for FictionPress.com. This started out as a copy of ffnet.py, but the extractIndividualUrls rapidly became completly different. I have not found a field that says total number of words for the story. 2010-11-11 09:55:37 -06:00
wsuetholz 30b914788c Add a new adapter for the potionsandsnitches web site. Once we started looking for the additional information for the title page, things were sufficiently different from the twilighted adapter that it couldn't be used anymore. 2010-11-10 23:11:08 -06:00
wsuetholz ed4ff4b6ab Trying to fix outputting some utf-8 text. Added the utf-8 header to all the source code.
Ended up modifying the removeEntities function to do a weird decode/encode step on the text passed in.  This seems to at least stop things from crashing..
2010-11-10 13:47:13 -06:00
wsuetholz 3f40df82dd Change to allow a chapter url to be passed in, yet still get the information it needs from the story index page. This will get the chapter links from the passed in url. 2010-11-10 12:36:21 -06:00
wsuetholz 7c252e83a1 It seems that self.numWords and self.numChapters can on occasion be numeric instead of string, so use the str() function when building the string for the title page. 2010-11-10 12:34:25 -06:00
wsuetholz d4c18fe1a8 Changed all references to downaloder.py to downloader.py. 2010-11-10 11:29:17 -06:00
wsuetholz b2ea9c3e73 Changed the title page generation to allow for easily adding and removing entries from the table of information shown.
Modified ffnet.py to use the mangled story title if given instead of the name portion of the URL.  As part of this, the Name and chapter number are now optional in the URL passed in.
2010-11-10 11:18:48 -06:00
wsuetholz 33d1fabd11 Changeset Tag Branch User Date Summary
53:c93e07566456			wsuetholz	2010-11-10 10:11	Ficwad wants you to login in order to view some stories..  They old ficwad.py got around that by starting with the first chapter instead of the story index page.  Since I needed the story index page I had changed it to switch to that page, and then scrape the chapter information from there, which doesn't work if the chapters are blocked.  While it still won't work if you pass in the URL for the story index page, I now switch back to the page that you passed in originally when looking for the chapters to download..  The one problem I have with this, is I change the self.url to the story index page, which should probably remain so that we have a consistent self.url even if the user starts with chapter 9 this time instead of chapter 1.
2010-11-10 10:18:46 -06:00
retiefjimm c326aa47b0 Need to reverse sort entities list to get entities with ';' ahead of versions without. Like '&quot;' and '&quot'. Otherwise '&quot;' becomes '";'. 2010-11-09 23:02:33 -06:00
wsuetholz d67668de23 Changes to the format of the title page. Tried to fix the description field on the title page and the contents.opf page. 2010-11-09 18:20:37 -06:00
wsuetholz 363c211401 Had left in a debugging statements that should have been removed.. 2010-11-09 18:18:42 -06:00
wsuetholz 379efc34f1 Add a title page to the resulting EPUB file. This required scraping more information from the web pages in order to populate the new fields. This change includes a change to the way that the output.py uses the adapters. It now gets passed in the adapter and then calls functions from the adapter in order to retrieve the scraped information. This will make it easier down the road when adding more information, or even pictures. 2010-11-09 16:35:46 -06:00
retiefjimm 778deaea00 Replace the haphazardly collected set of html entities with a list of known (browser) supported entities. Will now preserve right/left quotes. 2010-10-28 17:29:00 -05:00
retiefjimm 33c3f0b4fd Added tag fanficdownloader-0.2 for changeset 3eff15bceebe 2010-10-20 15:02:11 -05:00
retiefjimm 5bca3106df Adding a twipassword.py and a quick readme.txt. 2010-10-20 14:58:48 -05:00
retiefjimm c056ed0740 Fix zip routine so it checks for file mimetype doesn't just assume it exists. 2010-10-16 11:30:09 -05:00
retiefjimm 25cbab225c potionsandsnitches.net doesn't require login. Indeed, it doesn't even have the same login URL. 2010-10-14 12:42:29 -05:00
retiefjimm 4e53481d7d Somewhat kludgey fix for the kludgey way we extract the story text from fictionalley for stories with poor html. 2010-10-13 00:01:24 -05:00
retiefjimm b9a72d372d Add &#x0000; entity support, fix "<authorname> aa" from testing, add potionsandsnitches.net. 2010-10-11 20:44:19 -05:00
retiefjimm 7f8d7a7dbb Add '\n' after each </p> and <br /> tag. Extremely long(200k+) single line chapters (like ffnet gives) causes problems for nook.
Also removed a little dead code from ffnet.py.
2010-10-11 14:05:57 -05:00
retiefjimm dafa33c64e Generalize handling of &#<number>; entities. Generalize method to create a valid filename.
Remove print from fictionalley.py that was causing the output-before-302-header issue.
Some changes to chapter URL formation due to apparent differences in handling '//' in URLs in appengine vs downalod.py in 2.5.2.
2010-10-10 12:54:41 -05:00
retiefjimm 275a1dd6ad Add &#039;->'\'' to entity translation list. 2010-10-08 21:54:35 -05:00
retiefjimm f8701a9e8d Fix to force mimetype file to be uncompressed and first in epub. Tested on Python 2.5.2. 2010-10-08 18:46:03 -05:00
retiefjimm 466d5f4ee3 Remove Entities, fix bar ampersands in story & author names. 2010-10-06 14:31:21 -05:00
retiefjimm ca4b09d69d Fixes for some obscure encoding errors. UTF8 versions of apostrophe and dash in story titles & chapter titles could cause problems in a few places. 2010-10-03 19:23:39 -05:00
retiefjimm f75910ce7d Stop doing prettify on the HTML, it sometimes introduces extraneous white space. Change the chapter file names and ids inside the metadata to sequential chapter0001 rather than base64 of the chapter title. Replace invalid center & u tags with div styled centered and span styled underlined. Remove dead ffa.py file. Change downloda.py to not call ffa.py, but do call fictionalley. 2010-09-30 21:33:58 -05:00
retiefjimm 773ff3c03c Improved metadata for epubcheck, fixed a problem I introduced with &gt;/&lt;. 2010-09-29 21:32:04 -05:00
retiefjimm 9434431c7b Add twilighted.net(not com) to downaloder.py so it actually *works*. Add one-shot support for twilighted.net. 2010-09-28 19:52:30 -05:00
retiefjimm 3be9b3331e Replace &#XXX; entities in file names, allow BR tags--some stories use multiple BRs are section breaks. 2010-09-28 19:26:19 -05:00
retiefjimm 84fea5b896 Fictionalley.com working, also add twilighted.com to downaloder.py. 2010-09-28 19:26:06 -05:00
retiefjimm fe67201f95 Fix up code for harrypotterfanfiction.com so it works and add one-shot support. Add &nbsp; &lt; and &gt; to entities list. 2010-09-28 13:26:49 -05:00
retiefjimm ccb3fb36c5 Allow '&amp;' and '&pound;'. Allow single chapter/one-shot stories from FicWad. Correct story names from FicWad. Stop creating empty 'Story Index' chapter in stories from FicWad. Exit on chapter d/l error from FicWad. 2010-09-26 19:08:49 -05:00
Jimm 31c68ff796 Exit on ff.net error downloading chapter. Replace ':' with '_' in file name for html output, too. 2010-09-25 11:55:31 -05:00
retiefjimm 950807747c Replace ':' in filename with '_' for Windoze. appspot version appears to already do this. 2010-09-25 00:44:56 -05:00
retiefjimm b22d961e73 Allow one-shot fics for ffnet.
Write epub files as mode 'b' binary so it works on Windows.
Allow hr tags in epub output.
Don't compress file mimetype in epub file--required by standard.
2010-09-24 23:36:05 -05:00
sigizmund 2b2e7d52ec Starting to use source control again after a long break :-) 2010-07-17 14:44:14 +01:00
sigizmund 48d561de1a Fixed multiline ff.net 2009-12-21 13:40:11 +00:00
sigizmund 1740e0da48 Changes while moving to AJAX service 2009-12-21 13:12:47 +00:00
sigizmund ed9e2992c0 hpfiction added - not yet working 2009-12-18 18:57:53 +00:00
sigizmund c123e77221 somemerges 2009-12-18 18:56:44 +00:00
sigizmund 5143a2187a fictionpress.com support added 2009-12-18 18:55:57 +00:00
sigizmund 1f897843e0 Whole lot of fixes related to appengine 2009-12-18 14:51:53 +00:00
sigizmund ef97755ed3 AppEngine version. Not quite clear is it working or n ot 2009-12-18 12:26:23 +00:00
sigizmund 33855b7877 zipdir with in-memory stuff added 2009-12-18 11:40:29 +00:00
sigizmund 615228a2ac changed slightly to make it work with AppEngine 2009-12-18 11:23:37 +00:00
sigizmund 028c2df521 Fixed a problem with &#[digits]; for fanfiction.net. Just why BeautifulSoup cannot handle them? 2009-12-17 15:32:05 +00:00
sigizmund 2352ba6dfc forgot about html_constants 2009-12-17 15:18:31 +00:00
sigizmund 59697f1a75 Added adapter.py, now all working adapters must implement FanfictionSiteAdapter interface 2009-12-17 15:16:51 +00:00
sigizmund a01e0e2ed6 Fixed few problems, including the slash in filename 2009-12-17 14:37:31 +00:00
sigizmund 8c621d6a59 Small bug in ff.net parsing was fixed 2009-12-17 13:44:07 +00:00
sigizmund f55e6d3c05 epub zip is now produced correctly, also TOC works with Stanza now 2009-12-17 13:37:13 +00:00
sigizmund d2d183430d small changes to make work on other machines. why there's no Python 2.6 on Ubuntu? 2009-12-17 12:53:59 +00:00
sigizmund 6e73e5dc11 login_password again 2009-12-17 12:46:06 +00:00
sigizmund fd2ccb30ff login_password 2009-12-17 12:45:12 +00:00
sigizmund 41caae6226 fanfiction.net support added 2009-12-17 12:38:34 +00:00
kirillov 5adcf131b3 fanfiction.net support added 2009-12-17 12:38:15 +00:00
sigizmund bcf82e2632 zipdir added 2009-12-17 11:34:15 +00:00
sigizmund 259c0f3650 sample change 2009-12-17 11:30:23 +00:00
sigizmund 861d1561d9 test change 2009-12-17 11:29:10 +00:00
sigizmund 6ef95c634d FFA changed, architecture changed, not quite ready yet 2009-12-17 11:26:55 +00:00
sigizmund a9748061f0 minor changes 2009-12-15 15:26:45 +00:00
sigizmund c0459faa43 added 2009-12-15 15:23:48 +00:00
53 changed files with 4252 additions and 285 deletions
+17 -8
View File
@@ -1,20 +1,22 @@
# fanfictionloader ffd-retief
application: fanfictionloader
version: 4-0-7
runtime: python
# ffd-retief-hrd fanfictiondownloader
application: fanfictiondownloader
version: 4-2-1
runtime: python27
api_version: 1
threadsafe: true
handlers:
- url: /r3m0v3r.*
script: utils/remover.py
script: utils.remover.app
login: admin
- url: /tally.*
script: utils/tally.py
script: utils.tally.app
login: admin
- url: /fdownloadtask
script: main.py
script: main.app
login: admin
- url: /css
@@ -31,7 +33,14 @@ handlers:
upload: static/favicon\.ico
- url: /.*
script: main.py
script: main.app
builtins:
- datastore_admin: on
libraries:
- name: django
version: "1.2"
- name: PIL
version: "1.1.7"
+90
View File
@@ -0,0 +1,90 @@
#!/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'
__docformat__ = 'restructuredtext en'
# The class that all Interface Action plugin wrappers must inherit from
from calibre.customize import InterfaceActionBase
## Apparently the name for this class doesn't matter--it was still
## 'demo' for the first few versions.
class FanFictionDownLoaderBase(InterfaceActionBase):
'''
This class is a simple wrapper that provides information about the
actual plugin class. The actual interface plugin class is called
InterfacePlugin and is defined in the ffdl_plugin.py file, as
specified in the actual_plugin field below.
The reason for having two classes is that it allows the command line
calibre utilities to run without needing to load the GUI libraries.
'''
name = 'FanFictionDownLoader'
description = 'UI plugin to download FanFiction stories from various sites.'
supported_platforms = ['windows', 'osx', 'linux']
author = 'Jim Miller'
version = (1, 2, 3)
minimum_calibre_version = (0, 8, 30)
#: This field defines the GUI plugin class that contains all the code
#: that actually does something. Its format is module_path:class_name
#: The specified class must be defined in the specified module.
actual_plugin = 'calibre_plugins.fanfictiondownloader_plugin.ffdl_plugin:FanFictionDownLoaderPlugin'
def is_customizable(self):
'''
This method must return True to enable customization via
Preferences->Plugins
'''
return True
def config_widget(self):
'''
Implement this method and :meth:`save_settings` in your plugin to
use a custom configuration dialog.
This method, if implemented, must return a QWidget. The widget can have
an optional method validate() that takes no arguments and is called
immediately after the user clicks OK. Changes are applied if and only
if the method returns True.
If for some reason you cannot perform the configuration at this time,
return a tuple of two strings (message, details), these will be
displayed as a warning dialog to the user and the process will be
aborted.
The base class implementation of this method raises NotImplementedError
so by default no user configuration is possible.
'''
# It is important to put this import statement here rather than at the
# top of the module as importing the config class will also cause the
# GUI libraries to be loaded, which we do not want when using calibre
# from the command line
from calibre_plugins.fanfictiondownloader_plugin.config import ConfigWidget
return ConfigWidget(self.actual_plugin_)
def save_settings(self, config_widget):
'''
Save the settings specified by the user with config_widget.
:param config_widget: The widget returned by :meth:`config_widget`.
'''
config_widget.save_settings()
# Apply the changes
ac = self.actual_plugin_
if ac is not None:
ac.apply_settings()
# For testing, run from command line with this:
# calibre-debug -e __init__.py
#
if __name__ == '__main__':
from PyQt4.Qt import QApplication
from calibre.gui2.preferences import test_widget
app = QApplication([])
test_widget('Advanced', 'Plugins')
+20
View File
@@ -0,0 +1,20 @@
<hr />
<p>Created by Jim Miller, borrowing heavily from Grant Drake's
'<a href="http://www.mobileread.com/forums/showthread.php?t=134856">Reading List</a>',
'<a href="http://www.mobileread.com/forums/showthread.php?t=126727">Extract ISBN</a>' and
'<a href="http://www.mobileread.com/forums/showthread.php?t=134000">Count Pages</a>'
plugins.</p>
<p>
Calibre officially distributes plugins from the mobileread.com forum site.
The official distro channel for this plugin is there: <a href="http://www.mobileread.com/forums/showthread.php?t=163261">FanFictionDownLoader</a>
</p>
<p> I also monitor the
<a href="http://groups.google.com/group/fanfic-downloader">general users
group</a> for the downloader. That covers the web application and CLI, too.
</p>
The source for this plugin is available
<a href="http://code.google.com/p/fanficdownloader/source/checkout">here</a>.
+447
View File
@@ -0,0 +1,447 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2011, Grant Drake <grant.drake@gmail.com>'
__docformat__ = 'restructuredtext en'
import os
from PyQt4 import QtGui
from PyQt4.Qt import (Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout,
QTableWidgetItem, QFont, QLineEdit, QComboBox,
QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime)
from calibre.constants import iswindows
from calibre.gui2 import gprefs, error_dialog, UNDEFINED_QDATETIME
from calibre.gui2.actions import menu_action_unique_name
from calibre.gui2.keyboard import ShortcutConfig
from calibre.utils.config import config_dir
from calibre.utils.date import now, format_date, qt_to_dt, UNDEFINED_DATE
# Global definition of our plugin name. Used for common functions that require this.
plugin_name = None
# Global definition of our plugin resources. Used to share between the xxxAction and xxxBase
# classes if you need any zip images to be displayed on the configuration dialog.
plugin_icon_resources = {}
def set_plugin_icon_resources(name, resources):
'''
Set our global store of plugin name and icon resources for sharing between
the InterfaceAction class which reads them and the ConfigWidget
if needed for use on the customization dialog for this plugin.
'''
global plugin_icon_resources, plugin_name
plugin_name = name
plugin_icon_resources = resources
def get_icon(icon_name):
'''
Retrieve a QIcon for the named image from the zip file if it exists,
or if not then from Calibre's image cache.
'''
if icon_name:
pixmap = get_pixmap(icon_name)
if pixmap is None:
# Look in Calibre's cache for the icon
return QIcon(I(icon_name))
else:
return QIcon(pixmap)
return QIcon()
def get_pixmap(icon_name):
'''
Retrieve a QPixmap for the named image
Any icons belonging to the plugin must be prefixed with 'images/'
'''
global plugin_icon_resources, plugin_name
if not icon_name.startswith('images/'):
# We know this is definitely not an icon belonging to this plugin
pixmap = QPixmap()
pixmap.load(I(icon_name))
return pixmap
# Check to see whether the icon exists as a Calibre resource
# This will enable skinning if the user stores icons within a folder like:
# ...\AppData\Roaming\calibre\resources\images\Plugin Name\
if plugin_name:
local_images_dir = get_local_images_dir(plugin_name)
local_image_path = os.path.join(local_images_dir, icon_name.replace('images/', ''))
if os.path.exists(local_image_path):
pixmap = QPixmap()
pixmap.load(local_image_path)
return pixmap
# As we did not find an icon elsewhere, look within our zip resources
if icon_name in plugin_icon_resources:
pixmap = QPixmap()
pixmap.loadFromData(plugin_icon_resources[icon_name])
return pixmap
return None
def get_local_images_dir(subfolder=None):
'''
Returns a path to the user's local resources/images folder
If a subfolder name parameter is specified, appends this to the path
'''
images_dir = os.path.join(config_dir, 'resources/images')
if subfolder:
images_dir = os.path.join(images_dir, subfolder)
if iswindows:
images_dir = os.path.normpath(images_dir)
return images_dir
def create_menu_item(ia, parent_menu, menu_text, image=None, tooltip=None,
shortcut=(), triggered=None, is_checked=None):
'''
Create a menu action with the specified criteria and action
Note that if no shortcut is specified, will not appear in Preferences->Keyboard
This method should only be used for actions which either have no shortcuts,
or register their menus only once. Use create_menu_action_unique for all else.
'''
if shortcut is not None:
if len(shortcut) == 0:
shortcut = ()
else:
shortcut = _(shortcut)
ac = ia.create_action(spec=(menu_text, None, tooltip, shortcut),
attr=menu_text)
if image:
ac.setIcon(get_icon(image))
if triggered is not None:
ac.triggered.connect(triggered)
if is_checked is not None:
ac.setCheckable(True)
if is_checked:
ac.setChecked(True)
parent_menu.addAction(ac)
return ac
def create_menu_action_unique(ia, parent_menu, menu_text, image=None, tooltip=None,
shortcut=None, triggered=None, is_checked=None, shortcut_name=None,
unique_name=None):
'''
Create a menu action with the specified criteria and action, using the new
InterfaceAction.create_menu_action() function which ensures that regardless of
whether a shortcut is specified it will appear in Preferences->Keyboard
'''
orig_shortcut = shortcut
kb = ia.gui.keyboard
if unique_name is None:
unique_name = menu_text
if not shortcut == False:
full_unique_name = menu_action_unique_name(ia, unique_name)
if full_unique_name in kb.shortcuts:
shortcut = False
else:
if shortcut is not None and not shortcut == False:
if len(shortcut) == 0:
shortcut = None
else:
shortcut = _(shortcut)
if shortcut_name is None:
shortcut_name = menu_text.replace('&','')
ac = ia.create_menu_action(parent_menu, unique_name, menu_text, icon=None, shortcut=shortcut,
description=tooltip, triggered=triggered, shortcut_name=shortcut_name)
if shortcut == False and not orig_shortcut == False:
if ac.calibre_shortcut_unique_name in ia.gui.keyboard.shortcuts:
kb.replace_action(ac.calibre_shortcut_unique_name, ac)
if image:
ac.setIcon(get_icon(image))
if is_checked is not None:
ac.setCheckable(True)
if is_checked:
ac.setChecked(True)
return ac
def swap_author_names(author):
if author.find(',') == -1:
return author
name_parts = author.strip().partition(',')
return name_parts[2].strip() + ' ' + name_parts[0]
def get_library_uuid(db):
try:
library_uuid = db.library_id
except:
library_uuid = ''
return library_uuid
class ImageLabel(QLabel):
def __init__(self, parent, icon_name, size=16):
QLabel.__init__(self, parent)
pixmap = get_pixmap(icon_name)
self.setPixmap(pixmap)
self.setMaximumSize(size, size)
self.setScaledContents(True)
class ImageTitleLayout(QHBoxLayout):
'''
A reusable layout widget displaying an image followed by a title
'''
def __init__(self, parent, icon_name, title):
QHBoxLayout.__init__(self)
title_image_label = QLabel(parent)
pixmap = get_pixmap(icon_name)
if pixmap is None:
pixmap = get_pixmap('library.png')
# error_dialog(parent, _('Restart required'),
# _('You must restart Calibre before using this plugin!'), show=True)
else:
title_image_label.setPixmap(pixmap)
title_image_label.setMaximumSize(32, 32)
title_image_label.setScaledContents(True)
self.addWidget(title_image_label)
title_font = QFont()
title_font.setPointSize(16)
shelf_label = QLabel(title, parent)
shelf_label.setFont(title_font)
self.addWidget(shelf_label)
self.insertStretch(-1)
class SizePersistedDialog(QDialog):
'''
This dialog is a base class for any dialogs that want their size/position
restored when they are next opened.
'''
def __init__(self, parent, unique_pref_name):
QDialog.__init__(self, parent)
self.unique_pref_name = unique_pref_name
self.geom = gprefs.get(unique_pref_name, None)
self.finished.connect(self.dialog_closing)
def resize_dialog(self):
if self.geom is None:
self.resize(self.sizeHint())
else:
self.restoreGeometry(self.geom)
def dialog_closing(self, result):
geom = bytearray(self.saveGeometry())
gprefs[self.unique_pref_name] = geom
class ReadOnlyTableWidgetItem(QTableWidgetItem):
def __init__(self, text):
if text is None:
text = ''
QTableWidgetItem.__init__(self, text, QtGui.QTableWidgetItem.UserType)
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
class RatingTableWidgetItem(QTableWidgetItem):
def __init__(self, rating, is_read_only=False):
QTableWidgetItem.__init__(self, '', QtGui.QTableWidgetItem.UserType)
self.setData(Qt.DisplayRole, rating)
if is_read_only:
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
class DateTableWidgetItem(QTableWidgetItem):
def __init__(self, date_read, is_read_only=False, default_to_today=False):
if date_read == UNDEFINED_DATE and default_to_today:
date_read = now()
if is_read_only:
QTableWidgetItem.__init__(self, format_date(date_read, None), QtGui.QTableWidgetItem.UserType)
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
else:
QTableWidgetItem.__init__(self, '', QtGui.QTableWidgetItem.UserType)
self.setData(Qt.DisplayRole, QDateTime(date_read))
class NoWheelComboBox(QComboBox):
def wheelEvent (self, event):
# Disable the mouse wheel on top of the combo box changing selection as plays havoc in a grid
event.ignore()
class CheckableTableWidgetItem(QTableWidgetItem):
def __init__(self, checked=False, is_tristate=False):
QTableWidgetItem.__init__(self, '')
self.setFlags(Qt.ItemFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled ))
if is_tristate:
self.setFlags(self.flags() | Qt.ItemIsTristate)
if checked:
self.setCheckState(Qt.Checked)
else:
if is_tristate and checked is None:
self.setCheckState(Qt.PartiallyChecked)
else:
self.setCheckState(Qt.Unchecked)
def get_boolean_value(self):
'''
Return a boolean value indicating whether checkbox is checked
If this is a tristate checkbox, a partially checked value is returned as None
'''
if self.checkState() == Qt.PartiallyChecked:
return None
else:
return self.checkState() == Qt.Checked
class TextIconWidgetItem(QTableWidgetItem):
def __init__(self, text, icon):
QTableWidgetItem.__init__(self, text)
if icon:
self.setIcon(icon)
class ReadOnlyTextIconWidgetItem(ReadOnlyTableWidgetItem):
def __init__(self, text, icon):
ReadOnlyTableWidgetItem.__init__(self, text)
if icon:
self.setIcon(icon)
class ReadOnlyLineEdit(QLineEdit):
def __init__(self, text, parent):
if text is None:
text = ''
QLineEdit.__init__(self, text, parent)
self.setEnabled(False)
class KeyValueComboBox(QComboBox):
def __init__(self, parent, values, selected_key):
QComboBox.__init__(self, parent)
self.values = values
self.populate_combo(selected_key)
def populate_combo(self, selected_key):
self.clear()
selected_idx = idx = -1
for key, value in self.values.iteritems():
idx = idx + 1
self.addItem(value)
if key == selected_key:
selected_idx = idx
self.setCurrentIndex(selected_idx)
def selected_key(self):
for key, value in self.values.iteritems():
if value == unicode(self.currentText()).strip():
return key
class CustomColumnComboBox(QComboBox):
def __init__(self, parent, custom_columns, selected_column, initial_items=['']):
QComboBox.__init__(self, parent)
self.populate_combo(custom_columns, selected_column, initial_items)
def populate_combo(self, custom_columns, selected_column, initial_items=['']):
self.clear()
self.column_names = initial_items
if len(initial_items) > 0:
self.addItems(initial_items)
selected_idx = 0
for idx, value in enumerate(initial_items):
if value == selected_column:
selected_idx = idx
for key in sorted(custom_columns.keys()):
self.column_names.append(key)
self.addItem('%s (%s)'%(key, custom_columns[key]['name']))
if key == selected_column:
selected_idx = len(self.column_names) - 1
self.setCurrentIndex(selected_idx)
def get_selected_column(self):
return self.column_names[self.currentIndex()]
class KeyboardConfigDialog(SizePersistedDialog):
'''
This dialog is used to allow editing of keyboard shortcuts.
'''
def __init__(self, gui, group_name):
SizePersistedDialog.__init__(self, gui, 'Keyboard shortcut dialog')
self.gui = gui
self.setWindowTitle('Keyboard shortcuts')
layout = QVBoxLayout(self)
self.setLayout(layout)
self.keyboard_widget = ShortcutConfig(self)
layout.addWidget(self.keyboard_widget)
self.group_name = group_name
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
button_box.accepted.connect(self.commit)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
# Cause our dialog size to be restored from prefs or created on first usage
self.resize_dialog()
self.initialize()
def initialize(self):
self.keyboard_widget.initialize(self.gui.keyboard)
self.keyboard_widget.highlight_group(self.group_name)
def commit(self):
self.keyboard_widget.commit()
self.accept()
class DateDelegate(QStyledItemDelegate):
'''
Delegate for dates. Because this delegate stores the
format as an instance variable, a new instance must be created for each
column. This differs from all the other delegates.
'''
def __init__(self, parent):
QStyledItemDelegate.__init__(self, parent)
self.format = 'dd MMM yyyy'
def displayText(self, val, locale):
d = val.toDateTime()
if d <= UNDEFINED_QDATETIME:
return ''
return format_date(qt_to_dt(d, as_utc=False), self.format)
def createEditor(self, parent, option, index):
qde = QStyledItemDelegate.createEditor(self, parent, option, index)
qde.setDisplayFormat(self.format)
qde.setMinimumDateTime(UNDEFINED_QDATETIME)
qde.setSpecialValueText(_('Undefined'))
qde.setCalendarPopup(True)
return qde
def setEditorData(self, editor, index):
val = index.model().data(index, Qt.DisplayRole).toDateTime()
if val is None or val == UNDEFINED_QDATETIME:
val = now()
editor.setDateTime(val)
def setModelData(self, editor, model, index):
val = editor.dateTime()
if val <= UNDEFINED_QDATETIME:
model.setData(index, UNDEFINED_QDATETIME, Qt.EditRole)
else:
model.setData(index, QDateTime(val), Qt.EditRole)
+403
View File
@@ -0,0 +1,403 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2011, Jim Miller'
__docformat__ = 'restructuredtext en'
import traceback, copy
from PyQt4.Qt import (QDialog, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
QTextEdit, QComboBox, QCheckBox, QPushButton, QTabWidget)
from calibre.gui2 import dynamic, info_dialog
from calibre.utils.config import JSONConfig
from calibre.gui2.ui import get_gui
from calibre_plugins.fanfictiondownloader_plugin.dialogs \
import (SKIP, ADDNEW, UPDATE, UPDATEALWAYS, OVERWRITE, OVERWRITEALWAYS,
CALIBREONLY,collision_order)
from calibre_plugins.fanfictiondownloader_plugin.common_utils \
import ( get_library_uuid, KeyboardConfigDialog )
from calibre.gui2.complete import MultiCompleteLineEdit
# This is where all preferences for this plugin will be stored
# Remember that this name (i.e. plugins/fanfictiondownloader_plugin) is also
# in a global namespace, so make it as unique as possible.
# You should always prefix your config file name with plugins/,
# so as to ensure you dont accidentally clobber a calibre config file
all_prefs = JSONConfig('plugins/fanfictiondownloader_plugin')
# Set defaults used by all. Library specific settings continue to
# take from here.
all_prefs.defaults['personal.ini'] = get_resources('plugin-example.ini')
all_prefs.defaults['updatemeta'] = True
all_prefs.defaults['keeptags'] = False
all_prefs.defaults['urlsfromclip'] = True
all_prefs.defaults['updatedefault'] = True
all_prefs.defaults['fileform'] = 'epub'
all_prefs.defaults['collision'] = OVERWRITE
all_prefs.defaults['deleteotherforms'] = False
all_prefs.defaults['send_lists'] = ''
all_prefs.defaults['read_lists'] = ''
all_prefs.defaults['addtolists'] = False
all_prefs.defaults['addtoreadlists'] = False
all_prefs.defaults['addtolistsonread'] = False
# The list of settings to copy from all_prefs or the previous library
# when config is called for the first time on a library.
copylist = ['personal.ini',
'updatemeta',
'keeptags',
'urlsfromclip',
'updatedefault',
'fileform',
'collision',
'deleteotherforms',
'addtolists',
'addtoreadlists',
'addtolistsonread']
# fake out so I don't have to change the prefs calls anywhere. The
# Java programmer in me is offended by op-overloading, but it's very
# tidy.
class PrefsFacade():
def __init__(self,all_prefs):
self.all_prefs = all_prefs
self.lastlibid = None
def _get_copylist_prefs(self,frompref):
return filter( lambda x : x[0] in copylist, frompref.items() )
def _get_prefs(self):
libraryid = get_library_uuid(get_gui().current_db)
if libraryid not in self.all_prefs:
if self.lastlibid == None:
self.all_prefs[libraryid] = dict(self._get_copylist_prefs(self.all_prefs))
else:
self.all_prefs[libraryid] = dict(self._get_copylist_prefs(self.all_prefs[self.lastlibid]))
self.lastlibid = libraryid
return self.all_prefs[libraryid]
def _save_prefs(self,prefs):
libraryid = get_library_uuid(get_gui().current_db)
self.all_prefs[libraryid] = prefs
def __getitem__(self,k):
prefs = self._get_prefs()
if k not in prefs:
# pulls from all_prefs.defaults automatically if not set
# in all_prefs
return self.all_prefs[k]
return prefs[k]
def __setitem__(self,k,v):
prefs = self._get_prefs()
prefs[k]=v
self._save_prefs(prefs)
# to be avoided--can cause unexpected results as possibly ancient
# all_pref settings may be pulled.
def __delitem__(self,k):
prefs = self._get_prefs()
del prefs[k]
self._save_prefs(prefs)
prefs = PrefsFacade(all_prefs)
class ConfigWidget(QWidget):
def __init__(self, plugin_action):
QWidget.__init__(self)
self.plugin_action = plugin_action
self.l = QVBoxLayout()
self.setLayout(self.l)
tab_widget = QTabWidget(self)
self.l.addWidget(tab_widget)
self.basic_tab = BasicTab(self, plugin_action)
tab_widget.addTab(self.basic_tab, 'Basic')
self.personalini_tab = PersonalIniTab(self, plugin_action)
tab_widget.addTab(self.personalini_tab, 'personal.ini')
self.list_tab = ListTab(self, plugin_action)
tab_widget.addTab(self.list_tab, 'Reading Lists')
if 'Reading List' not in plugin_action.gui.iactions:
self.list_tab.setEnabled(False)
self.other_tab = OtherTab(self, plugin_action)
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['updatemeta'] = self.basic_tab.updatemeta.isChecked()
prefs['keeptags'] = self.basic_tab.keeptags.isChecked()
prefs['urlsfromclip'] = self.basic_tab.urlsfromclip.isChecked()
prefs['updatedefault'] = self.basic_tab.updatedefault.isChecked()
prefs['deleteotherforms'] = self.basic_tab.deleteotherforms.isChecked()
if self.list_tab:
# lists
prefs['send_lists'] = ', '.join(map( lambda x : x.strip(), filter( lambda x : x.strip() != '', unicode(self.list_tab.send_lists_box.text()).split(','))))
prefs['read_lists'] = ', '.join(map( lambda x : x.strip(), filter( lambda x : x.strip() != '', unicode(self.list_tab.read_lists_box.text()).split(','))))
# print("send_lists: %s"%prefs['send_lists'])
# print("read_lists: %s"%prefs['read_lists'])
prefs['addtolists'] = self.list_tab.addtolists.isChecked()
prefs['addtoreadlists'] = self.list_tab.addtoreadlists.isChecked()
prefs['addtolistsonread'] = self.list_tab.addtolistsonread.isChecked()
# personal.ini
ini = unicode(self.personalini_tab.ini.toPlainText())
if ini:
prefs['personal.ini'] = ini
else:
# if they've removed everything, reset to default.
prefs['personal.ini'] = get_resources('plugin-example.ini')
def edit_shortcuts(self):
self.save_settings()
# Force the menus to be rebuilt immediately, so we have all our actions registered
self.plugin_action.rebuild_menus()
d = KeyboardConfigDialog(self.plugin_action.gui, self.plugin_action.action_spec[0])
if d.exec_() == d.Accepted:
self.plugin_action.gui.keyboard.finalize()
class BasicTab(QWidget):
def __init__(self, parent_dialog, plugin_action):
self.parent_dialog = parent_dialog
self.plugin_action = plugin_action
QWidget.__init__(self)
self.l = QVBoxLayout()
self.setLayout(self.l)
horz = QHBoxLayout()
label = QLabel('Default Output &Format:')
horz.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.activated.connect(self.set_collisions)
label.setBuddy(self.fileform)
horz.addWidget(self.fileform)
self.l.addLayout(horz)
horz = QHBoxLayout()
label = QLabel('Default If Story Already Exists?')
label.setToolTip("What to do if there's already an existing story with the same title and author.")
horz.addWidget(label)
self.collision = QComboBox(self)
# add collision options
self.set_collisions()
i = self.collision.findText(prefs['collision'])
if i > -1:
self.collision.setCurrentIndex(i)
# self.collision.setToolTip('Overwrite will replace the existing story. Add New will create a new story with the same title and author.')
label.setBuddy(self.collision)
horz.addWidget(self.collision)
self.l.addLayout(horz)
self.updatemeta = QCheckBox('Default Update Calibre &Metadata?',self)
self.updatemeta.setToolTip('Update title, author, URL, tags, etc for story in Calibre from web site.')
self.updatemeta.setChecked(prefs['updatemeta'])
self.l.addWidget(self.updatemeta)
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.')
self.keeptags.setChecked(prefs['keeptags'])
self.l.addWidget(self.keeptags)
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.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.l.insertStretch(-1)
def set_collisions(self):
prev=self.collision.currentText()
self.collision.clear()
for o in collision_order:
if self.fileform.currentText() == 'epub' or o not in [UPDATE,UPDATEALWAYS]:
self.collision.addItem(o)
i = self.collision.findText(prev)
if i > -1:
self.collision.setCurrentIndex(i)
def show_defaults(self):
text = get_resources('plugin-defaults.ini')
ShowDefaultsIniDialog(self.windowIcon(),text,self).exec_()
class PersonalIniTab(QWidget):
def __init__(self, parent_dialog, plugin_action):
self.parent_dialog = parent_dialog
self.plugin_action = plugin_action
QWidget.__init__(self)
self.l = QVBoxLayout()
self.setLayout(self.l)
self.label = QLabel('personal.ini:')
self.l.addWidget(self.label)
self.ini = QTextEdit(self)
self.ini.setLineWrapMode(QTextEdit.NoWrap)
self.ini.setText(prefs['personal.ini'])
self.l.addWidget(self.ini)
self.defaults = QPushButton('View Defaults', 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)
# self.l.insertStretch(-1)
# let edit box fill the space.
def show_defaults(self):
text = get_resources('plugin-defaults.ini')
ShowDefaultsIniDialog(self.windowIcon(),text,self).exec_()
class ShowDefaultsIniDialog(QDialog):
def __init__(self, icon, text, parent=None):
QDialog.__init__(self, parent)
self.resize(600, 500)
self.l = QVBoxLayout()
self.setLayout(self.l)
self.label = QLabel("Plugin Defaults (Read-Only)")
self.label.setToolTip("These all of the plugin's configurable settings\nand their default settings.")
self.setWindowTitle(_('Plugin Defaults'))
self.setWindowIcon(icon)
self.l.addWidget(self.label)
self.ini = QTextEdit(self)
self.ini.setToolTip("These all of the plugin's configurable settings\nand their default settings.")
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.clicked.connect(self.hide)
self.l.addWidget(self.ok_button)
class ListTab(QWidget):
def __init__(self, parent_dialog, plugin_action):
self.parent_dialog = parent_dialog
self.plugin_action = plugin_action
QWidget.__init__(self)
self.l = QVBoxLayout()
self.setLayout(self.l)
try:
rl_plugin = plugin_action.gui.iactions['Reading List']
reading_lists = rl_plugin.get_list_names()
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.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.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.")
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.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.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.")
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.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.setChecked(prefs['addtolistsonread'])
self.l.addWidget(self.addtolistsonread)
self.l.insertStretch(-1)
class OtherTab(QWidget):
def __init__(self, parent_dialog, plugin_action):
self.parent_dialog = parent_dialog
self.plugin_action = plugin_action
QWidget.__init__(self)
self.l = QVBoxLayout()
self.setLayout(self.l)
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.clicked.connect(self.reset_dialogs)
self.l.addWidget(reset_confirmation_button)
self.l.insertStretch(-1)
def reset_dialogs(self):
for key in dynamic.keys():
if key.startswith('fanfictiondownloader_') and key.endswith('_again') \
and dynamic[key] is False:
dynamic[key] = True
info_dialog(self, _('Done'),
_('Confirmation dialogs have all been reset'), show=True)
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2012, Jim Miller'
__docformat__ = 'restructuredtext en'
from zipfile import ZipFile
from xml.dom.minidom import parseString
def get_dcsource(inputio):
epub = ZipFile(inputio, 'r')
## Find the .opf file.
container = epub.read("META-INF/container.xml")
containerdom = parseString(container)
rootfilenodelist = containerdom.getElementsByTagName("rootfile")
rootfilename = rootfilenodelist[0].getAttribute("full-path")
metadom = parseString(epub.read(rootfilename))
firstmetadom = metadom.getElementsByTagName("metadata")[0]
try:
source=firstmetadom.getElementsByTagName("dc:source")[0].firstChild.data.encode("utf-8")
except:
source=None
return source
+589
View File
@@ -0,0 +1,589 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2011, Jim Miller'
__docformat__ = 'restructuredtext en'
import traceback
from PyQt4 import QtGui
from PyQt4.Qt import (QDialog, QTableWidget, QMessageBox, QVBoxLayout, QHBoxLayout, QGridLayout,
QPushButton, QProgressDialog, QString, QLabel, QCheckBox, QIcon,
QTextEdit, QLineEdit, QInputDialog, QComboBox, QClipboard, QVariant,
QProgressDialog, QTimer, QDialogButtonBox, QPixmap, Qt,QAbstractItemView )
from calibre.gui2 import error_dialog, warning_dialog, question_dialog, info_dialog
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters,writers,exceptions
from calibre_plugins.fanfictiondownloader_plugin.common_utils \
import (ReadOnlyTableWidgetItem, ReadOnlyTextIconWidgetItem, SizePersistedDialog,
ImageTitleLayout, get_icon)
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,
UPDATEALWAYS,
OVERWRITE,
OVERWRITEALWAYS,
CALIBREONLY,]
class NotGoingToDownload(Exception):
def __init__(self,error,icon='dialog_error.png'):
self.error=error
self.icon=icon
def __str__(self):
return self.error
class AddNewDialog(SizePersistedDialog):
def __init__(self, gui, prefs, icon, url_list_text):
SizePersistedDialog.__init__(self, gui, 'FanFictionDownLoader plugin:add new dialog')
self.gui = gui
self.setMinimumWidth(300)
self.l = QVBoxLayout()
self.setLayout(self.l)
self.setWindowTitle('FanFictionDownLoader')
self.setWindowIcon(icon)
self.l.addWidget(QLabel('Story URL(s), one per line:'))
self.url = QTextEdit(self)
self.url.setToolTip('URLs for stories, one per line.\nWill take URLs from clipboard, but only valid URLs.')
self.url.setLineWrapMode(QTextEdit.NoWrap)
self.url.setText(url_list_text)
self.l.addWidget(self.url)
horz = QHBoxLayout()
label = QLabel('Output &Format:')
horz.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.activated.connect(self.set_collisions)
label.setBuddy(self.fileform)
horz.addWidget(self.fileform)
self.l.addLayout(horz)
horz = QHBoxLayout()
label = QLabel('If Story Already Exists?')
label.setToolTip("What to do if there's already an existing story with the same title and author.")
horz.addWidget(label)
self.collision = QComboBox(self)
# add collision options
self.set_collisions()
i = self.collision.findText(prefs['collision'])
if i > -1:
self.collision.setCurrentIndex(i)
# self.collision.setToolTip(OVERWRITE+' will replace the existing story.\n'+
# UPDATE+' will download new chapters only and add to existing EPUB.\n'+
# ADDNEW+' will create a new story with the same title and author.\n'+
# SKIP+' will not download existing stories.\n'+
# CALIBREONLY+' will not download stories, but will update Calibre metadata.')
label.setBuddy(self.collision)
horz.addWidget(self.collision)
self.l.addLayout(horz)
self.updatemeta = QCheckBox('Update Calibre &Metadata?',self)
self.updatemeta.setToolTip('Update metadata for story in Calibre from web site?')
self.updatemeta.setChecked(prefs['updatemeta'])
self.l.addWidget(self.updatemeta)
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
self.l.addWidget(button_box)
if url_list_text:
button_box.button(QDialogButtonBox.Ok).setFocus()
# restore saved size.
self.resize_dialog()
#self.resize(self.sizeHint())
def set_collisions(self):
prev=self.collision.currentText()
self.collision.clear()
for o in collision_order:
if self.fileform.currentText() == 'epub' or o not in [UPDATE,UPDATEALWAYS]:
self.collision.addItem(o)
i = self.collision.findText(prev)
if i > -1:
self.collision.setCurrentIndex(i)
def get_ffdl_options(self):
return {
'fileform': unicode(self.fileform.currentText()),
'collision': unicode(self.collision.currentText()),
'updatemeta': unicode(self.updatemeta.isChecked()),
}
def get_urlstext(self):
return unicode(self.url.toPlainText())
class UserPassDialog(QDialog):
'''
Need to collect User/Pass for some sites.
'''
def __init__(self, gui, site):
QDialog.__init__(self, gui)
self.gui = gui
self.status=False
self.setWindowTitle('User/Password')
self.l = QGridLayout()
self.setLayout(self.l)
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.user = QLineEdit(self)
self.l.addWidget(self.user,1,1)
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.clicked.connect(self.ok)
self.l.addWidget(self.ok_button,3,0)
self.cancel_button = QPushButton('Cancel', self)
self.cancel_button.clicked.connect(self.cancel)
self.l.addWidget(self.cancel_button,3,1)
self.resize(self.sizeHint())
def ok(self):
self.status=True
self.hide()
def cancel(self):
self.status=False
self.hide()
class MetadataProgressDialog(QProgressDialog):
'''
ProgressDialog displayed while fetching metadata for each story.
'''
def __init__(self, gui,
book_list,
options,
metadata_function,
startdownload_function):
QProgressDialog.__init__(self,
"Fetching metadata for stories...",
QString(), 0, len(book_list), gui)
self.setWindowTitle("Downloading metadata for stories")
self.setMinimumWidth(500)
self.gui = gui
self.book_list = book_list
self.options = options
self.metadata_function = metadata_function
self.startdownload_function = startdownload_function
self.i = 0
## self.do_loop does QTimer.singleShot on self.do_loop also.
## A weird way to do a loop, but that was the example I had.
QTimer.singleShot(0, self.do_loop)
self.exec_()
def updateStatus(self):
self.setLabelText("Fetched metadata for %d of %d"%(self.i+1,len(self.book_list)))
self.setValue(self.i+1)
print(self.labelText())
def do_loop(self):
if self.i == 0:
self.setValue(0)
book = self.book_list[self.i]
try:
## collision spec passed into getadapter by partial from ffdl_plugin
## no retval only if it exists, but collision is SKIP
self.metadata_function(book)
except NotGoingToDownload as d:
book['good']=False
book['comment']=unicode(d)
book['icon'] = d.icon
except Exception as e:
book['good']=False
book['comment']=unicode(e)
print("Exception: %s:%s"%(book,unicode(e)))
traceback.print_exc()
self.updateStatus()
self.i += 1
if self.i >= len(self.book_list) or self.wasCanceled():
return self.do_when_finished()
else:
QTimer.singleShot(0, self.do_loop)
def do_when_finished(self):
self.hide()
self.gui = None
# Queues a job to process these books in the background.
self.startdownload_function(self.book_list)
class AboutDialog(QDialog):
def __init__(self, parent, icon, text):
QDialog.__init__(self, parent)
self.resize(400, 250)
self.l = QGridLayout()
self.setLayout(self.l)
self.logo = QLabel()
self.logo.setMaximumWidth(110)
self.logo.setPixmap(QPixmap(icon.pixmap(100,100)))
self.label = QLabel(text)
self.label.setOpenExternalLinks(True)
self.label.setWordWrap(True)
self.setWindowTitle(_('About FanFictionDownLoader'))
self.setWindowIcon(icon)
self.l.addWidget(self.logo, 0, 0)
self.l.addWidget(self.label, 0, 1)
self.bb = QDialogButtonBox(self)
b = self.bb.addButton(_('OK'), self.bb.AcceptRole)
b.setDefault(True)
self.l.addWidget(self.bb, 2, 0, 1, -1)
self.bb.accepted.connect(self.accept)
class IconWidgetItem(ReadOnlyTextIconWidgetItem):
def __init__(self, text, icon, sort_key):
ReadOnlyTextIconWidgetItem.__init__(self, text, icon)
self.sort_key = sort_key
#Qt uses a simple < check for sorting items, override this to use the sortKey
def __lt__(self, other):
return self.sort_key < other.sort_key
class AuthorTableWidgetItem(ReadOnlyTableWidgetItem):
def __init__(self, text, sort_key):
ReadOnlyTableWidgetItem.__init__(self, text)
self.sort_key = sort_key
#Qt uses a simple < check for sorting items, override this to use the sortKey
def __lt__(self, other):
return self.sort_key < other.sort_key
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.gui = gui
self.setWindowTitle(header)
self.setWindowIcon(icon)
layout = QVBoxLayout(self)
self.setLayout(layout)
title_layout = ImageTitleLayout(self, 'images/icon.png',
header)
layout.addLayout(title_layout)
books_layout = QHBoxLayout()
layout.addLayout(books_layout)
self.books_table = StoryListTableWidget(self)
books_layout.addWidget(self.books_table)
button_layout = QVBoxLayout()
books_layout.addLayout(button_layout)
# self.move_up_button = QtGui.QToolButton(self)
# self.move_up_button.setToolTip('Move selected books up the list')
# self.move_up_button.setIcon(QIcon(I('arrow-up.png')))
# self.move_up_button.clicked.connect(self.books_table.move_rows_up)
# button_layout.addWidget(self.move_up_button)
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.setIcon(get_icon('list_remove.png'))
self.remove_button.clicked.connect(self.remove_from_list)
button_layout.addWidget(self.remove_button)
spacerItem1 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
button_layout.addItem(spacerItem1)
# self.move_down_button = QtGui.QToolButton(self)
# self.move_down_button.setToolTip('Move selected books down the list')
# self.move_down_button.setIcon(QIcon(I('arrow-down.png')))
# self.move_down_button.clicked.connect(self.books_table.move_rows_down)
# button_layout.addWidget(self.move_down_button)
options_layout = QHBoxLayout()
label = QLabel('Output &Format:')
options_layout.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.activated.connect(self.set_collisions)
label.setBuddy(self.fileform)
options_layout.addWidget(self.fileform)
label = QLabel('Update Mode:')
label.setToolTip("What sort of update to perform. May set default from plugin configuration.")
options_layout.addWidget(label)
self.collision = QComboBox(self)
# add collision options
self.set_collisions()
i = self.collision.findText(prefs['collision'])
if i > -1:
self.collision.setCurrentIndex(i)
# self.collision.setToolTip('Overwrite will replace the existing story. Add New will create a new story with the same title and author.')
label.setBuddy(self.collision)
options_layout.addWidget(self.collision)
self.updatemeta = QCheckBox('Update Calibre &Metadata?',self)
self.updatemeta.setToolTip('Update metadata for story in Calibre from web site? May set default from plugin configuration.')
self.updatemeta.setChecked(prefs['updatemeta'])
options_layout.addWidget(self.updatemeta)
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
options_layout.addWidget(button_box)
layout.addLayout(options_layout)
# Cause our dialog size to be restored from prefs or created on first usage
self.resize_dialog()
self.books_table.populate_table(books)
def set_collisions(self):
prev=self.collision.currentText()
self.collision.clear()
for o in collision_order:
if o not in [ADDNEW,SKIP] and \
(self.fileform.currentText() == 'epub' or o not in [UPDATE,UPDATEALWAYS]):
self.collision.addItem(o)
i = self.collision.findText(prev)
if i > -1:
self.collision.setCurrentIndex(i)
def remove_from_list(self):
self.books_table.remove_selected_rows()
def get_books(self):
return self.books_table.get_books()
def get_ffdl_options(self):
return {
'fileform': unicode(self.fileform.currentText()),
'collision': unicode(self.collision.currentText()),
'updatemeta': unicode(self.updatemeta.isChecked()),
}
class DisplayStoryListDialog(SizePersistedDialog):
def __init__(self, gui, header, prefs, icon, books,
label_text='',
save_size_name='FanFictionDownLoader plugin:display list dialog'):
SizePersistedDialog.__init__(self, gui, save_size_name)
# UpdateExistingDialog.__init__(self, gui, header, prefs, icon, books,
# save_size_name='FanFictionDownLoader plugin:display list dialog')
self.gui = gui
self.setWindowTitle(header)
self.setWindowIcon(icon)
layout = QVBoxLayout(self)
self.setLayout(layout)
title_layout = ImageTitleLayout(self, 'images/icon.png',
header)
layout.addLayout(title_layout)
self.books_table = StoryListTableWidget(self)
layout.addWidget(self.books_table)
options_layout = QHBoxLayout()
self.label = QLabel(label_text)
#self.label.setOpenExternalLinks(True)
#self.label.setWordWrap(True)
options_layout.addWidget(self.label)
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
options_layout.addWidget(button_box)
layout.addLayout(options_layout)
# Cause our dialog size to be restored from prefs or created on first usage
self.resize_dialog()
self.books_table.populate_table(books)
def get_books(self):
return self.books_table.get_books()
class StoryListTableWidget(QTableWidget):
def __init__(self, parent):
QTableWidget.__init__(self, parent)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
def populate_table(self, books):
self.clear()
self.setAlternatingRowColors(True)
self.setRowCount(len(books))
header_labels = ['','Title', 'Author', 'URL', 'Comment']
self.setColumnCount(len(header_labels))
self.setHorizontalHeaderLabels(header_labels)
self.horizontalHeader().setStretchLastSection(True)
#self.verticalHeader().setDefaultSectionSize(24)
self.verticalHeader().hide()
self.books={}
for row, book in enumerate(books):
self.populate_table_row(row, book)
self.books[row] = book
# turning True breaks up/down. Do we need either sorting or up/down?
self.setSortingEnabled(True)
self.resizeColumnsToContents()
self.setMinimumColumnWidth(1, 100)
self.setMinimumColumnWidth(2, 100)
self.setMinimumColumnWidth(3, 100)
self.setMinimumSize(300, 0)
# if len(books) > 0:
# self.selectRow(0)
self.sortItems(1)
self.sortItems(0)
def setMinimumColumnWidth(self, col, minimum):
if self.columnWidth(col) < minimum:
self.setColumnWidth(col, minimum)
def populate_table_row(self, row, book):
if book['good']:
icon = get_icon('ok.png')
val = 0
else:
icon = get_icon('minus.png')
val = 1
if 'icon' in book:
icon = get_icon(book['icon'])
status_cell = IconWidgetItem(None,icon,val)
status_cell.setData(Qt.UserRole, QVariant(val))
self.setItem(row, 0, status_cell)
title_cell = ReadOnlyTableWidgetItem(book['title'])
title_cell.setData(Qt.UserRole, QVariant(row))
self.setItem(row, 1, title_cell)
self.setItem(row, 2, AuthorTableWidgetItem(book['author'], book['author_sort']))
url_cell = ReadOnlyTableWidgetItem(book['url'])
#url_cell.setData(Qt.UserRole, QVariant(book['url']))
self.setItem(row, 3, url_cell)
comment_cell = ReadOnlyTableWidgetItem(book['comment'])
#comment_cell.setData(Qt.UserRole, QVariant(book))
self.setItem(row, 4, comment_cell)
def get_books(self):
books = []
#print("=========================\nbooks:%s"%self.books)
for row in range(self.rowCount()):
rnum = self.item(row, 1).data(Qt.UserRole).toPyObject()
book = self.books[rnum]
books.append(book)
return books
def remove_selected_rows(self):
self.setFocus()
rows = self.selectionModel().selectedRows()
if len(rows) == 0:
return
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)
if not confirm(message,'fanfictiondownloader_delete_item', self):
return
first_sel_row = self.currentRow()
for selrow in reversed(rows):
self.removeRow(selrow.row())
if first_sel_row < self.rowCount():
self.select_and_scroll_to_row(first_sel_row)
elif self.rowCount() > 0:
self.select_and_scroll_to_row(first_sel_row - 1)
def select_and_scroll_to_row(self, row):
self.selectRow(row)
self.scrollToItem(self.currentItem())
def move_rows_up(self):
self.setFocus()
rows = self.selectionModel().selectedRows()
if len(rows) == 0:
return
first_sel_row = rows[0].row()
if first_sel_row <= 0:
return
# Workaround for strange selection bug in Qt which "alters" the selection
# in certain circumstances which meant move down only worked properly "once"
selrows = []
for row in rows:
selrows.append(row.row())
selrows.sort()
for selrow in selrows:
self.swap_row_widgets(selrow - 1, selrow + 1)
scroll_to_row = first_sel_row - 1
if scroll_to_row > 0:
scroll_to_row = scroll_to_row - 1
self.scrollToItem(self.item(scroll_to_row, 0))
def move_rows_down(self):
self.setFocus()
rows = self.selectionModel().selectedRows()
if len(rows) == 0:
return
last_sel_row = rows[-1].row()
if last_sel_row == self.rowCount() - 1:
return
# Workaround for strange selection bug in Qt which "alters" the selection
# in certain circumstances which meant move down only worked properly "once"
selrows = []
for row in rows:
selrows.append(row.row())
selrows.sort()
for selrow in reversed(selrows):
self.swap_row_widgets(selrow + 2, selrow)
scroll_to_row = last_sel_row + 1
if scroll_to_row < self.rowCount() - 1:
scroll_to_row = scroll_to_row + 1
self.scrollToItem(self.item(scroll_to_row, 0))
def swap_row_widgets(self, src_row, dest_row):
self.blockSignals(True)
self.insertRow(dest_row)
for col in range(0, self.columnCount()):
self.setItem(dest_row, col, self.takeItem(src_row, col))
self.removeRow(src_row)
self.blockSignals(False)
+943
View File
@@ -0,0 +1,943 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2012, Jim Miller'
__docformat__ = 'restructuredtext en'
import time, os, copy, threading
from ConfigParser import SafeConfigParser
from StringIO import StringIO
from functools import partial
from datetime import datetime
from PyQt4.Qt import (QApplication, QMenu, QToolButton)
from calibre.ptempfile import PersistentTemporaryFile, PersistentTemporaryDirectory, remove_dir
from calibre.ebooks.metadata import MetaInformation, authors_to_string
from calibre.ebooks.metadata.meta import get_metadata
from calibre.gui2 import error_dialog, warning_dialog, question_dialog, info_dialog
from calibre.gui2.dialogs.message_box import ViewLog
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.utils.date import local_tz
# The class that all interface action plugins must inherit from
from calibre.gui2.actions import InterfaceAction
from calibre_plugins.fanfictiondownloader_plugin.common_utils import (set_plugin_icon_resources, get_icon,
create_menu_action_unique, get_library_uuid)
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters, writers, exceptions
from calibre_plugins.fanfictiondownloader_plugin.epubmerge import doMerge
from calibre_plugins.fanfictiondownloader_plugin.dcsource import get_dcsource
from calibre_plugins.fanfictiondownloader_plugin.config import (prefs)
from calibre_plugins.fanfictiondownloader_plugin.dialogs import (
AddNewDialog, UpdateExistingDialog, DisplayStoryListDialog,
MetadataProgressDialog, UserPassDialog, AboutDialog,
OVERWRITE, OVERWRITEALWAYS, UPDATE, UPDATEALWAYS, ADDNEW, SKIP, CALIBREONLY,
NotGoingToDownload )
# because calibre immediately transforms html into zip and don't want
# to have an 'if html'. db.has_format is cool with the case mismatch,
# but if I'm doing it anyway...
formmapping = {
'epub':'EPUB',
'mobi':'MOBI',
'html':'ZIP',
'txt':'TXT'
}
PLUGIN_ICONS = ['images/icon.png']
sendlists = ["Send to Nook", "Send to Kindle", "Send to Droid", "Add to Nook", "Add to Kindle", "Add to Droid"]
readlists = ["000"]
class FanFictionDownLoaderPlugin(InterfaceAction):
name = 'FanFictionDownLoader'
# Declare the main action associated with this plugin
# The keyboard shortcut can be None if you dont want to use a keyboard
# shortcut. Remember that currently calibre has no central management for
# keyboard shortcuts, so try to use an unusual/unused shortcut.
# (text, icon_path, tooltip, keyboard shortcut)
# icon_path isn't in the zip--icon loaded below.
action_spec = (name, None,
'Download FanFiction stories from various web sites', None)
action_type = 'global'
# make button menu drop down only
#popup_type = QToolButton.InstantPopup
def genesis(self):
# This method is called once per plugin, do initial setup here
# Read the plugin icons and store for potential sharing with the config widget
icon_resources = self.load_resources(PLUGIN_ICONS)
set_plugin_icon_resources(self.name, icon_resources)
base = self.interface_action_base_plugin
self.version = base.name+" v%d.%d.%d"%base.version
# Set the icon for this interface action
# The get_icons function is a builtin function defined for all your
# plugin code. It loads icons from the plugin zip file. It returns
# QIcon objects, if you want the actual data, use the analogous
# get_resources builtin function.
# Note that if you are loading more than one icon, for performance, you
# should pass a list of names to get_icons. In this case, get_icons
# will return a dictionary mapping names to QIcons. Names that
# are not found in the zip file will result in null QIcons.
icon = get_icon('images/icon.png')
# The qaction is automatically created from the action_spec defined
# above
self.qaction.setIcon(icon)
# Call function when plugin triggered.
self.qaction.triggered.connect(self.plugin_button)
# Assign our menu to this action
self.menu = QMenu(self.gui)
self.old_actions_unique_map = {}
self.qaction.setMenu(self.menu)
self.menu.aboutToShow.connect(self.about_to_show_menu)
self.menus_lock = threading.RLock()
def initialization_complete(self):
# otherwise configured hot keys won't work until the menu's
# been displayed once.
self.rebuild_menus()
def about_to_show_menu(self):
self.rebuild_menus()
def rebuild_menus(self):
with self.menus_lock:
# Show the config dialog
# The config dialog can also be shown from within
# Preferences->Plugins, which is why the do_user_config
# method is defined on the base plugin class
do_user_config = self.interface_action_base_plugin.do_user_config
self.menu.clear()
self.actions_unique_map = {}
self.add_action = self.create_menu_item_ex(self.menu, '&Add New from URL(s)', image='plus.png',
unique_name='Add New FanFiction Book(s) from URL(s)',
shortcut_name='Add New FanFiction Book(s) from URL(s)',
triggered=self.add_dialog )
self.update_action = self.create_menu_item_ex(self.menu, '&Update Existing FanFiction Book(s)', image='plusplus.png',
unique_name='Update Existing FanFiction Book(s)',
shortcut_name='Update Existing FanFiction Book(s)',
triggered=self.update_existing)
if 'Reading List' in self.gui.iactions and (prefs['addtolists'] or prefs['addtoreadlists']) :
## XXX mod and rebuild menu when lists selected/empty
self.menu.addSeparator()
addmenutxt, rmmenutxt = None, None
if prefs['addtolists'] and prefs['addtoreadlists'] :
addmenutxt = 'Add to "To Read" and "Send to Device" Lists'
if prefs['addtolistsonread']:
rmmenutxt = 'Remove from "To Read" and add to "Send to Device" Lists'
else:
rmmenutxt = 'Remove from "To Read" Lists'
elif prefs['addtolists'] :
addmenutxt = 'Add Selected to "Send to Device" Lists'
elif prefs['addtoreadlists']:
addmenutxt = 'Add to "To Read" Lists'
rmmenutxt = 'Remove from "To Read" Lists'
if addmenutxt:
self.add_send_action = self.create_menu_item_ex(self.menu, addmenutxt, image='plusplus.png',
unique_name=addmenutxt,
shortcut_name=addmenutxt,
triggered=partial(self.update_lists,add=True))
if rmmenutxt:
self.add_remove_action = self.create_menu_item_ex(self.menu, rmmenutxt, image='minusminus.png',
unique_name=rmmenutxt,
shortcut_name=rmmenutxt,
triggered=partial(self.update_lists,add=False))
try:
self.add_send_action.setEnabled( len(self.gui.library_view.get_selected_ids()) > 0 )
except:
pass
try:
self.add_remove_action.setEnabled( len(self.gui.library_view.get_selected_ids()) > 0 )
except:
pass
self.menu.addSeparator()
self.get_list_action = self.create_menu_item_ex(self.menu, 'Get URLs from Selected Books', image='bookmarks.png',
unique_name='Get URLs from Selected Books',
shortcut_name='Get URLs from Selected Books',
triggered=self.get_list_urls)
self.menu.addSeparator()
self.config_action = create_menu_action_unique(self, self.menu, '&Configure Plugin', shortcut=False,
image= 'config.png',
unique_name='Configure FanFictionDownLoader',
shortcut_name='Configure FanFictionDownLoader',
triggered=partial(do_user_config,parent=self.gui))
self.config_action = create_menu_action_unique(self, self.menu, '&About Plugin', shortcut=False,
image= 'images/icon.png',
unique_name='About FanFictionDownLoader',
shortcut_name='About FanFictionDownLoader',
triggered=self.about)
self.update_action.setEnabled( len(self.gui.library_view.get_selected_ids()) > 0 )
self.get_list_action.setEnabled( len(self.gui.library_view.get_selected_ids()) > 0 )
# Before we finalize, make sure we delete any actions for menus that are no longer displayed
for menu_id, unique_name in self.old_actions_unique_map.iteritems():
if menu_id not in self.actions_unique_map:
self.gui.keyboard.unregister_shortcut(unique_name)
self.old_actions_unique_map = self.actions_unique_map
self.gui.keyboard.finalize()
def about(self):
# Get the about text from a file inside the plugin zip file
# The get_resources function is a builtin function defined for all your
# plugin code. It loads files from the plugin zip file. It returns
# the bytes from the specified file.
#
# Note that if you are loading more than one file, for performance, you
# should pass a list of names to get_resources. In this case,
# get_resources will return a dictionary mapping names to bytes. Names that
# are not found in the zip file will not be in the returned dictionary.
text = get_resources('about.txt')
AboutDialog(self.gui,self.qaction.icon(),self.version + text).exec_()
def create_menu_item_ex(self, parent_menu, menu_text, image=None, tooltip=None,
shortcut=None, triggered=None, is_checked=None, shortcut_name=None,
unique_name=None):
ac = create_menu_action_unique(self, parent_menu, menu_text, image, tooltip,
shortcut, triggered, is_checked, shortcut_name, unique_name)
self.actions_unique_map[ac.calibre_shortcut_unique_name] = ac.calibre_shortcut_unique_name
return ac
def plugin_button(self):
if len(self.gui.library_view.get_selected_ids()) > 0 and prefs['updatedefault']:
self.update_existing()
else:
self.add_dialog()
def update_lists(self,add=True):
if len(self.gui.library_view.get_selected_ids()) > 0 and \
(prefs['addtolists'] or prefs['addtoreadlists']) :
self._update_reading_lists(self.gui.library_view.get_selected_ids(),add)
#self.gui.library_view.model().refresh_ids(self.gui.library_view.get_selected_ids())
def get_list_urls(self):
if len(self.gui.library_view.get_selected_ids()) > 0:
url_list = []
for book_id in self.gui.library_view.get_selected_ids():
url = self._get_story_url(self.gui.current_db, book_id)
if url != None:
url_list.append(url)
if url_list:
d = ViewLog(_("List of URLs"),"\n".join(url_list),parent=self.gui)
d.setWindowIcon(get_icon('bookmarks.png'))
d.exec_()
else:
info_dialog(self.gui, _('List of URLs'),
_('No URLs found in selected books.'),
show=True,
show_copy_button=False)
def add_dialog(self):
#print("add_dialog()")
url_list = self.get_urls_clip()
url_list_text = "\n".join(url_list)
# self.gui is the main calibre GUI. It acts as the gateway to access
# all the elements of the calibre user interface, it should also be the
# parent of the dialog
# AddNewDialog just collects URLs, format and presents buttons.
d = AddNewDialog(self.gui,
prefs,
self.qaction.icon(),
url_list_text,
)
d.exec_()
if d.result() != d.Accepted:
return
url_list = get_url_list(d.get_urlstext())
add_books = self._convert_urls_to_books(url_list)
#print("add_books:%s"%add_books)
#print("options:%s"%d.get_ffdl_options())
options = d.get_ffdl_options()
options['version'] = self.version
print(self.version)
self.start_downloads( options, add_books )
def update_existing(self):
#print("update_existing()")
previous = self.gui.library_view.currentIndex()
db = self.gui.current_db
book_ids = self.gui.library_view.get_selected_ids()
books = self._convert_calibre_ids_to_books(db, book_ids)
#print("update books:%s"%books)
d = UpdateExistingDialog(self.gui,
'Update Existing List',
prefs,
self.qaction.icon(),
books,
)
d.exec_()
if d.result() != d.Accepted:
return
update_books = d.get_books()
#print("update_books:%s"%update_books)
#print("options:%s"%d.get_ffdl_options())
# only if there's some good ones.
if 0 < len(filter(lambda x : x['good'], update_books)):
options = d.get_ffdl_options()
options['version'] = self.version
print(self.version)
self.start_downloads( options, update_books )
def get_urls_clip(self):
url_list = []
if prefs['urlsfromclip']:
for url in unicode(QApplication.instance().clipboard().text()).split():
if( self._is_good_downloader_url(url) ):
url_list.append(url)
return url_list
def apply_settings(self):
# No need to do anything with perfs here, but we could.
prefs
def start_downloads(self, options, books):
#print("start_downloads:%s"%books)
# create and pass temp dir.
tdir = PersistentTemporaryDirectory(prefix='fanfictiondownloader_')
options['tdir']=tdir
self.gui.status_bar.show_message(_('Started fetching metadata for %s stories.'%len(books)), 3000)
MetadataProgressDialog(self.gui,
books,
options,
partial(self.get_metadata_for_book, options = options),
partial(self.start_download_list, options = options))
# MetadataProgressDialog calls get_metadata_for_book for each 'good' story,
# get_metadata_for_book updates book for each,
# MetadataProgressDialog calls start_download_list at the end which goes
# into the BG, or shows list if no 'good' books.
def get_metadata_for_book(self,book,
options={'fileform':'epub',
'collision':ADDNEW,
'updatemeta':True}):
'''
Update passed in book dict with metadata from website and
necessary data. To be called from MetadataProgressDialog
'loop'. Also pops dialogs for is adult, user/pass.
'''
# The current database shown in the GUI
# db is an instance of the class LibraryDatabase2 from database.py
# This class has many, many methods that allow you to do a lot of
# things.
db = self.gui.current_db
fileform = options['fileform']
collision = options['collision']
updatemeta= options['updatemeta']
if not book['good']:
# book has already been flagged bad for whatever reason.
return
url = book['url']
print("url:%s"%url)
skip_date_update = False
## was self.ffdlconfig, but we need to be able to change it
## when doing epub update.
ffdlconfig = SafeConfigParser()
ffdlconfig.readfp(StringIO(get_resources("plugin-defaults.ini")))
ffdlconfig.readfp(StringIO(prefs['personal.ini']))
adapter = adapters.getAdapter(ffdlconfig,url)
options['personal.ini'] = prefs['personal.ini']
## three tries, that's enough if both user/pass & is_adult needed,
## or a couple tries of one or the other
for x in range(0,2):
try:
adapter.getStoryMetadataOnly()
except exceptions.FailedToLogin:
print("Login Failed, Need Username/Password.")
userpass = UserPassDialog(self.gui,url)
userpass.exec_() # exec_ will make it act modal
if userpass.status:
adapter.username = userpass.user.text()
adapter.password = userpass.passwd.text()
except exceptions.AdultCheckRequired:
if question_dialog(self.gui, 'Are You Adult?', '<p>'+
"%s requires that you be an adult. Please confirm you are an adult in your locale:"%url,
show_copy_button=False):
adapter.is_adult=True
# let other exceptions percolate up.
story = adapter.getStoryMetadataOnly()
writer = writers.getWriter(options['fileform'],adapter.config,adapter)
book['title'] = story.getMetadata("title", removeallentities=True)
book['author_sort'] = book['author'] = story.getMetadata("author", removeallentities=True)
book['publisher'] = story.getMetadata("site")
book['tags'] = writer.getTags()
book['comments'] = story.getMetadata("description") #, removeallentities=True) comments handles entities better.
# adapter.opener is the element with a threadlock. But del
# adapter.opener doesn't work--subproc fails when it tries
# to pull in the adapter object that hasn't been imported yet.
# book['adapter'] = adapter
book['is_adult'] = adapter.is_adult
book['username'] = adapter.username
book['password'] = adapter.password
book['icon'] = 'plus.png'
book['pubdate'] = story.getMetadataRaw('datePublished').replace(tzinfo=local_tz)
book['timestamp'] = None # filled below if not skipped.
if collision in (CALIBREONLY):
book['icon'] = 'metadata.png'
# Dialogs should prevent this case now.
if collision in (UPDATE,UPDATEALWAYS) and fileform != 'epub':
raise NotGoingToDownload("Cannot update non-epub format.")
book_id = None
if book['calibre_id'] != None:
# updating an existing book. Update mode applies.
print("update existing id:%s"%book['calibre_id'])
book_id = book['calibre_id']
# No handling needed: OVERWRITEALWAYS,CALIBREONLY
# only care about collisions when not ADDNEW
elif collision != ADDNEW:
# 'new' book from URL. collision handling applies.
print("from URL")
# find dups
mi = MetaInformation(story.getMetadata("title", removeallentities=True),
(story.getMetadata("author", removeallentities=True),)) # author is a list.
identicalbooks = db.find_identical_books(mi)
## removed for being overkill.
# for ib in identicalbooks:
# # only *really* identical if URL matches, too.
# # XXX make an option?
# if self._get_story_url(db,ib) == url:
# identicalbooks.append(ib)
#print("identicalbooks:%s"%identicalbooks)
if collision == SKIP and identicalbooks:
raise NotGoingToDownload("Skipping duplicate story.","list_remove.png")
if len(identicalbooks) > 1:
raise NotGoingToDownload("More than one identical book--can't tell which to update/overwrite.","minusminus.png")
if collision == CALIBREONLY and not identicalbooks:
raise NotGoingToDownload("Not updating Calibre Metadata, no existing book to update.","search_delete_saved.png")
if len(identicalbooks)>0:
book_id = identicalbooks.pop()
book['calibre_id'] = book_id
book['icon'] = 'edit-redo.png'
if book_id != None and collision != ADDNEW:
if options['collision'] in (CALIBREONLY):
book['comment'] = 'Metadata collected.'
# don't need temp file created below.
return
## newer/chaptercount checks are the same for both:
# Update epub, but only if more chapters.
if collision in (UPDATE,UPDATEALWAYS): # collision == UPDATE
# 'book' can exist without epub. If there's no existing epub,
# let it go and it will download it.
if db.has_format(book_id,fileform,index_is_id=True):
toupdateio = StringIO()
(epuburl,chaptercount) = doMerge(toupdateio,
[StringIO(db.format(book_id,'EPUB',
index_is_id=True))],
titlenavpoints=False,
striptitletoc=True,
forceunique=False)
urlchaptercount = int(story.getMetadata('numChapters'))
if chaptercount == urlchaptercount:
if collision == UPDATE:
raise NotGoingToDownload("Already contains %d chapters."%chaptercount,'edit-undo.png')
else:
# UPDATEALWAYS
skip_date_update = True
elif chaptercount > urlchaptercount:
raise NotGoingToDownload("Existing epub contains %d chapters, web site only has %d. Use Overwrite to force update." % (chaptercount,urlchaptercount),'dialog_error.png')
if collision == OVERWRITE and \
db.has_format(book_id,formmapping[fileform],index_is_id=True):
# check make sure incoming is newer.
lastupdated=story.getMetadataRaw('dateUpdated').date()
fileupdated=datetime.fromtimestamp(os.stat(db.format_abspath(book_id, formmapping[fileform], index_is_id=True))[8]).date()
if fileupdated > lastupdated:
raise NotGoingToDownload("Not Overwriting, web site is not newer.",'edit-undo.png')
# For update, provide a tmp file copy of the existing epub so
# it can't change underneath us.
if collision in (UPDATE,UPDATEALWAYS) and \
db.has_format(book['calibre_id'],'EPUB',index_is_id=True):
tmp = PersistentTemporaryFile(prefix='old-%s-'%book['calibre_id'],
suffix='.epub',
dir=options['tdir'])
db.copy_format_to(book_id,fileform,tmp,index_is_id=True)
print("existing epub tmp:"+tmp.name)
book['epub_for_update'] = tmp.name
if collision != CALIBREONLY and not skip_date_update:
# I'm half convinced this should be dateUpdated instead, but
# this behavior matches how epubs come out when imported
# dateCreated == packaged--epub/etc created.
book['timestamp'] = story.getMetadataRaw('dateCreated').replace(tzinfo=local_tz)
if book['good']: # there shouldn't be any !'good' books at this point.
# if still 'good', make a temp file to write the output to.
tmp = PersistentTemporaryFile(prefix='new-%s-'%book['calibre_id'],
suffix='.'+options['fileform'],
dir=options['tdir'])
print("title:"+book['title'])
print("outfile:"+tmp.name)
book['outfile'] = tmp.name
return
def start_download_list(self,book_list,
options={'fileform':'epub',
'collision':ADDNEW,
'updatemeta':True}):
'''
Called by MetadataProgressDialog to start story downloads BG processing.
adapter_list is a list of tuples of (url,adapter)
'''
#print("start_download_list:book_list:%s"%book_list)
## No need to BG process when CALIBREONLY! Fake it.
if options['collision'] in (CALIBREONLY):
class NotJob(object):
def __init__(self,result):
self.failed=False
self.result=result
notjob = NotJob(book_list)
self.download_list_completed(notjob,options=options)
return
# ## XXX show list before starting download.
# d = DisplayStoryListDialog(self.gui,
# 'Download List',
# prefs,
# self.qaction.icon(),
# book_list,
# label_text='Status of stories to be downloaded'
# )
# d.exec_()
# if d.result() != d.Accepted:
# return
for book in book_list:
if book['good']:
break
else:
## No good stories to try to download, go straight to
## list.
d = DisplayStoryListDialog(self.gui,
'Nothing to Download',
prefs,
self.qaction.icon(),
book_list,
label_text='None of the URLs/stories given can be/need to be downloaded.'
)
d.exec_()
return
func = 'arbitrary_n'
cpus = self.gui.job_manager.server.pool_size
args = ['calibre_plugins.fanfictiondownloader_plugin.jobs', 'do_download_worker',
(book_list, options, cpus)]
desc = 'Download FanFiction Book'
job = self.gui.job_manager.run_job(
self.Dispatcher(partial(self.download_list_completed,options=options)),
func, args=args,
description=desc)
self.gui.status_bar.show_message('Starting %d FanFictionDownLoads'%len(book_list),3000)
def download_list_completed(self, job, options={}):
if job.failed:
self.gui.job_exception(job, dialog_title='Failed to Download Stories')
return
previous = self.gui.library_view.currentIndex()
db = self.gui.current_db
# XXX Switch this to a calibre standard confirm that has a
# 'don't show this anymore' checkbox. (But only if all good?)
d = DisplayStoryListDialog(self.gui,
'Downloads finished, confirm to update Calibre',
prefs,
self.qaction.icon(),
job.result,
label_text='Stories will not be added or updated in Calibre without confirmation.'
)
d.exec_()
if d.result() == d.Accepted:
## in case the user removed any from the list.
book_list = d.get_books()
good_list = filter(lambda x : x['good'], book_list)
total_good = len(good_list)
self.gui.status_bar.show_message(_('Adding/Updating %s books.'%total_good), 3000)
for book in good_list:
print("add/update %s %s"%(book['title'],book['url']))
mi = self._make_mi_from_book(book)
if options['collision'] != CALIBREONLY:
self._add_or_update_book(book,options,prefs,mi)
if options['collision'] == CALIBREONLY or \
(options['updatemeta'] and book['good']) :
self._update_metadata(db, book['calibre_id'], book, mi)
add_list = filter(lambda x : x['good'] and x['added'], book_list)
update_list = filter(lambda x : x['good'] and not x['added'], book_list)
update_ids = [ x['calibre_id'] for x in update_list ]
if len(add_list):
## even shows up added to searchs. Nice.
self.gui.library_view.model().books_added(len(add_list))
if update_ids:
self.gui.library_view.model().refresh_ids(update_ids)
current = self.gui.library_view.currentIndex()
self.gui.library_view.model().current_changed(current, previous)
self.gui.tags_view.recount()
self.gui.status_bar.show_message(_('Finished Adding/Updating %d books.'%(len(update_list) + len(add_list))), 3000)
if len(update_list) + len(add_list) != total_good:
d = DisplayStoryListDialog(self.gui,
'Updates completed, final status',
prefs,
self.qaction.icon(),
book_list,
label_text='Stories have be added or updated in Calibre, some had additional problems.'
)
d.exec_()
print("all done, remove temp dir.")
remove_dir(options['tdir'])
def _add_or_update_book(self,book,options,prefs,mi=None):
db = self.gui.current_db
if mi == None:
mi = self._make_mi_from_book(book)
book_id = book['calibre_id']
if book_id == None:
book_id = db.create_book_entry(mi,
add_duplicates=True)
book['calibre_id'] = book_id
book['added'] = True
else:
book['added'] = False
if not db.add_format_with_hooks(book_id,
options['fileform'],
book['outfile'], index_is_id=True):
book['comment'] = "Adding format to book failed for some reason..."
book['good']=False
book['icon']='dialog_error.png'
if prefs['deleteotherforms']:
fmts = db.formats(book['calibre_id'], index_is_id=True).split(',')
for fmt in fmts:
if fmt != formmapping[options['fileform']]:
print("remove f:"+fmt)
db.remove_format(book['calibre_id'], fmt, index_is_id=True)#, notify=False
if prefs['addtolists'] or prefs['addtoreadlists']:
self._update_reading_lists([book_id],add=True)
return book_id
def _update_metadata(self, db, book_id, book, mi):
if prefs['keeptags']:
old_tags = db.get_tags(book_id)
# remove old Completed/In-Progress only if there's a new one.
if 'Completed' in mi.tags or 'In-Progress' in mi.tags:
old_tags = filter( lambda x : x not in ('Completed', 'In-Progress'), old_tags)
# remove old Last Update tags if there are new ones.
if len(filter( lambda x : not x.startswith("Last Update"), mi.tags)) > 0:
old_tags = filter( lambda x : not x.startswith("Last Update"), old_tags)
# mi.tags needs to be list, but set kills dups.
mi.tags = list(set(list(old_tags)+mi.tags))
# Set language english, but only if not already set.
oldmi = db.get_metadata(book_id,index_is_id=True)
if not oldmi.languages:
mi.languages=['eng']
db.set_metadata(book_id,mi)
def _get_clean_reading_lists(self,lists):
if lists == None or lists.strip() == "" :
return []
else:
return filter( lambda x : x, map( lambda x : x.strip(), lists.split(',') ) )
def _update_reading_lists(self,book_ids,add=True):
try:
rl_plugin = self.gui.iactions['Reading List']
except:
if prefs['addtolists'] or prefs['addtoreadlists']:
message="<p>You configured FanFictionDownLoader to automatically update Reading Lists, but you don't have the Reading List plugin installed anymore?</p>"
confirm(message,'fanfictiondownloader_no_reading_list_plugin', self.gui)
return
# XXX check for existence of lists, warning if not.
if prefs['addtoreadlists']:
if add:
addremovefunc = rl_plugin.add_books_to_list
else:
addremovefunc = rl_plugin.remove_books_from_list
lists = self._get_clean_reading_lists(prefs['read_lists'])
if len(lists) < 1 :
message="<p>You configured FanFictionDownLoader to automatically update \"To Read\" Reading Lists, but you don't have any lists set?</p>"
confirm(message,'fanfictiondownloader_no_read_lists', self.gui)
for l in lists:
if l in rl_plugin.get_list_names():
#print("add good read l:(%s)"%l)
addremovefunc(l,
book_ids,
display_warnings=False)
else:
if l != '':
message="<p>You configured FanFictionDownLoader to automatically update Reading List '%s', but you don't have a list of that name?</p>"%l
confirm(message,'fanfictiondownloader_no_reading_list_%s'%l, self.gui)
if prefs['addtolists'] and (add or (prefs['addtolistsonread'] and prefs['addtoreadlists']) ):
lists = self._get_clean_reading_lists(prefs['send_lists'])
if len(lists) < 1 :
message="<p>You configured FanFictionDownLoader to automatically update \"Send to Device\" Reading Lists, but you don't have any lists set?</p>"
confirm(message,'fanfictiondownloader_no_send_lists', self.gui)
for l in lists:
if l in rl_plugin.get_list_names():
#print("good send l:(%s)"%l)
rl_plugin.add_books_to_list(l,
book_ids,
display_warnings=False)
else:
if l != '':
message="<p>You configured FanFictionDownLoader to automatically update Reading List '%s', but you don't have a list of that name?</p>"%l
confirm(message,'fanfictiondownloader_no_reading_list_%s'%l, self.gui)
def _find_existing_book_id(self,db,book,matchurl=True):
mi = MetaInformation(book["title"],(book["author"],)) # author is a list.
identicalbooks = db.find_identical_books(mi)
if matchurl: # only *really* identical if URL matches, too.
for ib in identicalbooks:
if self._get_story_url(db,ib) == book['url']:
return ib
if identicalbooks:
return identicalbooks.pop()
return None
def _make_mi_from_book(self,book):
mi = MetaInformation(book['title'],(book['author'],)) # author is a list.
mi.set_identifiers({'url':book['url']})
mi.publisher = book['publisher']
mi.tags = book['tags']
#mi.languages = ['en'] # handled in _update_metadata so it can check for existing lang.
mi.pubdate = book['pubdate']
mi.timestamp = book['timestamp']
mi.comments = book['comments']
return mi
def _convert_urls_to_books(self, urls):
books = []
uniqueurls = set()
for url in urls:
book = self._convert_url_to_book(url)
if book['url'] in uniqueurls:
book['good'] = False
book['comment'] = "Same story already included."
uniqueurls.add(book['url'])
books.append(book)
return books
def _convert_url_to_book(self, url):
book = {}
book['good'] = True
book['calibre_id'] = None
book['title'] = 'Unknown'
book['author'] = 'Unknown'
book['author_sort'] = 'Unknown'
book['comment'] = ''
book['url'] = ''
book['added'] = False
self._set_book_url_and_comment(book,url)
return book
def _convert_calibre_ids_to_books(self, db, ids):
books = []
for book_id in ids:
books.append(self._convert_calibre_id_to_book(db,book_id))
return books
def _convert_calibre_id_to_book(self, db, book_id):
mi = db.get_metadata(book_id, index_is_id=True)
book = {}
book['good'] = True
book['calibre_id'] = mi.id
book['title'] = mi.title
book['author'] = authors_to_string(mi.authors)
book['author_sort'] = mi.author_sort
# book['series'] = mi.series
# if mi.series:
# book['series_index'] = mi.series_index
# else:
# book['series_index'] = 0
book['comment'] = ''
book['url'] = ""
book['added'] = False
url = self._get_story_url(db,book_id)
self._set_book_url_and_comment(book,url)
return book
def _set_book_url_and_comment(self,book,url):
if not url:
book['comment'] = "No story URL found."
book['good'] = False
book['icon'] = 'search_delete_saved.png'
else:
# get normalized url or None.
book['url'] = self._is_good_downloader_url(url)
if book['url'] == None:
book['url'] = url
book['comment'] = "URL is not a valid story URL."
book['good'] = False
book['icon']='dialog_error.png'
def _get_story_url(self, db, book_id):
identifiers = db.get_identifiers(book_id,index_is_id=True)
if 'url' in identifiers:
# identifiers have :->| in url.
#print("url from book:"+identifiers['url'].replace('|',':'))
return identifiers['url'].replace('|',':')
else:
## only epub has URL in it--at least where I can easily find it.
if db.has_format(book_id,'EPUB',index_is_id=True):
existingepub = db.format(book_id,'EPUB',index_is_id=True, as_file=True)
mi = get_metadata(existingepub,'EPUB')
identifiers = mi.get_identifiers()
if 'url' in identifiers:
#print("url from epub:"+identifiers['url'].replace('|',':'))
return identifiers['url'].replace('|',':')
# look for dc:source
return get_dcsource(existingepub)
return None
def _is_good_downloader_url(self,url):
# this is the accepted way to 'check for existance'? really?
try:
self.dummyconfig
except AttributeError:
self.dummyconfig = SafeConfigParser()
# pulling up an adapter is pretty low over-head. If
# it fails, it's a bad url.
try:
adapter = adapters.getAdapter(self.dummyconfig,url)
url = adapter.url
del adapter
return url
except:
return None;
def get_job_details(job):
'''
Convert the job result into a set of parameters including a detail message
summarising the success of the extraction operation.
This is used by both the threaded and worker approaches to extraction
'''
extracted_ids, same_isbn_ids, failed_ids, no_format_ids = job.result
if not hasattr(job, 'html_details'):
job.html_details = job.details
det_msg = []
for i, title in failed_ids:
if i in no_format_ids:
msg = title + ' (No formats)'
else:
msg = title + ' (ISBN not found)'
det_msg.append(msg)
if same_isbn_ids:
if det_msg:
det_msg.append('----------------------------------')
for i, title in same_isbn_ids:
msg = title + ' (Same ISBN)'
det_msg.append(msg)
if len(extracted_ids) > 0:
if det_msg:
det_msg.append('----------------------------------')
for i, title, last_modified, isbn in extracted_ids:
msg = '%s (Extracted %s)'%(title, isbn)
det_msg.append(msg)
det_msg = '\n'.join(det_msg)
return extracted_ids, same_isbn_ids, failed_ids, det_msg
def get_url_list(urls):
def f(x):
if x.strip(): return True
else: return False
# set removes dups.
return set(filter(f,urls.strip().splitlines()))
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.
+188
View File
@@ -0,0 +1,188 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2012, Jim Miller'
__copyright__ = '2011, Grant Drake <grant.drake@gmail.com>'
__docformat__ = 'restructuredtext en'
import time, os, traceback
from ConfigParser import SafeConfigParser
from StringIO import StringIO
#from itertools import izip
#from threading import Event
#from calibre.gui2.convert.single import sort_formats_by_preference
from calibre.utils.ipc.server import Server
from calibre.utils.ipc.job import ParallelJob
from calibre.utils.logging import Log
from calibre_plugins.fanfictiondownloader_plugin.dialogs import (NotGoingToDownload,
OVERWRITE, OVERWRITEALWAYS, UPDATE, UPDATEALWAYS, ADDNEW, SKIP, CALIBREONLY)
from calibre_plugins.fanfictiondownloader_plugin.fanficdownloader import adapters, writers, exceptions
from calibre_plugins.fanfictiondownloader_plugin.epubmerge import doMerge
# ------------------------------------------------------------------------------
#
# Functions to perform downloads using worker jobs
#
# ------------------------------------------------------------------------------
def do_download_worker(book_list, options,
cpus, notification=lambda x,y:x):
'''
Master job, to launch child jobs to extract ISBN for a set of books
This is run as a worker job in the background to keep the UI more
responsive and get around the memory leak issues as it will launch
a child job for each book as a worker process
'''
server = Server(pool_size=cpus)
print(options['version'])
total = 0
# Queue all the jobs
print("Adding jobs for URLs:")
for book in book_list:
if book['good']:
print("%s"%book['url'])
total += 1
args = ['calibre_plugins.fanfictiondownloader_plugin.jobs',
'do_download_for_worker',
(book,options)]
job = ParallelJob('arbitrary',
"url:(%s) id:(%s)"%(book['url'],book['calibre_id']),
done=None,
args=args)
job._book = book
# job._book_id = book_id
# job._title = title
# job._modified_date = modified_date
# job._existing_isbn = existing_isbn
server.add_job(job)
# This server is an arbitrary_n job, so there is a notifier available.
# Set the % complete to a small number to avoid the 'unavailable' indicator
notification(0.01, 'Downloading FanFiction Stories')
# dequeue the job results as they arrive, saving the results
count = 0
while True:
job = server.changed_jobs_queue.get()
# A job can 'change' when it is not finished, for example if it
# produces a notification. Ignore these.
job.update()
if not job.is_finished:
continue
# A job really finished. Get the information.
output_book = job.result
#print("output_book:%s"%output_book)
book_list.remove(job._book)
book_list.append(job.result)
book_id = job._book['calibre_id']
#title = job._title
count = count + 1
notification(float(count)/total, 'Downloaded Story')
# Add this job's output to the current log
print('Logfile for book ID %s (%s)'%(book_id, job._book['title']))
print(job.details)
if count >= total:
# All done!
break
server.close()
# return the book list as the job result
return book_list
def do_download_for_worker(book,options):
'''
Child job, to extract isbn from formats for this specific book,
when run as a worker job
'''
try:
book['comment'] = 'Download started...'
ffdlconfig = SafeConfigParser()
ffdlconfig.readfp(StringIO(get_resources("plugin-defaults.ini")))
ffdlconfig.readfp(StringIO(options['personal.ini']))
adapter = adapters.getAdapter(ffdlconfig,book['url'])
adapter.is_adult = book['is_adult']
adapter.username = book['username']
adapter.password = book['password']
story = adapter.getStoryMetadataOnly()
writer = writers.getWriter(options['fileform'],adapter.config,adapter)
outfile = book['outfile']
## 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...")
book['comment'] = 'Metadata collected.'
## checks were done earlier, it's new or not dup or newer--just write it.
elif options['collision'] in (ADDNEW, SKIP, OVERWRITE, OVERWRITEALWAYS) or \
('epub_for_update' not in book and options['collision'] in (UPDATE, UPDATEALWAYS)):
print("write to %s"%outfile)
writer.writeStory(outfilename=outfile, forceOverwrite=True)
book['comment'] = 'Download %s completed, %s chapters.'%(options['fileform'],story.getMetadata("numChapters"))
## checks were done earlier, just update it.
elif 'epub_for_update' in book and options['collision'] in (UPDATE, UPDATEALWAYS):
urlchaptercount = int(story.getMetadata('numChapters'))
## First, get existing epub with titlepage and tocpage stripped.
updateio = StringIO()
(epuburl,chaptercount) = doMerge(updateio,
[book['epub_for_update']],
titlenavpoints=False,
striptitletoc=True,
forceunique=False)
print("Do update - epub(%d) vs url(%d)" % (chaptercount, urlchaptercount))
print("write to %s"%outfile)
## Get updated title page/metadata by itself in an epub.
## Even if the title page isn't included, this carries the metadata.
titleio = StringIO()
writer.writeStory(outstream=titleio,metaonly=True)
newchaptersio = None
if urlchaptercount > chaptercount :
## Go get the new chapters
newchaptersio = StringIO()
adapter.setChaptersRange(chaptercount+1,urlchaptercount)
adapter.config.set("overrides",'include_tocpage','false')
adapter.config.set("overrides",'include_titlepage','false')
writer.writeStory(outstream=newchaptersio)
## Merge the three epubs together.
doMerge(outfile,
[titleio,updateio,newchaptersio],
fromfirst=True,
titlenavpoints=False,
striptitletoc=False,
forceunique=False)
book['comment'] = 'Update %s completed, added %s chapters for %s total.'%\
(options['fileform'],(urlchaptercount-chaptercount),urlchaptercount)
except NotGoingToDownload as d:
book['good']=False
book['comment']=unicode(d)
book['icon'] = d.icon
except Exception as e:
book['good']=False
book['comment']=unicode(e)
book['icon']='dialog_error.png'
print("Exception: %s:%s"%(book,unicode(e)))
traceback.print_exc()
#time.sleep(10)
return book
+5 -3
View File
@@ -3,6 +3,8 @@ cron:
url: /r3m0v3r
schedule: every 2 hours
- description: orphan cleanup job
url: /r3m0v3rOrphans
schedule: every 4 hours
# There's a bug in the Python 2.7 runtime that prevents this from
# working properly. In theory, there should never be orphans anyway.
#- description: orphan cleanup job
# url: /r3m0v3rOrphans
# schedule: every 4 hours
+22 -4
View File
@@ -88,8 +88,10 @@ include_tocpage: true
#output_filename: books/${title}-${siteabbrev}_${storyId}${formatext}
#output_filename: books/${formatname}/${siteabbrev}/${authorId}/${title}-${siteabbrev}_${storyId}${formatext}
output_filename: ${title}-${siteabbrev}_${storyId}${formatext}
## Make directories as needed.
make_directories: true
## Always overwrite output files. Otherwise, the downloader checks
## the timestamp on the existing file and only overwrites if the story
## has been updated more recently. Command line version only
@@ -97,6 +99,7 @@ make_directories: true
## put output (with output_filename) in a zip file zip_filename.
zip_output: false
## Can include directories. .zip will be added if not in name somewhere
zip_filename: ${title}-${siteabbrev}_${storyId}${formatext}.zip
@@ -105,6 +108,10 @@ zip_filename: ${title}-${siteabbrev}_${storyId}${formatext}.zip
## zip_filename.
allow_unsafe_filename: false
## entries to make epub subjects and calibre tags
## lastupdate creates two tags: "Last Update Year/Month: %Y/%m" and "Last Update: %Y/%m/%d"
include_subject_tags: extratags, genre, category, characters, lastupdate, status
## extra tags (comma separated) to include, primarily for epub.
extratags: FanFiction
@@ -113,6 +120,15 @@ extratags: FanFiction
## Primarily for commandline.
#slow_down_sleep_time:0.5
## output background color--only used by html and epub (and ignored in
## epub by many readers). Must be hex code, # will be added.
background_color: ffffff
## For use only with stand-alone CLI version--run a command on the
## generated file after it's produced. All of the titlepage_entries
## values are available, plus output_filename.
#post_process_cmd: addbook -f "${output_filename}" -t "${title}"
## Each output format has a section that overrides [defaults]
[html]
@@ -128,10 +144,6 @@ windows_eol: true
## epub is already a zip file.
zip_output: false
## entries to make epub subject tags
## lastupdate creates two tags: "Last Update Year/Month: %Y/%m" and "Last Update: %Y/%m/%d"
include_subject_tags: extratags, genre, category, characters, lastupdate, status
## epub carries the TOC in metadata.
## mobi generated from epub will have a TOC at the end.
include_tocpage: false
@@ -245,6 +257,12 @@ output_filename: ${title}-${siteabbrev}_${authorId}_${storyId}${formatext}
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[www.tthfanfic.org]
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[overrides]
## It may sometimes be useful to override all of the specific format,
## site and site:format sections in your private configuration. For
+39 -28
View File
@@ -24,6 +24,9 @@ from os.path import normpath, expanduser, isfile, join
from StringIO import StringIO
from optparse import OptionParser
import getpass
import string
from subprocess import call
from epubmerge import doMerge
@@ -38,7 +41,9 @@ import ConfigParser
def writeStory(config,adapter,writeformat,metaonly=False,outstream=None):
writer = writers.getWriter(writeformat,config,adapter)
writer.writeStory(outstream=outstream,metaonly=metaonly)
output_filename=writer.getOutputFileName()
del writer
return output_filename
def main():
@@ -65,7 +70,7 @@ def main():
help="Update an existing epub with new chapter, give epub filename instead of storyurl. Not compatible with inserted TOC.",)
parser.add_option("--force",
action="store_true", dest="force",
help="Force update of an existing epub, download and overwrite all chapters.",)
help="Force overwrite or update of an existing epub, download and overwrite all chapters.",)
(options, args) = parser.parse_args()
@@ -79,14 +84,17 @@ def main():
conflist = []
homepath = join(expanduser("~"),".fanficdownloader")
if isfile(join(homepath,"defaults.ini")):
conflist.append(join(homepath,"defaults.ini"))
if isfile(join(homepath,"personal.ini")):
conflist.append(join(homepath,"personal.ini"))
if isfile("defaults.ini"):
conflist.append("defaults.ini")
if isfile(join(homepath,"personal.ini")):
conflist.append(join(homepath,"personal.ini"))
if isfile("personal.ini"):
conflist.append("personal.ini")
if options.configfile:
conflist.extend(options.configfile)
@@ -97,6 +105,10 @@ def main():
config.add_section("overrides")
except ConfigParser.DuplicateSectionError:
pass
if options.force:
config.set("overrides","always_overwrite","true")
if options.options:
for opt in options.options:
(var,val) = opt.split('=')
@@ -112,27 +124,28 @@ def main():
striptitletoc=True,
forceunique=False)
print "Updating %s, URL: %s" % (args[0],url)
filename = args[0]
output_filename = args[0]
config.set("overrides","output_filename",args[0])
else:
url = args[0]
adapter = adapters.getAdapter(config,url)
try:
adapter.getStoryMetadataOnly()
except exceptions.FailedToLogin:
print "Login Failed, Need Username/Password."
sys.stdout.write("Username: ")
adapter.username = sys.stdin.readline().strip()
adapter.password = getpass.getpass(prompt='Password: ')
#print("Login: `%s`, Password: `%s`" % (adapter.username, adapter.password))
adapter.getStoryMetadataOnly()
except exceptions.AdultCheckRequired:
print "Please confirm you are an adult in your locale: (y/n)?"
if sys.stdin.readline().strip().lower().startswith('y'):
adapter.is_adult=True
adapter.getStoryMetadataOnly()
## three tries, that's enough if both user/pass & is_adult needed,
## or a couple tries of one or the other
for x in range(0,2):
try:
adapter.getStoryMetadataOnly()
except exceptions.FailedToLogin:
print "Login Failed, Need Username/Password."
sys.stdout.write("Username: ")
adapter.username = sys.stdin.readline().strip()
adapter.password = getpass.getpass(prompt='Password: ')
#print("Login: `%s`, Password: `%s`" % (adapter.username, adapter.password))
except exceptions.AdultCheckRequired:
print "Please confirm you are an adult in your locale: (y/n)?"
if sys.stdin.readline().strip().lower().startswith('y'):
adapter.is_adult=True
if options.update and not options.force:
urlchaptercount = int(adapter.getStoryMetadataOnly().getMetadata('numChapters'))
@@ -184,16 +197,14 @@ def main():
adapter.setChaptersRange(options.begin,options.end)
if options.format == "all":
## For testing. Doing all three formats actually causes
## some interesting config issues with format-specific
## sections. But it should rarely be an issue.
writeStory(config,adapter,"epub",options.metaonly)
writeStory(config,adapter,"html",options.metaonly)
writeStory(config,adapter,"txt",options.metaonly)
else:
writeStory(config,adapter,options.format,options.metaonly)
output_filename=writeStory(config,adapter,options.format,options.metaonly)
if not options.metaonly and adapter.getConfig("post_process_cmd"):
metadata = adapter.story.metadata
metadata['output_filename']=output_filename
call(string.Template(adapter.getConfig("post_process_cmd"))
.substitute(metadata), shell=True)
del adapter
except exceptions.InvalidStoryURL, isu:
+13 -2
View File
@@ -216,7 +216,7 @@ def doMerge(outputio,files,authoropts=[],titleopt=None,descopt=None,
try:
outputepub.writestr(href,
epub.read(relpath+item.getAttribute("href")))
if re.match(r'.*/(file|chapter)\d+\.xhtml',href):
if re.match(r'.*/(file|chapter)\d+\.x?html',href):
filecount+=1
items.append((id,href,item.getAttribute("media-type")))
filelist.append(href)
@@ -224,10 +224,21 @@ def doMerge(outputio,files,authoropts=[],titleopt=None,descopt=None,
pass # Skip missing files.
for itemref in metadom.getElementsByTagName("itemref"):
if not striptitletoc or not re.match(r'(title|toc)_page', itemref.getAttribute("idref")):
itemrefs.append(bookid+itemref.getAttribute("idref"))
booknum=booknum+1;
if not forceunique:
# If not forceunique, it's an epub update.
# If there's a "calibre_bookmarks.txt", it's from reading
# in Calibre and should be preserved.
try:
fn = "META-INF/calibre_bookmarks.txt"
outputepub.writestr(fn,epub.read(fn))
except:
pass
## create content.opf file.
uniqueid="epubmerge-uid-%d" % time() # real sophisticated uid scheme.
@@ -355,7 +366,7 @@ def doMerge(outputio,files,authoropts=[],titleopt=None,descopt=None,
## during TOC generation to save loops.
outputepub.writestr("content.opf",contentdom.toxml('utf-8'))
outputepub.writestr("toc.ncx",tocncxdom.toxml('utf-8'))
# declares all the files created by Windows. otherwise, when
# it runs in appengine, windows unzips the files as 000 perms.
for zf in outputepub.filelist:
+6
View File
@@ -1,6 +1,12 @@
## This is an example of what your personal configuration might look
## like.
[defaults]
## Some sites also require the user to confirm they are adult for
## adult content. In commandline version, this should go in your
## personal.ini, not defaults.ini.
#is_adult:true
## Most common, I expect will be using this to save username/passwords
## for different sites.
[www.twilighted.net]
+36 -18
View File
@@ -15,21 +15,52 @@
# limitations under the License.
#
import os, re, sys, glob
import os, re, sys, glob, types
from os.path import dirname, basename, normpath
import logging
import urlparse as up
import fanficdownloader.exceptions as exceptions
from .. import exceptions as exceptions
## must import each adapter here.
import adapter_test1
import adapter_fanfictionnet
import adapter_castlefansorg
import adapter_fanfictionnet
import adapter_fictionalleyorg
import adapter_fictionpresscom
import adapter_ficwadcom
import adapter_fimfictionnet
import adapter_harrypotterfanfictioncom
import adapter_mediaminerorg
import adapter_potionsandsnitchesnet
import adapter_tenhawkpresentscom
import adapter_adastrafanficcom
import adapter_thewriterscoffeeshopcom
import adapter_tthfanficorg
import adapter_twilightednet
import adapter_twiwritenet
import adapter_whoficcom
import adapter_siyecouk
## This bit of complexity allows adapters to be added by just adding
## the source file. It eliminates the long if/else clauses we used to
## need to pick out the adapter.
## importing. It eliminates the long if/else clauses we used to need
## to pick out the adapter.
## List of registered site adapters.
__class_list = []
def imports():
for name, val in globals().items():
if isinstance(val, types.ModuleType):
yield val.__name__
for x in imports():
if "fanficdownloader.adapters.adapter_" in x:
#print x
__class_list.append(sys.modules[x].getClass())
def getAdapter(config,url):
## fix up leading protocol.
fixedurl = re.sub(r"(?i)^[htp]+[:/]+","http://",url.strip())
@@ -63,16 +94,3 @@ def getClassFor(domain):
for cls in __class_list:
if cls.matchesSite(domain):
return cls
## Automatically import each adapter_*.py file.
## Each implement getClass() to their class
filelist = glob.glob(dirname(__file__)+'/adapter_*.py')
sys.path.insert(0,normpath(dirname(__file__)))
for file in filelist:
#print "file: "+basename(file)[:-3]
module = __import__(basename(file)[:-3])
__class_list.append(module.getClass())
del sys.path[0]
@@ -21,9 +21,9 @@ import re
import urllib
import urllib2
import fanficdownloader.BeautifulSoup as bs
from fanficdownloader.htmlcleanup import stripHTML
import fanficdownloader.exceptions as exceptions
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate
@@ -79,6 +79,11 @@ class AdAstraFanficComSiteAdapter(BaseSiteAdapter):
if "Content is only suitable for mature adults. May contain explicit language and adult themes. Equivalent of NC-17." in data:
raise exceptions.AdultCheckRequired(self.url)
# problems with some stories, but only in calibre. I suspect
# issues with different SGML parsers in python. This is a
# nasty hack, but it works.
data = data[data.index("<body"):]
# use BeautifulSoup HTML parser to make everything easier to find.
soup = bs.BeautifulSoup(data)
@@ -142,6 +147,12 @@ class AdAstraFanficComSiteAdapter(BaseSiteAdapter):
for cat in catstext:
self.story.addToList('category',cat.string)
if 'Characters' in label:
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
charstext = [char.string for char in chars]
for char in charstext:
self.story.addToList('characters',char.string)
if 'Genre' in label:
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=1'))
genrestext = [genre.string for genre in genres]
@@ -175,7 +186,13 @@ class AdAstraFanficComSiteAdapter(BaseSiteAdapter):
logging.debug('Getting chapter text from: %s' % url)
soup = bs.BeautifulStoneSoup(self._fetchUrl(url),
data = self._fetchUrl(url)
# problems with some stories, but only in calibre. I suspect
# issues with different SGML parsers in python. This is a
# nasty hack, but it works.
data = data[data.index("<body"):]
soup = bs.BeautifulStoneSoup(data,
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
span = soup.find('div', {'id' : 'story'})
@@ -20,9 +20,9 @@ import logging
import re
import urllib2
import fanficdownloader.BeautifulSoup as bs
from fanficdownloader.htmlcleanup import stripHTML
import fanficdownloader.exceptions as exceptions
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate
@@ -51,11 +51,11 @@ from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate
# updated to reflect the class below it. That, plus getSiteDomain()
# take care of 'Registering'.
def getClass():
return FanficCastleTVNetAdapter # XXX
return CastleFansOrgAdapter # XXX
# Class name has to be unique. Our convention is camel case the
# sitename with Adapter at the end. www is skipped.
class FanficCastleTVNetAdapter(BaseSiteAdapter): # XXX
class CastleFansOrgAdapter(BaseSiteAdapter): # XXX
def __init__(self, config, url):
BaseSiteAdapter.__init__(self, config, url)
@@ -74,10 +74,10 @@ class FanficCastleTVNetAdapter(BaseSiteAdapter): # XXX
logging.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() + '/fanfic/viewstory.php?sid='+self.story.getMetadata('storyId'))
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','csltv') # XXX
self.story.setMetadata('siteabbrev','cslf') # XXX
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
@@ -91,13 +91,13 @@ class FanficCastleTVNetAdapter(BaseSiteAdapter): # XXX
@staticmethod # must be @staticmethod, don't remove it.
def getSiteDomain():
# The site domain. Does have www here, if it uses it.
return 'fanfic.castletv.net' # XXX
return 'castlefans.org' # XXX
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/viewstory.php?sid=1234"
return "http://"+self.getSiteDomain()+"/fanfic/viewstory.php?sid=1234"
def getSiteURLPattern(self):
return re.escape("http://"+self.getSiteDomain()+"/viewstory.php?sid=")+r"\d+$"
return re.escape("http://"+self.getSiteDomain()+"/fanfic/viewstory.php?sid=")+r"\d+$"
## Login seems to be reasonably standard across eFiction sites.
def needToLoginCheck(self, data):
@@ -120,7 +120,7 @@ class FanficCastleTVNetAdapter(BaseSiteAdapter): # XXX
params['cookiecheck'] = '1'
params['submit'] = 'Submit'
loginUrl = 'http://' + self.getSiteDomain() + '/user.php?action=login'
loginUrl = 'http://' + self.getSiteDomain() + '/fanfic/user.php?action=login'
logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
params['penname']))
@@ -192,7 +192,7 @@ class FanficCastleTVNetAdapter(BaseSiteAdapter): # XXX
# Find the chapters:
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")):
# just in case there's tags, like <i> in chapter titles.
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/'+chapter['href']+addurl))
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/fanfic/'+chapter['href']+addurl))
self.story.setMetadata('numChapters',len(self.chapterUrls))
@@ -232,6 +232,12 @@ class FanficCastleTVNetAdapter(BaseSiteAdapter): # XXX
for cat in catstext:
self.story.addToList('category',cat.string)
if 'Characters' in label:
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
charstext = [char.string for char in chars]
for char in charstext:
self.story.addToList('characters',char.string)
## Not all sites use Genre, but there's no harm to
## leaving it in. Check to make sure the type_id number
## is correct, though--it's site specific.
@@ -21,8 +21,8 @@ import re
import urllib2
import time
import fanficdownloader.BeautifulSoup as bs
import fanficdownloader.exceptions as exceptions
from .. import BeautifulSoup as bs
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate
@@ -70,7 +70,7 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
url = self.origurl
logging.debug("URL: "+url)
# use BeautifulSoup HTML parser to make everything easier to find.
try:
data = self._fetchUrl(url)
@@ -86,7 +86,29 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
if "Chapter 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.
try:
chapcount = len(soup.find('select', { 'name' : 'chapter' } ).findAll('option'))
# get chapter part of url.
except:
chapcount = 1
chapter = url.split('/',)[5]
tryurl = "http://%s/s/%s/%d/"%(self.getSiteDomain(),
self.story.getMetadata('storyId'),
chapcount+1)
print('=Trying newer chapter: %s' % tryurl)
newdata = self._fetchUrl(tryurl)
if "Chapter not found. Please check to see you are not using an outdated url." \
not in newdata:
print('=======Found newer chapter: %s' % tryurl)
soup = bs.BeautifulSoup(newdata)
except:
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])
@@ -175,6 +197,12 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
a = soup.find('a', href='http://www.fictionratings.com/')
self.story.setMetadata('rating',a.string)
# used below to get correct characters.
metatext = a.findNext(text=re.compile(r' - Reviews:'))
if metatext == None: # indicates there's no Reviews, look for id: instead.
metatext = a.findNext(text=re.compile(r' - id:'))
#print("========= metatext:\n%s"%metatext)
# after Rating, the same bit of text containing id:123456 contains
# Complete--if completed.
if 'Complete' in a.findNext(text=re.compile(r'id:'+self.story.getMetadata('storyId'))):
@@ -183,22 +211,36 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter):
self.story.setMetadata('status', 'In-Progress')
# Parse genre(s) from <meta name="description" content="..."
# <meta name="description" content="Chapter 1 of a Harry Potter - Family/Friendship fanfiction. Dudley Dursley would be the first to say he lived a very normal life. But what happens when he gets invited to his cousin Harry Potter's wedding? Will Dudley get the courage to apologize for the torture he caused all those years ago? Harry/Ginny story..">
# <meta name="description" content="A Gundam Wing/AC and Gundam Seed - Romance/Sci-Fi crossover fanfiction with characters: & Kira Y.. Story summary: One-Shoot dividido en dos partes. Kira va en camino a rescatar a Lacus, pero él no es el unico. Dos personajes de diferentes universos Gundams. SEED vs ZERO.">
# <meta name="description" content="Chapter 1 of a Alvin and the chipmunks and Alpha and Omega crossover fanfiction with characters: Alvin S. & Humphrey. You'll just have to read to find out... No Flames Plesae... and tell me what you want to see by PM'ing me....">
# genre is after first -, but before first 'fanfiction'.
m = re.match(r"^(?:Chapter \d+ of a|A) (?:.*?) (?:- (?P<genres>.*?)) (?:crossover )?fanfiction",
# <meta name="description" content="A Transformers/Beast Wars - Humor fanfiction with characters Prowl & Sideswipe. Story summary: Sideswipe is bored. Prowl appears to be so, too or at least, Sideswipe thinks he looks bored . So Sideswipe entertains them. After all, what's more fun than a race? Song-fic.">
# <meta name="description" content="Chapter 1 of a Transformers/Beast Wars - Adventure/Friendship fanfiction with characters Bumblebee. TFA: What would you do if you was being abused all you life? Follow NightRunner as she goes through her spark breaking adventure of getting away from her father..">
# (fp)<meta name="description" content="Chapter 1 of a Sci-Fi - Adventure/Humor fiction. Felix Max was just your regular hyperactive kid until he accidently caused his own fathers death. Now he has meta-humans trying to hunt him down with a corrupt goverment to back them up. Oh, and did I mention he has no Powers yet?.">
# <meta name="description" content="Chapter 1 of a Bleach - Adventure/Angst fanfiction with characters Ichigo K. & Neliel T. O./Nel. Time travel with a twist. Time can be a real bi***. Ichigo finds that fact out when he accidentally goes back in time. Is this his second chance or is fate just screwing with him. Not a crack fic.IchixNelXHime.">
m = re.match(r"^(?:Chapter \d+ of a|A) (?:.*?) (?:- (?P<genres>.*?) )?(?:crossover )?(?:fan)?fiction(?:[ ]+with characters (?P<char1>.*?\.?)(?: & (?P<char2>.*?\.?))?\. )?",
soup.find('meta',{'name':'description'})['content'])
if m != None:
genres=m.group('genres')
# Hurt/Comfort is one genre.
genres=re.sub('Hurt/Comfort','Hurt-Comfort',genres)
for g in genres.split('/'):
self.story.addToList('genre',g)
if genres != None:
# Hurt/Comfort is one genre.
genres=re.sub('Hurt/Comfort','Hurt-Comfort',genres)
for g in genres.split('/'):
self.story.addToList('genre',g)
if m.group('char1') != None:
# At this point we've proven that there's character(s)
# We can't reliably parse characters out of meta name="description".
# There's no way to tell that "with characters Ichigo K. & Neliel T. O./Nel. " ends at "Nel.", not "T."
# But we can pull them from the reviewstext line, now that we know about existance of chars.
# reviewstext can take form of:
# - English - Shinji H. - Updated: 01-13-12 - Published: 12-20-11 - id:7654123
# - English - Adventure/Angst - Ichigo K. & Neliel T. O./Nel - Reviews:
mc = re.match(r" - (?P<lang>[^ ]+ - )(?P<genres>[^ ]+ - )? (?P<chars>.+?) - (Reviews|Updated|Published)",
metatext)
chars = mc.group("chars")
for c in chars.split(' & '):
self.story.addToList('characters',c)
return
def getChapterText(self, url):
logging.debug('Getting chapter text from: %s' % url)
time.sleep(0.5) ## ffnet(and, I assume, fpcom) tends to fail
@@ -21,9 +21,9 @@ import re
import urllib
import urllib2
import fanficdownloader.BeautifulSoup as bs
from fanficdownloader.htmlcleanup import stripHTML
import fanficdownloader.exceptions as exceptions
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate
@@ -22,9 +22,9 @@ import urllib2
import time
import httplib, urllib
import fanficdownloader.BeautifulSoup as bs
import fanficdownloader.exceptions as exceptions
from fanficdownloader.htmlcleanup import stripHTML
from .. import BeautifulSoup as bs
from .. import exceptions as exceptions
from ..htmlcleanup import stripHTML
from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate
@@ -152,6 +152,12 @@ class FicwadComSiteAdapter(BaseSiteAdapter):
for g in m.group(1).split(','):
self.story.addToList('genre',g)
m = re.match(r".*?Characters: (.*?) -.*?",metastr)
if m:
for g in m.group(1).split(','):
if g:
self.story.addToList('characters',g)
m = re.match(r".*?Published: ([0-9/]+?) -.*?",metastr)
if m:
self.story.setMetadata('datePublished',makeDate(m.group(1), "%Y/%m/%d"))
@@ -21,9 +21,9 @@ import re
import urllib2
import cookielib as cl
import fanficdownloader.BeautifulSoup as bs
from fanficdownloader.htmlcleanup import stripHTML
import fanficdownloader.exceptions as exceptions
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate
@@ -87,8 +87,10 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
raise exceptions.AdultCheckRequired(self.url)
soup = bs.BeautifulSoup(data).find("div", {"class":"content_box post_content_box"})
title, author = [link.text for link in soup.find("h2").findAll("a")]
titleheader = soup.find("h2")
title = titleheader.find("a", href=re.compile(r'^/story/')).text
author = titleheader.find("a", href=re.compile(r'^/user/')).text
self.story.setMetadata("title", title)
self.story.setMetadata("author", author)
self.story.setMetadata("authorId", author) # The author's name will be unique
@@ -126,7 +128,9 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
status = status_bar.text.split("|")[0].strip().replace("Incomplete", "In-Progress").replace("On Hiatus", "In-Progress").replace("Complete", "Completed")
self.story.setMetadata('status', status)
self.story.setMetadata('rating', status_bar.span.text)
self.story.setMetadata('numWords', status_bar.div.b.text)
# This way is less elegant, perhaps, but more robust in face of format changes.
numWords = status_bar.find("div",{"class":"word_count"}).b.text
self.story.setMetadata('numWords', numWords)
description_soup = soup.find("div", {"class":"description"})
# Sometimes the description has an expanding element
@@ -158,4 +162,4 @@ class FimFictionNetSiteAdapter(BaseSiteAdapter):
if soup == None:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
return utf8FromSoup(soup)
@@ -21,9 +21,9 @@ import re
import urllib
import urllib2
import fanficdownloader.BeautifulSoup as bs
from fanficdownloader.htmlcleanup import stripHTML
import fanficdownloader.exceptions as exceptions
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate
@@ -159,6 +159,11 @@ class HarryPotterFanFictionComSiteAdapter(BaseSiteAdapter):
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(','):
@@ -21,9 +21,9 @@ import re
import urllib
import urllib2
import fanficdownloader.BeautifulSoup as bs
from fanficdownloader.htmlcleanup import stripHTML
import fanficdownloader.exceptions as exceptions
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate
@@ -21,9 +21,9 @@ import re
import urllib
import urllib2
import fanficdownloader.BeautifulSoup as bs
from fanficdownloader.htmlcleanup import stripHTML
import fanficdownloader.exceptions as exceptions
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate
@@ -145,6 +145,16 @@ class PotionsAndSnitchesNetSiteAdapter(BaseSiteAdapter):
for cat in catstext:
self.story.addToList('category',cat.string)
if 'Characters' in label:
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
charstext = [char.string for char in chars]
for char in charstext:
if char == "!Snape and Harry (required)":
self.story.addToList('characters',"Snape")
self.story.addToList('characters',"Harry")
else:
self.story.addToList('characters',char.string)
if 'Genre' in label:
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class'))
genrestext = [genre.string for genre in genres]
@@ -0,0 +1,282 @@
# -*- coding: utf-8 -*-
# Copyright 2011 Fanficdownloader team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import time
import logging
import re
import urllib2
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate
# This function is called by the downloader in all adapter_*.py files
# in this dir to register the adapter class. So it needs to be
# updated to reflect the class below it. That, plus getSiteDomain()
# take care of 'Registering'.
def getClass():
return SiyeCoUkAdapter # XXX
# Class name has to be unique. Our convention is camel case the
# sitename with Adapter at the end. www is skipped.
class SiyeCoUkAdapter(BaseSiteAdapter): # XXX
def __init__(self, config, url):
BaseSiteAdapter.__init__(self, config, url)
self.decode = ["Windows-1252",
"utf8",]# 1252 is a superset of iso-8859-1.
# Most sites that claim to be
# iso-8859-1 (and some that claim to be
# utf8) are really windows-1252.
# self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
# self.password = ""
# self.is_adult=False
# get storyId from url--url validation guarantees query is only sid=1234
self.story.setMetadata('storyId',self.parsedUrl.query.split('=',)[1])
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL('http://' + self.getSiteDomain() + '/siye/viewstory.php?sid='+self.story.getMetadata('storyId'))
# Each adapter needs to have a unique site abbreviation.
self.story.setMetadata('siteabbrev','siye') # XXX
# If all stories from the site fall into the same category,
# the site itself isn't likely to label them as such, so we
# do.
self.story.addToList("category","Harry Potter") # XXX
# The date format will vary from site to site.
# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
self.dateformat = "%Y.%m.%d" # XXX
@staticmethod # must be @staticmethod, don't remove it.
def getSiteDomain():
# The site domain. Does have www here, if it uses it.
return 'www.siye.co.uk' # XXX
@classmethod
def getAcceptDomains(cls):
return ['www.siye.co.uk','siye.co.uk']
def getSiteExampleURLs(self):
return "http://"+self.getSiteDomain()+"/siye/viewstory.php?sid=1234"
def getSiteURLPattern(self):
return re.escape("http://")+r"(www\.)?"+re.escape("siye.co.uk/siye/viewstory.php?sid=")+r"\d+$"
# ## Login seems to be reasonably standard across eFiction sites.
# def needToLoginCheck(self, data):
# if 'Registered Users Only' in data \
# or 'There is no such account on our website' in data \
# or "That password doesn't match the one in our database" in data:
# return True
# else:
# return False
# def performLogin(self, url):
# params = {}
# if self.password:
# params['penname'] = self.username
# params['password'] = self.password
# else:
# params['penname'] = self.getConfig("username")
# params['password'] = self.getConfig("password")
# params['cookiecheck'] = '1'
# params['submit'] = 'Submit'
# loginUrl = 'http://' + self.getSiteDomain() + '/user.php?action=login'
# logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
# params['penname']))
# d = self._fetchUrl(loginUrl, params)
# if "Member Account" not in d : #Member Account
# logging.info("Failed to login to URL %s as %s" % (loginUrl,
# params['penname']))
# raise exceptions.FailedToLogin(url,params['penname'])
# return False
# else:
# return True
## Getting the chapter list and the meta data, plus 'is adult' checking.
def extractChapterUrlsAndMetadata(self):
# if self.is_adult or self.getConfig("is_adult"):
# # Weirdly, different sites use different warning numbers.
# # If the title search below fails, there's a good chance
# # you need a different number. print data at that point
# # and see what the 'click here to continue' url says.
# addurl = "&ageconsent=ok&warning=4" # XXX
# else:
# addurl=""
# index=1 makes sure we see the story chapter index. Some
# sites skip that for one-chapter stories.
# Except it doesn't this time. :-/
url = self.url #+'&index=1'+addurl
logging.debug("URL: "+url)
try:
data = self._fetchUrl(url)
except urllib2.HTTPError, e:
if e.code == 404:
raise exceptions.StoryDoesNotExist(self.url)
else:
raise e
# if self.needToLoginCheck(data):
# # need to log in for this one.
# self.performLogin(url)
# data = self._fetchUrl(url)
# # The actual text that is used to announce you need to be an
# # adult varies from site to site. Again, print data before
# # the title search to troubleshoot.
# if "Age Consent Required" in data: # XXX
# raise exceptions.AdultCheckRequired(self.url)
# if "Access denied. This story has not been validated by the adminstrators of this site." in data:
# raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
# use BeautifulSoup HTML parser to make everything easier to find.
soup = bs.BeautifulSoup(data)
# print data
# Now go hunting for all the meta data and the chapter list.
# Find authorid and URL from... author url.
a = soup.find('a', href=re.compile(r"viewuser.php\?uid=\d+"))
self.story.setMetadata('authorId',a['href'].split('=')[1])
self.story.setMetadata('authorUrl','http://'+self.host+'/siye/'+a['href'])
self.story.setMetadata('author',a.string)
# need(or easier) to pull other metadata from the author's list page.
authsoup = bs.BeautifulSoup(self._fetchUrl(self.story.getMetadata('authorUrl')))
## Title
titlea = authsoup.find('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"$"))
self.story.setMetadata('title',titlea.string)
# Find the chapters (from soup, not authsoup):
for chapter in soup.findAll('a', href=re.compile(r'viewstory.php\?sid='+self.story.getMetadata('storyId')+"&chapter=\d+$")):
# just in case there's tags, like <i> in chapter titles.
self.chapterUrls.append((stripHTML(chapter),'http://'+self.host+'/siye/'+chapter['href']))
if self.chapterUrls:
self.story.setMetadata('numChapters',len(self.chapterUrls))
else:
self.chapterUrls.append((self.story.getMetadata('title'),url))
self.story.setMetadata('numChapters',1)
# The stuff we can get from the chapter list/one-shot page are
# in the first table with 95% width.
metatable = soup.find('table',{'width':'95%'})
# Categories
cat_as = metatable.findAll('a', href=re.compile(r'categories.php'))
for cat_a in cat_as:
self.story.addToList('category',stripHTML(cat_a))
moremetaparts = stripHTML(metatable).split('\n')
for part in moremetaparts:
part = part.strip()
if part.startswith("Characters:"):
part = part[part.find(':')+1:]
for item in part.split(','):
if item.strip() == "Harry/Ginny":
self.story.addToList('characters',"Harry")
self.story.addToList('characters',"Ginny")
elif item.strip() not in ("None","All"):
self.story.addToList('characters',item)
if part.startswith("Genres:"):
part = part[part.find(':')+1:]
for item in part.split(','):
if item.strip() != "None":
self.story.addToList('genre',item)
if part.startswith("Warnings:"):
part = part[part.find(':')+1:]
for item in part.split(','):
if item.strip() != "None":
self.story.addToList('warnings',item)
if part.startswith("Rating:"):
part = part[part.find(':')+1:]
self.story.setMetadata('rating',part)
if part.startswith("Summary:"):
part = part[part.find(':')+1:]
self.story.setMetadata('description',part)
# want to get the next tr of the table.
#print("%s"%titlea.parent.parent.findNextSibling('tr'))
# eFiction sites don't help us out a lot with their meta data
# formating, so it's a little ugly.
moremeta = stripHTML(titlea.parent.parent.findNextSibling('tr'))
for part in moremeta.replace(' - ','\n').split('\n'):
#print("part:%s"%part)
try:
(name,value) = part.split(': ')
except:
# not going to worry about fancier processing for the bits
# that don't match.
continue
name=name.strip()
value=value.strip()
if name == 'Published':
self.story.setMetadata('datePublished', makeDate(value, self.dateformat))
if name == 'Updated':
self.story.setMetadata('dateUpdated', makeDate(value, self.dateformat))
if name == 'Completed':
if value == 'Yes':
self.story.setMetadata('status', 'Completed')
else:
self.story.setMetadata('status', 'In-Progress')
if name == 'Words':
self.story.setMetadata('numWords', value)
# grab the text for an individual chapter.
def getChapterText(self, url):
logging.debug('Getting chapter text from: %s' % url)
# soup = bs.BeautifulSoup(self._fetchUrl(url))
# BeautifulSoup objects to <p> inside <span>, which
# technically isn't allowed.
soup = bs.BeautifulStoneSoup(self._fetchUrl(url),
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
# not the most unique thing in the world, but it appears to be
# the best we can do here.
story = soup.find('span', {'style' : 'font-size: 100%;'})
if None == story:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
return utf8FromSoup(story)
@@ -21,9 +21,9 @@ import re
import urllib
import urllib2
import fanficdownloader.BeautifulSoup as bs
from fanficdownloader.htmlcleanup import stripHTML
import fanficdownloader.exceptions as exceptions
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate
@@ -178,6 +178,12 @@ class TenhawkPresentsComSiteAdapter(BaseSiteAdapter):
for cat in catstext:
self.story.addToList('category',cat.string)
if 'Characters' in label:
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
charstext = [char.string for char in chars]
for char in charstext:
self.story.addToList('characters',char.string)
if 'Genre' in label:
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class'))
genrestext = [genre.string for genre in genres]
+20 -10
View File
@@ -19,8 +19,8 @@ import datetime
import time
import logging
import fanficdownloader.BeautifulSoup as bs
import fanficdownloader.exceptions as exceptions
from .. import BeautifulSoup as bs
from .. import exceptions
from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate
@@ -54,6 +54,12 @@ class TestSiteAdapter(BaseSiteAdapter):
if self.story.getMetadata('storyId') == '666':
raise exceptions.StoryDoesNotExist(self.url)
if self.story.getMetadata('storyId').startswith('670'):
time.sleep(1.0)
if self.story.getMetadata('storyId').startswith('671'):
time.sleep(1.0)
if self.getConfig("username"):
self.username = self.getConfig("username")
@@ -61,25 +67,26 @@ class TestSiteAdapter(BaseSiteAdapter):
raise exceptions.FailedToLogin(self.url,self.username)
if self.story.getMetadata('storyId') == '664':
self.story.setMetadata(u'title',"Test Story Title "+self.crazystring)
self.story.setMetadata(u'title',"Test Story Title "+self.story.getMetadata('storyId')+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.setMetadata(u'title',"Test Story Title "+self.story.getMetadata('storyId'))
self.story.setMetadata('author','Test Author aa')
self.story.setMetadata('storyUrl',self.url)
self.story.setMetadata('description',u'Description '+self.crazystring+u''' Done
Some more longer description. "I suck at summaries!" "Better than it sounds!" "My first fic"
''')
self.story.setMetadata('datePublished',makeDate("1972-01-31","%Y-%m-%d"))
self.story.setMetadata('datePublished',makeDate("1975-03-15","%Y-%m-%d"))
self.story.setMetadata('dateCreated',datetime.datetime.now())
if self.story.getMetadata('storyId') == '669':
self.story.setMetadata('dateUpdated',datetime.datetime.now())
else:
self.story.setMetadata('dateUpdated',makeDate("1975-01-31","%Y-%m-%d"))
self.story.setMetadata('dateUpdated',makeDate("1975-04-15","%Y-%m-%d"))
self.story.setMetadata('numWords','123456')
self.story.setMetadata('status','In-Completed')
self.story.setMetadata('rating','Tweenie')
self.story.setMetadata('author','Test Author aa')
self.story.setMetadata('authorId','98765')
self.story.setMetadata('authorUrl','http://author/url')
@@ -100,7 +107,7 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
('Chapter 3, Over Cinnabar',self.url+"&chapter=4"),
('Chapter 4',self.url+"&chapter=5"),
('Chapter 5',self.url+"&chapter=6"),
# ('Chapter 6',self.url+"&chapter=6"),
('Chapter 6',self.url+"&chapter=6"),
# ('Chapter 7',self.url+"&chapter=6"),
# ('Chapter 8',self.url+"&chapter=6"),
# ('Chapter 9',self.url+"&chapter=6"),
@@ -128,8 +135,9 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
if self.story.getMetadata('storyId') == '667':
raise exceptions.FailedToDownload("Error downloading Chapter: %s!" % url)
if self.story.getMetadata('storyId') == '670':
time.sleep(2.0)
if self.story.getMetadata('storyId').startswith('670') or \
self.story.getMetadata('storyId').startswith('672'):
time.sleep(1.0)
if "chapter=1" in url :
text=u'''
@@ -143,6 +151,8 @@ 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>And other storyId will succeed with the same output.</p>
</div>
'''
@@ -21,9 +21,9 @@ import re
import urllib
import urllib2
import fanficdownloader.BeautifulSoup as bs
from fanficdownloader.htmlcleanup import stripHTML
import fanficdownloader.exceptions as exceptions
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate
@@ -123,6 +123,11 @@ class TheWritersCoffeeShopComSiteAdapter(BaseSiteAdapter):
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
# problems with some stories, but only in calibre. I suspect
# issues with different SGML parsers in python. This is a
# nasty hack, but it works.
data = data[data.index("<body"):]
# use BeautifulSoup HTML parser to make everything easier to find.
soup = bs.BeautifulSoup(data)
@@ -175,6 +180,12 @@ class TheWritersCoffeeShopComSiteAdapter(BaseSiteAdapter):
for cat in catstext:
self.story.addToList('category',cat.string)
if 'Characters' in label:
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
charstext = [char.string for char in chars]
for char in charstext:
self.story.addToList('characters',char.string)
if 'Genre' in label:
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class'))
genrestext = [genre.string for genre in genres]
@@ -201,7 +212,13 @@ class TheWritersCoffeeShopComSiteAdapter(BaseSiteAdapter):
logging.debug('Getting chapter text from: %s' % url)
soup = bs.BeautifulStoneSoup(self._fetchUrl(url),
data = self._fetchUrl(url)
# problems with some stories, but only in calibre. I suspect
# issues with different SGML parsers in python. This is a
# nasty hack, but it works.
data = data[data.index("<body"):]
soup = bs.BeautifulStoneSoup(data,
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
span = soup.find('div', {'id' : 'story'})
@@ -0,0 +1,239 @@
# -*- coding: utf-8 -*-
# Copyright 2011 Fanficdownloader team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import time
import logging
import re
import urllib2
import time
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate
class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter):
def __init__(self, config, url):
BaseSiteAdapter.__init__(self, config, url)
self.story.setMetadata('siteabbrev','tth')
self.dateformat = "%d %b %y"
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'))
logging.debug("storyId: (%s)"%self.story.getMetadata('storyId'))
# normalized story URL.
self._setURL("http://"+self.getSiteDomain()\
+"/Story-"+self.story.getMetadata('storyId'))
else:
raise exceptions.InvalidStoryURL(url,
self.getSiteDomain(),
self.getSiteExampleURLs())
@staticmethod
def getSiteDomain():
return 'www.tthfanfic.org'
def getSiteExampleURLs(self):
return "http://www.tthfanfic.org/Story-5583 http://www.tthfanfic.org/Story-5583/Greywizard+Marked+By+Kane.htm http://www.tthfanfic.org/T-526321777890480578489880055880/Story-26448-15/batzulger+Willow+Rosenberg+and+the+Mind+Riders.htm"
# http://www.tthfanfic.org/T-526321777848988007890480555880/Story-26448-15/batzulger+Willow+Rosenberg+and+the+Mind+Riders.htm
# http://www.tthfanfic.org/Story-5583
# http://www.tthfanfic.org/Story-5583/Greywizard+Marked+By+Kane.htm
# http://www.tthfanfic.org/story.php?no=26093
def getSiteURLPattern(self):
return r"http://www.tthfanfic.org(/(T-\d+/)?Story-|/story.php\?no=)(?P<id>\d+)(-\d+)?(/.*)?$"
# tth won't send you future updates if you aren't 'caught up'
# on the story. Login isn't required for F21, but logging in will
# mark stories you've downloaded as 'read' on tth.
def performLogin(self):
params = {}
if self.password:
params['urealname'] = self.username
params['password'] = self.password
else:
params['urealname'] = self.getConfig("username")
params['password'] = self.getConfig("password")
params['loginsubmit'] = 'Login'
if not params['password']:
return
loginUrl = 'http://' + self.getSiteDomain() + '/login.php'
logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
params['urealname']))
## need to pull empty login page first to get ctkn and
## password name, which are BUSs
# <form method='post' action='/login.php' accept-charset="utf-8">
# <input type='hidden' name='ctkn' value='4bdf761f5bea06bf4477072afcbd0f8d721d1a4f989c09945a9e87afb7a66de1'/>
# <input type='text' id='urealname' name='urealname' value=''/>
# <input type='password' id='password' name='6bb3fcd148d148629223690bf19733b8'/>
# <input type='submit' value='Login' name='loginsubmit'/>
soup = bs.BeautifulSoup(self._fetchUrl(loginUrl))
params['ctkn']=soup.find('input', {'name':'ctkn'})['value']
params[soup.find('input', {'id':'password'})['name']] = params['password']
d = self._fetchUrl(loginUrl, params)
if "Stories Published" not in d : #Member Account
logging.info("Failed to login to URL %s as %s" % (loginUrl,
params['penname']))
raise exceptions.FailedToLogin(url,params['penname'])
return False
else:
return True
def extractChapterUrlsAndMetadata(self):
# fetch the chapter. From that we will get almost all the
# metadata and chapter list
url=self.url
logging.debug("URL: "+url)
# tth won't send you future updates if you aren't 'caught up'
# on the story. Login isn't required for F21, but logging in will
# mark stories you've downloaded as 'read' on tth.
self.performLogin()
# use BeautifulSoup HTML parser to make everything easier to find.
try:
data = self._fetchUrl(url)
soup = bs.BeautifulSoup(data)
except urllib2.HTTPError, e:
if e.code == 404:
raise exceptions.StoryDoesNotExist(url)
else:
raise e
if "<h2>Story Not Found</h2>" in data:
raise exceptions.StoryDoesNotExist(url)
if "NOTE: This story is rated FR21 which is above your chosen filter level." in data:
if self.is_adult or self.getConfig("is_adult"):
form = soup.find('form', {'id':'sitemaxratingform'})
params={'ctkn':form.find('input', {'name':'ctkn'})['value'],
'sitemaxrating':'5'}
logging.info("Attempting to get rating cookie for %s" % url)
data = self._postUrl("http://"+self.getSiteDomain()+'/setmaxrating.php',params)
# refetch story page.
data = self._fetchUrl(url)
soup = bs.BeautifulSoup(data)
else:
raise exceptions.AdultCheckRequired(self.url)
# http://www.tthfanfic.org/AuthorStories-3449/Greywizard.htm
# Find authorid and URL from... author url.
a = soup.find('a', href=re.compile(r"^/AuthorStories-\d+"))
self.story.setMetadata('authorId',a['href'].split('/')[1].split('-')[1])
self.story.setMetadata('authorUrl','http://'+self.host+a['href'])
self.story.setMetadata('author',stripHTML(a))
try:
# going to pull part of the meta data from author list page.
logging.debug("**AUTHOR** URL: "+self.story.getMetadata('authorUrl'))
authordata = self._fetchUrl(self.story.getMetadata('authorUrl'))
authorsoup = bs.BeautifulSoup(authordata)
# author can have several pages, scan until we find it.
while( not authorsoup.find('a', href=re.compile(r"^/Story-"+self.story.getMetadata('storyId'))) ):
nextpage = 'http://'+self.host+authorsoup.find('a', {'class':'arrowf'})['href']
logging.debug("**AUTHOR** nextpage URL: "+nextpage)
authordata = self._fetchUrl(nextpage)
authorsoup = bs.BeautifulSoup(authordata)
except urllib2.HTTPError, e:
if e.code == 404:
raise exceptions.StoryDoesNotExist(url)
else:
raise e
storydiv = authorsoup.find('div', {'id':'st'+self.story.getMetadata('storyId'), 'class':re.compile(r"storylistitem")})
self.story.setMetadata('description',stripHTML(storydiv.find('div',{'class':'storydesc'})))
self.story.setMetadata('title',stripHTML(storydiv.find('a',{'class':'storylink'})))
verticaltable = soup.find('table', {'class':'verticaltable'})
BtVS = True
for cat in verticaltable.findAll('a', href=re.compile(r"^/Category-")):
if cat.string not in ['General', 'Non-BtVS/AtS Stories', 'BtVS/AtS Non-Crossover', 'Non-BtVS Crossovers']:
self.story.addToList('category',cat.string)
else:
if 'Non-BtVS' in cat.string:
BtVS = False
if BtVS:
self.story.addToList('category','Buffy: The Vampire Slayer')
verticaltabletds = verticaltable.findAll('td')
self.story.setMetadata('rating', verticaltabletds[2].string)
self.story.setMetadata('numWords', verticaltabletds[4].string)
# Complete--if completed.
if 'Yes' in verticaltabletds[10].string:
self.story.setMetadata('status', 'Completed')
else:
self.story.setMetadata('status', 'In-Progress')
self.story.setMetadata('datePublished',makeDate(stripHTML(verticaltabletds[8].string), self.dateformat))
self.story.setMetadata('dateUpdated',makeDate(stripHTML(verticaltabletds[9].string), self.dateformat))
for icon in storydiv.find('span',{'class':'storyicons'}).findAll('img'):
if( icon['title'] not in ['Non-Crossover'] ) :
self.story.addToList('genre',icon['title'])
# Find the chapter selector
select = soup.find('select', { 'name' : 'chapnav' } )
if select is None:
# no selector found, so it's a one-chapter story.
self.chapterUrls.append((self.story.getMetadata('title'),url))
else:
allOptions = select.findAll('option')
for o in allOptions:
url = "http://"+self.host+o['value']
# just in case there's tags, like <i> in chapter titles.
self.chapterUrls.append((stripHTML(o),url))
self.story.setMetadata('numChapters',len(self.chapterUrls))
return
def getChapterText(self, url):
logging.debug('Getting chapter text from: %s' % url)
soup = bs.BeautifulSoup(self._fetchUrl(url))
div = soup.find('div', {'id' : 'storyinnerbody'})
if None == div:
raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url)
# strip out included chapter title, if present, to avoid doubling up.
try:
div.find('h3').extract()
except:
pass
return utf8FromSoup(div)
def getClass():
return TwistingTheHellmouthSiteAdapter
@@ -21,9 +21,9 @@ import re
import urllib
import urllib2
import fanficdownloader.BeautifulSoup as bs
from fanficdownloader.htmlcleanup import stripHTML
import fanficdownloader.exceptions as exceptions
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate
@@ -118,6 +118,12 @@ class TwilightedNetSiteAdapter(BaseSiteAdapter):
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
# problems with some stories, but only in calibre. I suspect
# issues with different SGML parsers in python. This is a
# nasty hack, but it works.
# twilighted isn't writing <body> ??? wtf?
data = "<html><body>"+data[data.index("</head>"):]
# use BeautifulSoup HTML parser to make everything easier to find.
soup = bs.BeautifulSoup(data)
@@ -170,6 +176,12 @@ class TwilightedNetSiteAdapter(BaseSiteAdapter):
for cat in catstext:
self.story.addToList('category',cat.string)
if 'Characters' in label:
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
charstext = [char.string for char in chars]
for char in charstext:
self.story.addToList('characters',char.string)
## twilighted.net doesn't use genre.
# if 'Genre' in label:
# genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class'))
@@ -197,7 +209,14 @@ class TwilightedNetSiteAdapter(BaseSiteAdapter):
logging.debug('Getting chapter text from: %s' % url)
soup = bs.BeautifulStoneSoup(self._fetchUrl(url),
data = self._fetchUrl(url)
# problems with some stories, but only in calibre. I suspect
# issues with different SGML parsers in python. This is a
# nasty hack, but it works.
# twilighted isn't writing <body> ??? wtf?
data = "<html><body>"+data[data.index("</head>"):]
soup = bs.BeautifulStoneSoup(data,
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
span = soup.find('div', {'id' : 'story'})
@@ -21,9 +21,9 @@ import re
import urllib
import urllib2
import fanficdownloader.BeautifulSoup as bs
from fanficdownloader.htmlcleanup import stripHTML
import fanficdownloader.exceptions as exceptions
from .. import BeautifulSoup as bs
from ..htmlcleanup import stripHTML
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate
@@ -84,7 +84,7 @@ class TwiwriteNetSiteAdapter(BaseSiteAdapter):
params['submit'] = 'Submit'
loginUrl = 'http://' + self.getSiteDomain() + '/user.php?action=login'
logging.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
logging.info("Will now login to URL (%s) as (%s)" % (loginUrl,
params['penname']))
d = self._fetchUrl(loginUrl, params)
@@ -117,7 +117,12 @@ class TwiwriteNetSiteAdapter(BaseSiteAdapter):
if "Access denied. This story has not been validated by the adminstrators of this site." in data:
raise exceptions.FailedToDownload(self.getSiteDomain() +" says: Access denied. This story has not been validated by the adminstrators of this site.")
# problems with some stories, but only in calibre. I suspect
# issues with different SGML parsers in python. This is a
# nasty hack, but it works.
data = data[data.index("<body"):]
# use BeautifulSoup HTML parser to make everything easier to find.
soup = bs.BeautifulSoup(data)
@@ -178,6 +183,12 @@ class TwiwriteNetSiteAdapter(BaseSiteAdapter):
for cat in catstext:
self.story.addToList('category',cat.string)
if 'Characters' in label:
chars = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=characters'))
charstext = [char.string for char in chars]
for char in charstext:
self.story.addToList('characters',char.string)
if 'Genre' in label:
genres = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=class&type_id=3'))
genrestext = [genre.string for genre in genres]
@@ -211,7 +222,13 @@ class TwiwriteNetSiteAdapter(BaseSiteAdapter):
logging.debug('Getting chapter text from: %s' % url)
soup = bs.BeautifulStoneSoup(self._fetchUrl(url),
data = self._fetchUrl(url)
# problems with some stories, but only in calibre. I suspect
# issues with different SGML parsers in python. This is a
# nasty hack, but it works.
data = data[data.index("<body"):]
soup = bs.BeautifulStoneSoup(data,
selfClosingTags=('br','hr')) # otherwise soup eats the br/hr tags.
span = soup.find('div', {'id' : 'story'})
+11 -2
View File
@@ -20,8 +20,8 @@ import logging
import re
import urllib2
import fanficdownloader.BeautifulSoup as bs
import fanficdownloader.exceptions as exceptions
from .. import BeautifulSoup as bs
from .. import exceptions as exceptions
from base_adapter import BaseSiteAdapter, utf8FromSoup, makeDate
@@ -158,6 +158,15 @@ class WhoficComSiteAdapter(BaseSiteAdapter):
for g in genre.split(r', '):
self.story.addToList('genre',g)
# line 3 is characters.
chars = metadatachunks[3]
charsearch="<i>Characters:</i>"
if charsearch in chars:
chars = chars[metadatachunks[3].index(charsearch)+len(charsearch):]
for c in chars.split(','):
if c.strip() != u'None':
self.story.addToList('characters',c)
# the next line is stuff with ' - ' separators *and* names--with tags.
moremeta = metadatachunks[5]
moremeta = re.sub(r'<[^>]+>','',moremeta) # strip tags.
+32 -6
View File
@@ -39,13 +39,13 @@ except:
pass
#logging.info("Hook to make default deadline 10.0 NOT installed--not using appengine")
from fanficdownloader.story import Story
from fanficdownloader.configurable import Configurable
from fanficdownloader.htmlcleanup import removeEntities, removeAllEntities, stripHTML
from fanficdownloader.exceptions import InvalidStoryURL
from ..story import Story
from ..configurable import Configurable
from ..htmlcleanup import removeEntities, removeAllEntities, stripHTML
from ..exceptions import InvalidStoryURL
try:
import fanficdownloader.chardet as chardet
from .. import chardet as chardet
except ImportError:
chardet = None
@@ -63,10 +63,15 @@ class BaseSiteAdapter(Configurable):
return re.match(self.getSiteURLPattern(), self.url)
def __init__(self, config, url):
self.config = config
Configurable.__init__(self, config)
self.addConfigSection(self.getSiteDomain())
self.addConfigSection("overrides")
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
self.password = ""
self.is_adult=False
self.opener = u2.build_opener(u2.HTTPCookieProcessor())
self.storyDone = False
self.metadataDone = False
@@ -223,8 +228,29 @@ class BaseSiteAdapter(Configurable):
def getChapterText(self, url):
"Needs to be overriden in each adapter class."
pass
fullmon = {"January":"01", "February":"02", "March":"03", "April":"04", "May":"05",
"June":"06","July":"07", "August":"08", "September":"09", "October":"10",
"November":"11", "December":"12" }
def makeDate(string,format):
# Surprise! Abstracting this turned out to be more useful than
# just saving bytes.
# fudge english month names for people who's locale is set to
# non-english. All our current sites date in english, even if
# there's non-english content.
do_abbrev = "%b" in format
if "%B" in format or do_abbrev:
format = format.replace("%B","%m").replace("%b","%m")
for (name,num) in fullmon.items():
if do_abbrev:
name = name[:3] # first three for abbrev
if name in string:
string = string.replace(name,num)
break
return datetime.datetime.strptime(string,format)
acceptable_attributes = ['href','name']
+24 -5
View File
@@ -17,7 +17,7 @@
import os
from htmlcleanup import conditionalRemoveEntities
from htmlcleanup import conditionalRemoveEntities, removeAllEntities
class Story:
@@ -25,20 +25,22 @@ class Story:
try:
self.metadata = {'version':os.environ['CURRENT_VERSION_ID']}
except:
self.metadata = {'version':'4.0'}
self.metadata = {'version':'4.2'}
self.chapters = [] # chapters will be tuples of (title,html)
self.listables = {} # some items (extratags, category, warnings & genres) are also kept as lists.
def setMetadata(self, key, value):
## still keeps &lt; &lt; and &amp;
self.metadata[key]=conditionalRemoveEntities(value)
def getMetadataRaw(self,key):
if self.metadata.has_key(key):
return self.metadata[key]
def getMetadata(self, key):
def getMetadata(self, key, removeallentities=False):
value = None
if self.getLists().has_key(key):
return ', '.join(self.getList(key))
value = ', '.join(self.getList(key))
if self.metadata.has_key(key):
value = self.metadata[key]
if value:
@@ -48,16 +50,33 @@ class Story:
value = value.strftime("%Y-%m-%d %H:%M:%S")
if key == "datePublished" or key == "dateUpdated":
value = value.strftime("%Y-%m-%d")
if removeallentities and value != None:
return removeAllEntities(value)
else:
return value
def getAllMetadata(self):
'''
All single value *and* list value metadata as strings.
'''
allmetadata = {}
for k in self.metadata.keys():
allmetadata[k] = self.getMetadata(k)
for l in self.listables.keys():
allmetadata[l] = self.getMetadata(l)
return allmetadata
def addToList(self,listname,value):
if value==None:
return
value = conditionalRemoveEntities(value)
if not self.listables.has_key(listname):
self.listables[listname]=[]
# prevent duplicates.
if not value in self.listables[listname]:
self.listables[listname].append(conditionalRemoveEntities(value))
self.listables[listname].append(value)
def getList(self,listname):
if not self.listables.has_key(listname):
+1 -1
View File
@@ -18,7 +18,7 @@
## This could (should?) use a dynamic loader like adapters, but for
## now, it's static, since there's so few of them.
from fanficdownloader.exceptions import FailedToDownload
from ..exceptions import FailedToDownload
from writer_html import HTMLWriter
from writer_txt import TextWriter
+37 -9
View File
@@ -24,8 +24,8 @@ import zipfile
from zipfile import ZipFile, ZIP_DEFLATED
import logging
from fanficdownloader.configurable import Configurable
from fanficdownloader.htmlcleanup import removeEntities, removeAllEntities, stripHTML
from ..configurable import Configurable
from ..htmlcleanup import removeEntities, removeAllEntities, stripHTML
class BaseStoryWriter(Configurable):
@@ -101,6 +101,12 @@ class BaseStoryWriter(Configurable):
}
self.story.setMetadata('formatname',self.getFormatName())
self.story.setMetadata('formatext',self.getFormatExt())
for tag in self.getConfigList("extratags"):
self.story.addToList("extratags",tag)
def getMetadata(self,key):
return stripHTML(self.story.getMetadata(key))
def getOutputFileName(self):
if self.getConfig('zip_output'):
@@ -115,7 +121,7 @@ class BaseStoryWriter(Configurable):
return self.formatFileName(self.getConfig('zip_filename'))
def formatFileName(self,template):
values = self.story.metadata
values = origvalues = self.story.getAllMetadata()
# fall back default:
if not template:
template="${title}-${siteabbrev}_${storyId}${formatext}"
@@ -123,7 +129,7 @@ class BaseStoryWriter(Configurable):
if not self.getConfig('allow_unsafe_filename'):
values={}
pattern = re.compile(r"[^a-zA-Z0-9_\. \[\]\(\)&'-]+")
for k in self.story.metadata.keys():
for k in origvalues.keys():
values[k]=re.sub(pattern,'_', removeAllEntities(self.story.getMetadata(k)))
return string.Template(template).substitute(values).encode('utf8')
@@ -180,12 +186,11 @@ class BaseStoryWriter(Configurable):
self._write(out,END.substitute(self.story.metadata))
# if no outstream is given, write to file.
def writeStory(self,outstream=None,metaonly=False):
for tag in self.getConfigList("extratags"):
self.story.addToList("extratags",tag)
def writeStory(self,outstream=None, metaonly=False, outfilename=None, forceOverwrite=False):
self.metaonly = metaonly
outfilename=self.getOutputFileName()
if outfilename == None:
outfilename=self.getOutputFileName()
if not outstream:
close=True
@@ -199,7 +204,7 @@ class BaseStoryWriter(Configurable):
os.mkdir(path) ## os.makedirs() doesn't work in 2.5.2?
## Check for output file date vs updated date here
if not self.getConfig('always_overwrite'):
if not (self.getConfig('always_overwrite') or forceOverwrite):
if os.path.exists(outfilename):
## date() truncs off time, which files have, but sites don't report.
lastupdated=self.story.getMetadataRaw('dateUpdated').date()
@@ -242,6 +247,29 @@ class BaseStoryWriter(Configurable):
if close:
outstream.close()
def getTags(self):
# set to avoid duplicates subject tags.
subjectset = set()
if self.story.getMetadataRaw('dateUpdated'):
# Last Update tags for Bill.
self.story.addToList('lastupdate',self.story.getMetadataRaw('dateUpdated').strftime("Last Update Year/Month: %Y/%m"))
self.story.addToList('lastupdate',self.story.getMetadataRaw('dateUpdated').strftime("Last Update: %Y/%m/%d"))
for entry in self.validEntries:
if entry in self.getConfigList("include_subject_tags") and \
entry not in self.story.getLists() and \
self.story.getMetadata(entry):
subjectset.add(self.getMetadata(entry))
# listables all go into dc:subject tags, but only if they are configured.
for (name,lst) in self.story.getLists().iteritems():
if name in self.getConfigList("include_subject_tags"):
for tag in lst:
subjectset.add(tag)
return list(subjectset)
def writeStoryImpl(self, out):
"Must be overriden by sub classes."
pass
+8 -23
View File
@@ -26,7 +26,7 @@ from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
from xml.dom.minidom import parse, parseString, getDOMImplementation
from base_writer import *
from fanficdownloader.htmlcleanup import stripHTML
from ..htmlcleanup import stripHTML
class EpubWriter(BaseStoryWriter):
@@ -41,7 +41,10 @@ class EpubWriter(BaseStoryWriter):
def __init__(self, config, story):
BaseStoryWriter.__init__(self, config, story)
self.EPUB_CSS='''body { margin-left: 2%; margin-right: 2%; margin-top: 2%; margin-bottom: 2%; text-align: justify; }
self.EPUB_CSS = string.Template('''
body { margin: 2%;
text-align: justify;
background-color: #${background_color}; }
pre { font-size: x-small; }
sml { font-size: small; }
h1 { text-align: center; }
@@ -63,7 +66,7 @@ h6 { text-align: center; }
.smcap {font-variant: small-caps;}
.u {text-decoration: underline;}
.bold {font-weight: bold;}
'''
''')
self.EPUB_TITLE_PAGE_START = string.Template('''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
@@ -153,9 +156,6 @@ h6 { text-align: center; }
</html>
''')
def getMetadata(self,key):
return stripHTML(self.story.getMetadata(key))
def writeStoryImpl(self, out):
## Python 2.5 ZipFile is rather more primative than later
@@ -249,27 +249,12 @@ h6 { text-align: center; }
metadata.appendChild(newTag(contentdom,"meta",
attrs={"name":"calibre:timestamp",
"content":self.story.getMetadataRaw('dateUpdated').strftime("%Y-%m-%dT%H:%M:%S")}))
# Last Update tags for Bill.
self.story.addToList('lastupdate',self.story.getMetadataRaw('dateUpdated').strftime("Last Update Year/Month: %Y/%m"))
self.story.addToList('lastupdate',self.story.getMetadataRaw('dateUpdated').strftime("Last Update: %Y/%m/%d"))
if self.getMetadata('description'):
metadata.appendChild(newTag(contentdom,"dc:description",text=
self.getMetadata('description')))
# set to avoid duplicates subject tags.
subjectset = set()
for entry in self.validEntries:
if entry in self.getConfigList("include_subject_tags") and \
entry not in self.story.getLists() and \
self.story.getMetadata(entry):
subjectset.add(self.getMetadata(entry))
# listables all go into dc:subject tags, but only if they are configured.
for (name,lst) in self.story.getLists().iteritems():
if name in self.getConfigList("include_subject_tags"):
for tag in lst:
subjectset.add(tag)
for subject in subjectset:
for subject in self.getTags():
metadata.appendChild(newTag(contentdom,"dc:subject",text=subject))
@@ -376,7 +361,7 @@ h6 { text-align: center; }
del tocncxdom
# write stylesheet.css file.
outputepub.writestr("OEBPS/stylesheet.css",self.EPUB_CSS)
outputepub.writestr("OEBPS/stylesheet.css",self.EPUB_CSS.substitute({"background_color":self.getConfig("background_color")}))
# write title page.
if self.getConfig("titlepage_use_table"):
+4
View File
@@ -39,6 +39,7 @@ class HTMLWriter(BaseStoryWriter):
<head>
<title>${title} by ${author}</title>
<style type="text/css">
body { background-color: #${background_color}; }
.CI {
text-align:center;
margin-top:0px;
@@ -94,6 +95,9 @@ class HTMLWriter(BaseStoryWriter):
def writeStoryImpl(self, out):
# minor cheat, tucking bg into metadata.
if self.getConfig("background_color"):
self.story.metadata["background_color"] = self.getConfig("background_color")
self._write(out,self.HTML_FILE_START.substitute(self.story.metadata))
self.writeTitlePage(out,
+2 -5
View File
@@ -20,8 +20,8 @@ import string
import StringIO
from base_writer import *
from fanficdownloader.htmlcleanup import stripHTML
from fanficdownloader.mobi import Converter
from ..htmlcleanup import stripHTML
from ..mobi import Converter
class MobiWriter(BaseStoryWriter):
@@ -124,9 +124,6 @@ class MobiWriter(BaseStoryWriter):
</html>
''')
def getMetadata(self,key):
return stripHTML(self.story.getMetadata(key))
def writeStoryImpl(self, out):
files = []
+1 -1
View File
@@ -21,7 +21,7 @@ from textwrap import wrap
from base_writer import *
from fanficdownloader.html2text import html2text, BODY_WIDTH
from ..html2text import html2text, BODY_WIDTH
## In BaseStoryWriter, we define _write to encode <unicode> objects
## back into <string> for true output. But txt needs to write the
+52 -20
View File
@@ -54,33 +54,53 @@
much easier. </p>
</div>
<!-- put announcements here, h3 is a good title size. -->
<h3>Please Switch to <a href="http://fanfictiondownloader.appspot.com/">New Version</a></h3>
<h3>This is the Official Multithreading Version</h3>
<p>
We have a new, more efficient, version of the system up now. Please start using the
<a href="http://fanfictiondownloader.appspot.com/">New
Version here</a>. If for some reason, the new version
doesn't work for you, please let us know on
the <a href="http://groups.google.com/group/fanfic-downloader">Fanfiction
Downloader Google Group</a>.
</p>
<h3>New Google Quotas</h3>
<p>
Google has changed their quota limits for free
applications using their AppEngine system, like this one.
We expect that there will be times when the
system exceeds it's permitted processing quota.
This version of the application uses Python 2.7 and
multithreading to try and reduce our usage. Google
considers Python 2.7 Experimental still, so there may be issues.
</p>
<p>
You also have the option of running the downloader on your
own computer if you have Python available.
<a href="http://code.google.com/p/fanficdownloader/downloads/list">Download here.</a>
<b>FanFictionDownLoader calibre Plugin</b>
<br /><br />
There's now a version of this downloader that runs
entirely inside the
popular <a href="http://calibre-ebook.com/">calibre</a>
ebook management package as a plugin.
<br /><br />
Once you have calibre installed and running, inside
calibre, you can go to 'Get plugins to enhance calibre' or
'Get new plugins' and
install <a href="http://www.mobileread.com/forums/showthread.php?t=163261">FanFictionDownLoader</a>.
</p>
<p>
<b>Wider support for 'Characters'</b>
<br /><br />
We now collect 'Character' lists for most of the supported sites.
<br /><br />
There's huge variation in how different sites choose to define characters--well beyond our ability to standardize.
<br /><br />
So if you don't like and don't want to see characters in your title pages or tags, you can turn them off in your configuration with:
<pre>
[defaults]
# note that characters is removed. This removes it from your title page.
titlepage_entries: category,genre,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,numWords,site,description
# note that characters is removed. This removes it from your tags.
include_subject_tags: extratags, genre, category, status
</pre>
If you like them for some sites, but not others, just copy the titlepage_entries and include_subject_tags to sections for different sites and edit to taste.
</p>
<p>
If you have any problems with this application, please
report them in
the <a href="http://groups.google.com/group/fanfic-downloader">Fanfiction
Downloader Google Group</a>. The
<a href="http://4-0-6.fanfictionloader.appspot.com">Previous
<a href="http://4-2-0.fanfictiondownloader.appspot.com">Previous
Version</a> is also available for you to use if necessary.
</p>
<div id='error'>
@@ -201,10 +221,10 @@
Use the URL of the story's chapter list, such as
<br /><a href="http://fanfiction.tenhawkpresents.com/viewstory.php?sid=294">http://fanfiction.tenhawkpresents.com/viewstory.php?sid=294</a>.
</dd>
<dt>fanfic.castletv.net</dt>
<dt>castlefans.org</dt>
<dd>
Use the URL of the story's chapter list, such as
<br /><a href="http://fanfic.castletv.net/viewstory.php?sid=123">http://fanfic.castletv.net/viewstory.php?sid=123</a>.
<br /><a href="http://castlefans.org/fanfic/viewstory.php?sid=123">http://castlefans.org/fanfic/viewstory.php?sid=123</a>.
</dd>
<dt>fimfiction.net</dt>
<dd>
@@ -213,6 +233,18 @@
<br /> or the URL of any chapter, such as
<br /><a href="http://www.fimfiction.com/story/123/1/">http://www.fimfiction.com/story/123/1/</a>.
</dd>
<dt>tthfanfic.org</dt>
<dd>
Use the URL of any story, with or without chapter, title and notice, such as
<br /><a href="http://www.tthfanfic.org/Story-5583">http://www.tthfanfic.org/Story-5583</a>
<br /><a href="http://www.tthfanfic.org/Story-5583/Greywizard+Marked+By+Kane.htm">http://www.tthfanfic.org/Story-5583/Greywizard+Marked+By+Kane.htm</a>.
<br /><a href="http://www.tthfanfic.org/T-99999999/Story-26448-15/batzulger+Willow+Rosenberg+and+the+Mind+Riders.htm">http://www.tthfanfic.org/T-99999999/Story-26448-15/batzulger+Willow+Rosenberg+and+the+Mind+Riders.htm</a>.
</dd>
<dt>www.siye.co.uk</dt>
<dd>
Use the URL of the story's chapter list, such as
<br /><a href="http://www.siye.co.uk/siye/viewstory.php?sid=123">http://www.siye.co.uk/siye/viewstory.php?sid=123</a>.
</dd>
</dl>
+23 -28
View File
@@ -41,23 +41,24 @@ import ConfigParser
## Console page first, you will get a django version mismatch error when you
## to go hit one of the application pages. Just change a file again, and
## make sure to hit an app page before the SDK page to clear it.
os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
from google.appengine.dist import use_library
use_library('django', '1.2')
#os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
#from google.appengine.dist import use_library
#use_library('django', '1.2')
from google.appengine.ext import db
from google.appengine.api import taskqueue
from google.appengine.api import users
from google.appengine.ext import webapp
#from google.appengine.ext import webapp
import webapp2
from google.appengine.ext.webapp import template
from google.appengine.ext.webapp import util
#from google.appengine.ext.webapp2 import util
from google.appengine.runtime import DeadlineExceededError
from ffstorage import *
from fanficdownloader import adapters, writers, exceptions
class UserConfigServer(webapp.RequestHandler):
class UserConfigServer(webapp2.RequestHandler):
def getUserConfig(self,user):
config = ConfigParser.SafeConfigParser()
@@ -73,7 +74,7 @@ class UserConfigServer(webapp.RequestHandler):
return config
class MainHandler(webapp.RequestHandler):
class MainHandler(webapp2.RequestHandler):
def get(self):
user = users.get_current_user()
if user:
@@ -160,7 +161,7 @@ class EditConfigServer(UserConfigServer):
self.response.out.write(template.render(path, template_values))
class FileServer(webapp.RequestHandler):
class FileServer(webapp2.RequestHandler):
def get(self):
fileId = self.request.get('id')
@@ -221,7 +222,7 @@ class FileServer(webapp.RequestHandler):
path = os.path.join(os.path.dirname(__file__), 'status.html')
self.response.out.write(template.render(path, template_values))
class FileStatusServer(webapp.RequestHandler):
class FileStatusServer(webapp2.RequestHandler):
def get(self):
user = users.get_current_user()
if not user:
@@ -257,7 +258,7 @@ class FileStatusServer(webapp.RequestHandler):
path = os.path.join(os.path.dirname(__file__), 'status.html')
self.response.out.write(template.render(path, template_values))
class ClearRecentServer(webapp.RequestHandler):
class ClearRecentServer(webapp2.RequestHandler):
def get(self):
user = users.get_current_user()
if not user:
@@ -282,7 +283,7 @@ class ClearRecentServer(webapp.RequestHandler):
logging.info('Deleted %d instances download.' % num)
self.redirect("/?error=recentcleared")
class RecentFilesServer(webapp.RequestHandler):
class RecentFilesServer(webapp2.RequestHandler):
def get(self):
user = users.get_current_user()
if not user:
@@ -556,20 +557,14 @@ def urlEscape(data):
p = re.compile(r'([^\w])')
return p.sub(toPercentDecimal, data.encode("utf-8"))
def main():
application = webapp.WSGIApplication([('/', MainHandler),
('/fdowntask', FanfictionDownloaderTask),
('/fdown', FanfictionDownloader),
(r'/file.*', FileServer),
('/status', FileStatusServer),
('/recent', RecentFilesServer),
('/editconfig', EditConfigServer),
('/clearrecent', ClearRecentServer),
],
debug=False)
util.run_wsgi_app(application)
if __name__ == '__main__':
logging.getLogger().setLevel(logging.DEBUG)
main()
logging.getLogger().setLevel(logging.DEBUG)
app = webapp2.WSGIApplication([('/', MainHandler),
('/fdowntask', FanfictionDownloaderTask),
('/fdown', FanfictionDownloader),
(r'/file.*', FileServer),
('/status', FileStatusServer),
('/recent', RecentFilesServer),
('/editconfig', EditConfigServer),
('/clearrecent', ClearRecentServer),
],
debug=False)
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# epubmerge.py 1.0
# Copyright 2011, Jim Miller
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from glob import glob
from makezip import createZipFile
if __name__=="__main__":
filename="FanFictionDownLoader.zip"
exclude=['*.pyc','*~','*.xcf']
# from top dir. 'w' for overwrite
createZipFile(filename,"w",
['plugin-defaults.ini','plugin-example.ini','epubmerge.py','fanficdownloader'],
exclude=exclude)
#from calibre-plugin dir. 'a' for append
os.chdir('calibre-plugin')
files=['about.txt','images',]
files.extend(glob('*.py'))
files.extend(glob('plugin-import-name-*.txt'))
createZipFile("../"+filename,"a",
files,exclude=exclude)
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# epubmerge.py 1.0
# Copyright 2011, Jim Miller
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os, zipfile, sys
from glob import glob
def addFolderToZip(myZipFile,folder,exclude=[]):
folder = folder.encode('ascii') #convert path to ascii for ZipFile Method
excludelist=[]
for ex in exclude:
excludelist.extend(glob(folder+"/"+ex))
for file in glob(folder+"/*"):
if file in excludelist:
continue
if os.path.isfile(file):
#print file
myZipFile.write(file, file, zipfile.ZIP_DEFLATED)
elif os.path.isdir(file):
addFolderToZip(myZipFile,file,exclude=exclude)
def createZipFile(filename,mode,files,exclude=[]):
myZipFile = zipfile.ZipFile( filename, mode ) # Open the zip file for writing
excludelist=[]
for ex in exclude:
excludelist.extend(glob(ex))
for file in files:
if file in excludelist:
continue
file = file.encode('ascii') #convert path to ascii for ZipFile Method
if os.path.isfile(file):
(filepath, filename) = os.path.split(file)
#print file
myZipFile.write( file, filename, zipfile.ZIP_DEFLATED )
if os.path.isdir(file):
addFolderToZip(myZipFile,file,exclude=exclude)
myZipFile.close()
return (1,filename)
+246
View File
@@ -0,0 +1,246 @@
# Copyright 2012 Fanficdownloader team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
[defaults]
## [defaults] section applies to all formats and sites but may be
## overridden at several levels
## Some sites also require the user to confirm they are adult for
## adult content. In commandline version, this should go in your
## personal.ini, not defaults.ini.
#is_adult:true
## All available titlepage_entries and the label used for them:
## <entryname>_label:<label>
## Labels may be customized.
title_label:Title
storyUrl_label:Story URL
description_label:Summary
author_label:Author
authorUrl_label:Author URL
## epub, txt, html
formatname_label:File Format
## .epub, .txt, .html
formatext_label:File Extension
## Category and Genre have overlap, depending on the site.
## Sometimes Harry Potter is a category and Fantasy a genre. (fanfiction.net)
## Sometimes Fantasy is category *and* a genre (fictionpress.com)
## Sometimes there are multiple categories and/or genres.
category_label:Category
genre_label:Genre
characters_label:Characters
## Completed/In-Progress
status_label:Status
## Dates story first published, last updated, and downloaded(last with time).
datePublished_label:Published
dateUpdated_label:Updated
dateCreated_label:Packaged
## Rating depends on the site. Some use K,T,M,etc, and some PG,R,NC-17
rating_label:Rating
## Also depends on the site.
warnings_label:Warnings
numChapters_label:Chapters
numWords_label:Words
## www.fanfiction.net, fictionalley.com, etc.
site_label:Publisher
## ffnet, fpcom, etc.
siteabbrev_label:Site Abbrev
## The site's unique story/author identifier. Usually a number.
storyId_label:Story ID
authorId_label:Author ID
## Primarily to put specific values in dc:subject tags for epub. Will
## show up in Calibre as tags. Also carried into mobi when converted.
extratags_label:Extra Tags
## The version of fanficdownloader
##
version_label:FFDL Version
## items to include in the title page
## Empty entries will *not* appear, even if in the list.
## All current formats already include title and author.
titlepage_entries: category,genre,characters,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,numWords,site,description
## include title page as first page.
include_titlepage: true
## include a TOC page before the story text
include_tocpage: true
## website encoding(s) In theory, each website reports the character
## encoding they use for each page. In practice, some sites report it
## incorrectly. Each adapter has a default list, usually "utf8,
## Windows-1252" or "Windows-1252, utf8", but this will let you
## explicitly set the encoding and order if you need to. The special
## value 'auto' will call chardet and use the encoding it reports if
## it has +90% confidence. 'auto' is not reliable.
#website_encodings: auto, utf8, Windows-1252
## entries to make epub subjects and calibre tags
## lastupdate creates two tags: "Last Update Year/Month: %Y/%m" and "Last Update: %Y/%m/%d"
include_subject_tags: extratags, genre, category, characters, status
## extra tags (comma separated) to include, primarily for epub.
extratags: FanFiction
## number of seconds to sleep between calls to the story site. May by
## useful if pulling large numbers of stories or if the site is slow.
#slow_down_sleep_time:0.5
## output background color--only used by html and epub (and ignored in
## epub by many readers). Must be hex code, # will be added.
background_color: ffffff
## Each output format has a section that overrides [defaults]
[html]
[txt]
## Add URLs since there aren't links.
titlepage_entries: category,genre,status,datePublished,dateUpdated,dateCreated,rating,warnings,numChapters,numWords,site,storyUrl, authorUrl, description
## use \r\n for line endings, the windows convention. text output only.
windows_eol: true
[epub]
## epub carries the TOC in metadata.
## mobi generated from epub will have a TOC at the end.
include_tocpage: false
## epub->mobi conversions typically don't like tables.
titlepage_use_table: false
## When using tables, make these span both columns.
wide_titlepage_entries: description, storyUrl, author URL
[mobi]
## mobi TOC cannot be turned off right now.
#include_tocpage: true
## Each site has a section that overrides [defaults] *and* the format
## sections test1.com specifically is not a real story site. Instead,
## it is a fake site for testing configuration and output. It uses
## URLs like: http://test1.com?sid=12345
[test1.com]
extratags: FanFiction,Testing
## If necessary, you can define [<site>:<format>] sections to
## customize the formats differently for the same site. Overrides
## defaults, format and site.
[test1.com:txt]
extratags: FanFiction,Testing,Text
[test1.com:html]
extratags: FanFiction,Testing,HTML
[www.fanfiction.net]
[www.fictionpress.com]
## Clear FanFiction from defaults, fictionpress.com is original fiction.
extratags:
[www.ficwad.com]
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
## defaults.ini.
#username:YourName
#password:yourpassword
[www.twilighted.net]
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
## defaults.ini.
#username:YourName
#password:yourpassword
[www.twiwrite.net]
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
## defaults.ini.
#username:YourName
#password:yourpassword
[www.whofic.com]
[www.mediaminer.org]
[www.thewriterscoffeeshop.com]
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
## defaults.ini.
#username:YourName
#password:yourpassword
## Some sites also require the user to confirm they are adult for
## adult content. In commandline version, this should go in your
## personal.ini, not defaults.ini.
#is_adult:true
[www.ficwad.com]
## Some sites require login (or login for some rated stories) The
## program can prompt you, or you can save it in config. In
## commandline version, this should go in your personal.ini, not
## defaults.ini.
#username:YourName
#password:yourpassword
[www.adastrafanfic.com]
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[www.fictionalley.org]
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[www.harrypotterfanfiction.com]
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[www.fimfiction.net]
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
[www.tthfanfic.org]
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content. In commandline version,
## this should go in your personal.ini, not defaults.ini.
#is_adult:true
## tth is a little unusual--it doesn't require user/pass, but the site
## keeps track of which chapters you've read and won't send another
## update until it thinks you're up to date. This way, on download,
## it thinks you're up to date.
#username:YourName
#password:yourpassword
[overrides]
## It may sometimes be useful to override all of the specific format,
## site and site:format sections in your private configuration. For
## example, this extratags param here would override all of the
## extratags params in all other sections. Only commandline options
## beat overrides.
#extratags:fanficdownloader
+55
View File
@@ -0,0 +1,55 @@
## This is an example of what your personal configuration might look
## like.
[defaults]
## Some sites also require the user to confirm they are adult for
## adult content. In commandline version, this should go in your
## personal.ini, not defaults.ini.
#is_adult:true
## Most common, I expect will be using this to save username/passwords
## for different sites.
[www.twilighted.net]
#username:YourPenname
#password:YourPassword
[www.ficwad.com]
#username:YourUsername
#password:YourPassword
[www.twiwrite.net]
#username:YourName
#password:yourpassword
[www.adastrafanfic.com]
## Some sites do not require a login, but do require the user to
## confirm they are adult for adult content.
#is_adult:true
[www.thewriterscoffeeshop.com]
#username:YourName
#password:yourpassword
#is_adult:true
[www.fictionalley.org]
#is_adult:true
[www.harrypotterfanfiction.com]
#is_adult:true
[www.fimfiction.net]
#is_adult:true
[www.tthfanfic.org]
#is_adult:true
## tth is a little unusual--it doesn't require user/pass, but the site
## keeps track of which chapters you've read and won't send another
## update until it thinks you're up to date. This way, on download,
## it thinks you're up to date.
#username:YourName
#password:yourpassword
## This section will override anything in the system defaults or other
## sections here.
[overrides]
+20 -9
View File
@@ -43,17 +43,28 @@
<div id='greeting'>
<p>Hi, {{ nickname }}! These are the fanfics you've recently requested.</p>
<p><a href="/clearrecent">Clear your Recent Downloads List</a></p>
<h3>Please Switch to <a href="http://fanfictiondownloader.appspot.com/">New Version</a></h3>
<p>
We have a new, more efficient, version of the system up now. Please start using the
<a href="http://fanfictiondownloader.appspot.com/">New
Version here</a>. If for some reason, the new version
doesn't work for you, please let us know on
the <a href="http://groups.google.com/group/fanfic-downloader">Fanfiction
Downloader Google Group</a>.
</p>
</div>
</div>
<div id='typebox'>
<p>
<b>FanFictionDownLoader calibre Plugin</b>
<br /><br />
There's now a version of this downloader that runs
entirely inside the
popular <a href="http://calibre-ebook.com/">calibre</a>
ebook management package as a plugin.
<br /><br />
Once you have calibre installed and running, inside
calibre, you can go to 'Get plugins to enhance calibre' or
'Get new plugins' and
install <a href="http://www.mobileread.com/forums/showthread.php?t=163261">FanFictionDownLoader</a>.
</p>
</div>
<div id='helpbox'>
{% for fic in fics %}
+18 -7
View File
@@ -66,14 +66,25 @@
</div>
{% endif %}
<p>See your personal list of <a href="/recent">previously downloaded fanfics</a>.</p>
<h3>Please Switch to <a href="http://fanfictiondownloader.appspot.com/">New Version</a></h3>
</div>
<div id='typebox'>
<p>
We have a new, more efficient, version of the system up now. Please start using the
<a href="http://fanfictiondownloader.appspot.com/">New
Version here</a>. If for some reason, the new version
doesn't work for you, please let us know on
the <a href="http://groups.google.com/group/fanfic-downloader">Fanfiction
Downloader Google Group</a>.
<b>FanFictionDownLoader calibre Plugin</b>
<br /><br />
There's now a version of this downloader that runs
entirely inside the
popular <a href="http://calibre-ebook.com/">calibre</a>
ebook management package as a plugin.
<br /><br />
Once you have calibre installed and running, inside
calibre, you can go to 'Get plugins to enhance calibre' or
'Get new plugins' and
install <a href="http://www.mobileread.com/forums/showthread.php?t=163261">FanFictionDownLoader</a>.
</p>
</div>
<div style='text-align: center'>
+11 -14
View File
@@ -25,15 +25,16 @@ Copyright 2011 Fanficdownloader team
import datetime
import logging
from google.appengine.ext.webapp import util
from google.appengine.ext import webapp
#from google.appengine.ext.webapp import util
import webapp2
#from google.appengine.ext import webapp
from google.appengine.api import users
from google.appengine.api import taskqueue
from google.appengine.api import memcache
from ffstorage import *
class Remover(webapp.RequestHandler):
class Remover(webapp2.RequestHandler):
def get(self):
logging.debug("Starting r3m0v3r")
user = users.get_current_user()
@@ -56,9 +57,10 @@ class Remover(webapp.RequestHandler):
logging.debug('Delete '+d.url)
logging.info('Deleted instances: %d' % num)
self.response.headers['Content-Type'] = 'text/html'
self.response.out.write('Deleted instances: %d<br>' % num)
class RemoveOrphanDataChunks(webapp.RequestHandler):
class RemoveOrphanDataChunks(webapp2.RequestHandler):
def get(self):
logging.debug("Starting RemoveOrphanDataChunks")
@@ -98,15 +100,10 @@ class RemoveOrphanDataChunks(webapp.RequestHandler):
memcache.set('orphan_search_cursor',chunks.cursor())
logging.info('Deleted %d orphan chunks from %d total.' % (deleted,num))
self.response.headers['Content-Type'] = 'text/html'
self.response.out.write('Deleted %d orphan chunks from %d total.' % (deleted,num))
def main():
application = webapp.WSGIApplication([('/r3m0v3r', Remover),
('/r3m0v3rOrphans', RemoveOrphanDataChunks)],
debug=False)
util.run_wsgi_app(application)
if __name__ == '__main__':
logging.getLogger().setLevel(logging.DEBUG)
main()
logging.getLogger().setLevel(logging.DEBUG)
app = webapp2.WSGIApplication([('/r3m0v3r', Remover),
('/r3m0v3rOrphans', RemoveOrphanDataChunks)],
debug=False)
+8 -13
View File
@@ -18,15 +18,16 @@
import datetime
import logging
from google.appengine.ext.webapp import util
from google.appengine.ext import webapp
#from google.appengine.ext.webapp import util
import webapp2
#from google.appengine.ext import webapp
from google.appengine.api import users
from google.appengine.api import taskqueue
from google.appengine.api import memcache
from ffstorage import *
class Tally(webapp.RequestHandler):
class Tally(webapp2.RequestHandler):
def get(self):
logging.debug("Starting Tally")
user = users.get_current_user()
@@ -57,13 +58,7 @@ class Tally(webapp.RequestHandler):
logging.info('Tallied %d fics.' % num)
self.response.out.write('<br/>Tallied %d fics.<br/>' % num)
def main():
application = webapp.WSGIApplication([('/tally', Tally),
],
debug=False)
util.run_wsgi_app(application)
if __name__ == '__main__':
logging.getLogger().setLevel(logging.DEBUG)
main()
logging.getLogger().setLevel(logging.DEBUG)
app = webapp2.WSGIApplication([('/tally', Tally),
],
debug=False)