Compare commits

..
465 Commits
Author SHA1 Message Date
Ruud 8d108b92bf One Up 2013-09-23 21:48:12 +02:00
Ruud 46783028b1 Merge branch 'refs/heads/develop' into desktop 2013-09-23 21:36:45 +02:00
Ruud b5d2a41d60 Enable NewzNab bij default 2013-09-23 21:35:40 +02:00
Ruud cc3aad49ed Remove FTDWorld 2013-09-23 21:35:29 +02:00
Ruud 2365e1859f Don't show suggestions if there aren't any. fix #2153 2013-09-22 10:47:13 +02:00
Ruud 03700e0a04 Userscript image didn't show 2013-09-22 00:43:50 +02:00
Ruud 1ff4901846 Make sure to remove listener, even after fail 2013-09-21 22:29:15 +02:00
Ruud d70a71a12e Make nonblock debug message 2013-09-21 22:17:01 +02:00
Ruud 866d9621cb Create new listener list 2013-09-21 22:16:44 +02:00
Ruud 2d3fc03a00 Revert back to UTF8 when ss encoding fails. fix #2220 2013-09-21 13:56:17 +02:00
Ruud 19f782e4a5 Don't try to change elements that don't exist. fix #2219 2013-09-21 12:41:06 +02:00
Ruud fdd851d29a Binsearch age parse failed for release new than 1 day. fix #2217 2013-09-21 12:14:40 +02:00
Ruud 6cd38a3469 Providers missing in wizard 2013-09-21 11:20:53 +02:00
Ruud bfa3b87188 Only show soon and late with no releases 2013-09-21 11:07:16 +02:00
Ruud 69a9fa1193 Simplify string before checking on imdb 2013-09-20 18:08:27 +02:00
Ruud 9e0805ec89 Hide IE clear button on search 2013-09-20 18:08:12 +02:00
Ruud d08c7c57a8 One up! 2013-09-20 17:46:54 +02:00
Ruud eeeb845ef3 Simplify string before checking on imdb 2013-09-20 17:30:11 +02:00
Ruud 651a063f94 Fix about submenu 2013-09-20 16:33:01 +02:00
Ruud f20aaa2d9d Hide IE clear button on search 2013-09-20 16:23:42 +02:00
Ruud ba925ec191 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	couchpotato/core/plugins/suggestion/main.py
2013-09-20 16:12:40 +02:00
Ruud f67c6fe8be Only remove images from cache folder on cleanup 2013-09-20 16:07:18 +02:00
Ruud 8d38fa87a4 Copy unrar dll to cache folder. fix #2205 2013-09-20 16:06:23 +02:00
Ruud 7c79c6d1f3 Update TorrentShack url. fix #2209 2013-09-20 12:51:58 +02:00
Ruud b0781b45f8 Different seperator for folder and filename 2013-09-19 23:49:23 +02:00
Ruud Burger ee53539906 Merge pull request #2163 from mano3m/develop_utorrent
Fix folder issue uTorrent
2013-09-19 14:40:16 -07:00
Ruud c8ab6a06fb ASCII encode md5 string. closes #2167 2013-09-19 23:39:15 +02:00
Ruud c75ac51eb7 Try the info dict to get title. fix #2206 2013-09-19 23:29:21 +02:00
Ruud 33d7d994d4 Don't try to finish an already closed connection 2013-09-19 23:16:49 +02:00
Ruud 96291f63da Create db backup dir before trying to use it. fix #2207 2013-09-19 22:11:10 +02:00
Ruud 6464bb065d Better year guessing. fix #609 2013-09-18 23:04:54 +02:00
Ruud 8b45b6f1a0 Only backup database max once an hour. fix #1218 2013-09-18 22:07:07 +02:00
Ruud 70ba5d80cd Trailers not downloading. fix #1563 2013-09-18 21:42:25 +02:00
Ruud ac30152930 Don't start new long-poll right away. 2013-09-17 21:45:43 +02:00
Ruud ad01a3da4d Update GuessIt 2013-09-17 21:04:15 +02:00
Ruud 5f5f17112a Don't try to search SceneAccess for BR-Disk. fix #2188 2013-09-17 20:48:01 +02:00
Ruud 156da670e8 Encode before checking imdb content. fix #2186 2013-09-17 20:43:41 +02:00
Ruud 821c26f35b Return default cached suggestion list. fix #2191 2013-09-17 20:39:20 +02:00
Ruud a092f394fa Snatch next didn't pick correct element 2013-09-17 20:18:41 +02:00
Ruud 18e3194e27 Better category defaults 2013-09-16 22:37:10 +02:00
Ruud 08a1e1e582 Done use faulty None value for category 2013-09-16 22:33:45 +02:00
Ruud 074005ed02 Use existing category on re-add. fix #2182 2013-09-16 22:33:26 +02:00
Ruud Burger 7660a3d78f Merge pull request #2180 from techmunk/2107
Deluge SSL negotiation errors on Windows machines.
2013-09-16 13:09:49 -07:00
Techmunk 9211e60804 Use the actual SSLv3 constant in deluge transfer.py. 2013-09-17 00:06:35 +10:00
Techmunk 87f295be28 Fix Deluge SSL negotiation errors on Windows machines. 2013-09-16 23:12:46 +10:00
mano3m cfa89c8921 [uTorrent] Guarantee a folder
uTorrent does not create a folder in case only one file is present in
the torrent. This is a workaround that detects torrents with one file.
It then removes the torrent and readds it with a specified subfolder.
2013-09-15 10:01:59 +02:00
Ruud 70f834d925 Gilles de la Tourette 2013-09-15 00:46:39 +02:00
Ruud 6b4e4fd440 Only show login when both username and password are filled in. fix #2157 2013-09-14 11:41:16 +02:00
Ruud b83b2453a0 not in 2013-09-12 22:50:08 +02:00
Ruud 82d31d996d Set order changes on each run. fix #2148 2013-09-12 22:29:59 +02:00
Ruud 4faa617039 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2013-09-12 11:08:14 +02:00
Ruud a1d2276668 Match variable name in ubuntu init. fix #2149 2013-09-12 11:07:49 +02:00
Ruud 19c50f728e Suggestions, mark as seen. 2013-09-11 22:41:38 +02:00
Ruud a94307c59f rTorrent import cleanup 2013-09-11 21:33:11 +02:00
Ruud c6403e87f1 Get releases when cleaning up managed movies 2013-09-11 12:24:50 +02:00
Ruud b56cd3439e added_identifiers needs to be mutable. fix #2140 #2141 2013-09-11 09:28:30 +02:00
Ruud 25693d44eb Count NONE as success for NZBGet. fix #2135 2013-09-11 09:07:32 +02:00
Ruud 43af25a30e Fix menu phone styling 2013-09-10 23:50:17 +02:00
Ruud 023278e0c0 Remove webkit button styling 2013-09-10 23:32:51 +02:00
Ruud 0634c79f74 Give minified own FileHandler 2013-09-10 23:21:31 +02:00
Ruud 31b3c2ef64 Change static path 2013-09-10 22:59:31 +02:00
Ruud 4a71f2c556 Login styling 2013-09-10 22:58:41 +02:00
Ruud 9783409756 Login base 2013-09-10 18:02:04 +02:00
Ruud Burger c7e85c00ca Merge pull request #2133 from mythin/fix-variable-change
Fix the variable passed to the getImdb method
2013-09-09 23:32:14 -07:00
Mythin 94647bbb57 Fix the variable passed to the getImdb method 2013-09-09 23:08:49 -07:00
Ruud 1aa26a5a6c Replace protocol if it doesn't exist 2013-09-09 22:28:21 +02:00
Ruud df13a0edc2 Ignore modules with only .pyc files in them. 2013-09-08 22:12:08 +02:00
Ruud 52a0de3b59 Deleting from late block didn't work 2013-09-06 23:12:22 +02:00
Ruud 38886b28f7 Hide soon and late blocks on dashboard if their empty. fix #1778 2013-09-06 23:05:41 +02:00
Ruud 226cf6fc38 Make sure to not query db when there aren't any ids 2013-09-06 22:45:37 +02:00
Ruud 203a52bfd1 Don't load updater.js twice 2013-09-06 20:17:21 +02:00
Ruud 1b6bf13619 Optimize and order dashboard list 2013-09-06 20:03:34 +02:00
Ruud bc94e90994 Optimize available char listing 2013-09-06 19:37:39 +02:00
Ruud 347125365f movie.list didn't keep order 2013-09-06 19:19:20 +02:00
Ruud 59a718be20 Optimize events with single handler 2013-09-06 00:41:15 +02:00
Ruud c41b3a612a Optimize dashboard soon listing 2013-09-06 00:24:17 +02:00
Ruud 23f77df911 Optimize profile queries 2013-09-06 00:23:52 +02:00
Ruud 117b952455 Default back to type on protocol. fix #2120 2013-09-05 21:46:00 +02:00
Ruud 7714504831 Run dashboard calls serial 2013-09-04 23:20:03 +02:00
Ruud 5c61c24c04 Lazyload file list in manage tab 2013-09-04 22:39:42 +02:00
Ruud b11e1d48e0 Suggestion listing: load library in single query 2013-09-04 22:30:32 +02:00
Ruud a6ce114284 Optimize suggestion listing 2013-09-04 22:30:32 +02:00
Ruud 88d512eacc Don't try to use releases when there aren't any 2013-09-04 22:30:32 +02:00
Ruud f4d5366c93 Remove profile from dashboard list 2013-09-04 22:30:32 +02:00
Ruud ac9aaec7b8 Optimize movie.list 2013-09-04 22:30:32 +02:00
Ruud 0c5b950c87 Add manual to tryNextRelease 2013-09-04 22:30:32 +02:00
Ruud 47141f8e4f Api: added release.for_movie
Get all releases for a single movie
2013-09-04 22:30:32 +02:00
Ruud ec302fe665 Make sure that a faulty api call end after error 2013-09-04 13:46:51 +02:00
Ruud 7f304b0c28 Don't load profile on movie list 2013-09-03 22:50:27 +02:00
Ruud 8f88f7d89b Javascript and css cleanup 2013-09-03 22:13:42 +02:00
Ruud 400fd461ab Always add timestamp to registered statics 2013-09-03 21:12:22 +02:00
Ruud cd8d2d4808 PublicHD description cache timeout 2013-09-03 20:23:40 +02:00
Ruud 4cfa79488f PublicHD cache description call 2013-09-03 20:21:49 +02:00
Ruud b5993bcc21 NonBlock calls need to finish 2013-09-03 19:14:59 +02:00
Ruud 6af00bf026 Standardize cache_key generation 2013-09-03 12:48:24 +02:00
Ruud 97c456c9e1 Optimize quality caching 2013-09-03 12:47:44 +02:00
Ruud 08f44197f3 Use own cache 2013-09-03 12:14:02 +02:00
Ruud 779c7d2942 Remove mutable objects from function args 2013-09-02 22:44:44 +02:00
Ruud 7fd14e0283 Code cleanup 2013-09-02 21:59:06 +02:00
Ruud 7d32a8750d type > protocol 2013-09-02 16:53:39 +02:00
Ruud 110e0b78fc Merge branch 'file_extension' of git://github.com/DarthNerdus/CouchPotatoServer into DarthNerdus-file_extension 2013-09-02 16:51:17 +02:00
Ruud bc77812488 Copy file and maybe copy stats. fix #349 2013-09-02 16:49:57 +02:00
Ruud 3e28cd5c95 local ip checking helper 2013-09-02 15:27:18 +02:00
Ruud 2715dbaaa5 Don't do failed checking on local requests 2013-09-02 15:27:06 +02:00
Ruud 3baf12d3e4 Make sure cleanhost only has one trailing slash 2013-09-02 14:54:54 +02:00
Ruud a428d36604 Wrap requests in try for better failing
Or would it be worse failing?
2013-09-02 14:35:05 +02:00
Ruud b5207bc88c Return releasedate as string 2013-09-02 14:27:16 +02:00
Ruud 910578a2ac Use TheMovieDB v3 api 2013-09-02 14:10:31 +02:00
Ruud 88176997e7 Don't use year if it's the first in the identified string. fix #1815 2013-09-02 00:00:27 +02:00
Ruud 233e6f9be0 Movie class wasn't remove on delete cancel. fix #1962 2013-09-01 23:33:24 +02:00
Ruud 1fd11fb547 Don't show delete dialog for category if it doesn't exist yet. fix #1961 2013-09-01 23:28:55 +02:00
Ruud 8bfd206578 Option to disable direct searching on adding. closes #2054 2013-09-01 23:18:12 +02:00
Ruud 62c6fd2e40 Don't error out on faulty PublicHD page. fix #2014 2013-09-01 23:05:28 +02:00
Ruud ac2d2a0463 Always search on empty release dates. fix #2035 2013-09-01 22:51:59 +02:00
Ruud c1e4b47b99 Return category by default. fix #2073 2013-09-01 18:21:53 +02:00
Jesse Read 32b479467a Fix missed type/protocol change. Fixes torrents being created as .movie files. 2013-08-31 20:45:37 -04:00
Ruud 6cab2b34d6 Continue after empty folder while loading plugins 2013-09-01 02:10:31 +02:00
Ruud 9e744199fe Make sure messages isn't empty 2013-09-01 01:44:47 +02:00
Ruud b22021e7f0 Try next log remove, don't stop 2013-09-01 00:43:53 +02:00
Ruud 68bdf47ea4 Use protocol, not type for sorting 2013-09-01 00:31:47 +02:00
Ruud af2876bd71 Lock same api routes 2013-09-01 00:24:47 +02:00
Ruud 1e5d6bad2a Lock while editing listeners 2013-09-01 00:24:18 +02:00
Ruud f6c836157d Movie db to bottom in scanner 2013-09-01 00:22:22 +02:00
Ruud d10874f216 Video object on iPad doesn't listen to z-index. fix #2093 2013-08-31 19:22:32 +02:00
Ruud 700713abcf Don't try to use undefined response 2013-08-31 17:48:19 +02:00
Ruud 5180426fc1 Remove debug print 2013-08-31 17:09:23 +02:00
Ruud e1c8a08f2f Run api requests in own thread 2013-08-31 17:07:46 +02:00
Ruud 16f0bcc3ac Don't run handler if it doesn't exist.. 2013-08-31 17:04:53 +02:00
Ruud 9c98a38604 Tornado update 2013-08-31 15:59:47 +02:00
Ruud 1b03c7e474 Use finish instead of write 2013-08-31 15:32:45 +02:00
Ruud 689feb78d0 Torrentshack missin category for pre-dvd releases. fix #2083 2013-08-31 14:33:30 +02:00
Ruud 336b15b199 Deluge import cleanup 2013-08-30 19:21:31 +02:00
Ruud 4a4bb819ec Merge branch 'deluge' of git://github.com/techmunk/CouchPotatoServer into techmunk-deluge 2013-08-30 18:40:35 +02:00
Techmunk 48be010f33 Fix up some debug messages, and the torrent completed status. 2013-08-30 10:25:58 +10:00
Techmunk 104e21b314 Fix for deluge downloading torrent files. 2013-08-28 20:41:02 +10:00
Ruud aaf5cab138 Encode folder returned from downloader. fix #2071 2013-08-27 23:38:51 +02:00
Ruud 22b744340a Properly remove backup folder 2013-08-27 22:25:56 +02:00
Techmunk 2954558004 Fix up deluge is Finished status matching. 2013-08-27 20:13:29 +10:00
Ruud b797590a4e Make sure extr_files exists 2013-08-25 20:16:08 +02:00
Ruud 9d71fe1724 Deluge proper error logging. fix #2069 2013-08-25 12:24:15 +02:00
Ruud 9ad0ed642d Don't use type yet. fix #2068 2013-08-25 12:07:13 +02:00
Ruud cbd217271d Don't load options twice 2013-08-25 00:59:37 +02:00
Ruud 65896497fb Return true for loader 2013-08-24 20:22:31 +02:00
Ruud 54a37b577d Import cleanup
Conflicts:
	couchpotato/core/providers/torrent/sceneaccess/main.py
2013-08-24 20:15:54 +02:00
Ruud f1948ffb6a Just load media recursively 2013-08-24 20:12:59 +02:00
Jason Mehring 7dd3b0ed15 fix loader error messages for modules that are selected recursively but are not really modules 2013-08-24 20:07:32 +02:00
Jason Mehring 11fcfa8202 Moved library and refactored to its now location. Modified anything firing libray.add/update/_release date to now fire library.add.movie...
Conflicts:
	couchpotato/core/loader.py
	couchpotato/core/media/show/_base/main.py
	couchpotato/core/media/show/library/season/main.py
2013-08-24 20:04:27 +02:00
Ruud 199e61ea14 Fallback on type for current downloads 2013-08-24 16:37:16 +02:00
Ruud 0daa6c8eff Merge branch 'develop_unrar_fixes' of git://github.com/mano3m/CouchPotatoServer into mano3m-develop_unrar_fixes 2013-08-24 16:16:48 +02:00
Ruud b1b5f97f03 Deluge fixes 2013-08-24 16:14:18 +02:00
Ruud 32d5587669 Don't load modules without __init__.py 2013-08-24 16:06:17 +02:00
mano3m c13c0f24e5 Change type to protocol in release and renamer 2013-08-24 15:50:19 +02:00
mano3m 7eb1d72333 remove move exception from unrar PR 2013-08-24 15:50:19 +02:00
Ruud 3d6ec1feba Move info providers to proper folder 2013-08-24 15:31:30 +02:00
Ruud c267232160 Add unrar support
Thanks @mano3m
2013-08-24 15:04:56 +02:00
Ruud 48f4b008df Move deluge lib to libs folder 2013-08-24 14:46:46 +02:00
Ruud ae1f181fbf Merge branch 'deluge' of git://github.com/techmunk/CouchPotatoServer into techmunk-deluge
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
2013-08-24 14:42:17 +02:00
Ruud cbfee72d51 rTorrent make pause advanced setting 2013-08-24 14:38:57 +02:00
Ruud ee709054f2 rTorrent rename type to protocol
code styling
2013-08-24 14:35:57 +02:00
Ruud ee60ec962b Merge branch 'feature/dev_rtorrent' of git://github.com/fuzeman/CouchPotatoServer into fuzeman-feature/dev_rtorrent 2013-08-24 14:33:17 +02:00
Ruud e013e38c5e Update ubuntu.init
Thanks @moriame
2013-08-24 14:26:16 +02:00
Ruud 20aa78105f Do window size check inside load event 2013-08-24 14:22:15 +02:00
Ruud 770590e4f2 Match default ports
Thanks @cpg
2013-08-24 14:08:05 +02:00
Ruud 8e9e7b49ea Simplify linking
Thanks @mano3m
2013-08-24 14:03:17 +02:00
Ruud 08554889fd Add the old rottentomatoes to default enabled list 2013-08-24 13:34:45 +02:00
Ruud 8ac2869de3 Merge branch 'rotten_tomatoes_custom_urls' of git://github.com/Lordcrash/CouchPotatoServer into Lordcrash-rotten_tomatoes_custom_urls 2013-08-24 13:28:10 +02:00
Ruud bb8e8a0df5 Merge branch 'develop_seed_fixes' of git://github.com/mano3m/CouchPotatoServer into mano3m-develop_seed_fixes 2013-08-24 13:22:29 +02:00
Ruud e2bd6a91cd MPAA rating for renamer 2013-08-24 13:21:39 +02:00
Ruud ed0e5ef497 XMBC notification, better remote folder description 2013-08-24 12:24:15 +02:00
Ruud e1e475e605 Merge branch 'develop_XBMC' of git://github.com/mano3m/CouchPotatoServer into mano3m-develop_XBMC 2013-08-24 12:19:32 +02:00
Ruud cef5b04eb1 Return unique imdb list 2013-08-24 12:14:15 +02:00
Ruud 7e44af936d Watch shutdown when adding automation movies 2013-08-24 12:14:02 +02:00
Ruud 6aec5a9a60 Cleanup IMDB provider 2013-08-24 12:13:45 +02:00
Ruud 79c75c886b Merge branch 'develop_automationIMDB' of git://github.com/dkboy/CouchPotatoServer into dkboy-develop_automationIMDB 2013-08-24 10:59:32 +02:00
mano3m bf6bcaed72 provide more info in case no movie is found
Several users reported an issue with "more than one group found (0)",
and it was unclear to them what it meant. This might help.
2013-08-22 21:20:02 +02:00
mano3m 70bc2a6656 use right variable for pause
fixes #2049
2013-08-21 20:59:39 +02:00
mano3m 695cdea447 Remove 'move' exception
No need to remove files when 'move' is selected as the downloaders do
this themselves now when cleaning up
2013-08-21 20:59:38 +02:00
mano3m d0735a6d58 Add failsafe for symlink errors
E.g. on Windows you need Admin rights to symlink...
2013-08-21 20:59:38 +02:00
mano3m 175c26bea9 Fix untagDir and hastagDir
Changes in commit 8a252bff64 broke the
tagging functionality
2013-08-21 20:59:23 +02:00
Techmunk 8a298edd4e Implementation of Deluge downloader. 2013-08-21 23:52:54 +10:00
Ruud 9860a1c138 Default to movie type 2013-08-18 13:17:40 +02:00
Ruud 3dff598d03 Add multiprovider for provider grouping 2013-08-18 11:48:00 +02:00
Ruud 62b571d5f1 Rename type to protocol 2013-08-18 11:47:54 +02:00
Ruud 3af6623a91 Move registerPlugin to __new__ magic 2013-08-18 11:47:49 +02:00
Ruud c73ed8a4c5 Add multiple categories for BRRIP on TPB. fix #2025 2013-08-16 20:05:30 +02:00
Ruud 4d5ba65254 Migrate options 2013-08-16 17:23:40 +02:00
Ruud 91856f1159 Searcher base
Re-usable cronjob code
2013-08-16 16:52:12 +02:00
Ruud f7da408f83 Searcher conf section 2013-08-16 10:21:44 +02:00
Ruud 2824c55231 Give moviesearcher a unique name 2013-08-15 23:52:48 +02:00
Ruud 874655846c Move movie plugin to media folder 2013-08-15 23:52:43 +02:00
Ruud 1620acedb1 Move movie to new media type folder 2013-08-15 23:52:37 +02:00
Ruud 6395e5dbbb Cleanup console log 2013-08-15 23:52:16 +02:00
Ruud 251d9cdb8a Placeholder for preferred words 2013-08-15 18:47:57 +02:00
Ruud 623571acbb Make category destination editable 2013-08-15 18:31:06 +02:00
Ruud 250f07ffa7 Optimize dashboard query 2013-08-14 16:55:57 +02:00
Ruud 8917d7c16c Optimize movie.list query 2013-08-14 16:47:59 +02:00
Ruud d759280c18 Don't update library items on shutdown 2013-08-14 12:31:41 +02:00
Ruud 67bc3903d4 Don't show loader for scanner if page isn't loaded yet 2013-08-14 12:20:38 +02:00
Ruud cf6f83a44b Option to disable manage scan at startup. fix #1951 2013-08-14 12:14:52 +02:00
Ruud 4b15563ba3 Don't use in_progress when it isn't set 2013-08-14 12:13:52 +02:00
Ruud dc36e15448 Don't run multiple manage.progress requests 2013-08-14 11:56:08 +02:00
Ruud 0b6330e98b Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2013-08-13 20:56:46 +02:00
Ruud 2e93687bb4 Don't try to loop over empty enablers 2013-08-13 17:46:41 +02:00
Ruud 0f925a466a Also ignore __ when importing folders 2013-08-13 17:31:12 +02:00
Ruud 16eeeda787 Ignore folder include with __ at beginning 2013-08-13 17:25:24 +02:00
Ruud 52f1df98bb Don't try to split on empty string 2013-08-13 16:51:46 +02:00
Ruud a0ccff23a3 Remove duplicate spaces 2013-08-13 16:08:34 +02:00
Ruud b8bed627a8 Add possible title with some char replacements 2013-08-13 16:08:21 +02:00
Ruud 8d058d9dc8 Add hdscr to screener quality 2013-08-13 15:45:05 +02:00
Ruud 57e92ff8d3 Optimized frontend notifications 2013-08-13 15:40:56 +02:00
Ruud 6eff724f97 Clean nonblocking requestshandler 2013-08-13 15:36:11 +02:00
Ruud Burger 55c3fe503b Merge pull request #1985 from mano3m/develop_nzbget
Fix NZBGet url issue
2013-08-12 01:21:41 -07:00
Ruud Burger 7f1ac63c58 Merge pull request #2005 from mano3m/develop_sorting
Regard torrents and torrent_magnet the same
2013-08-12 01:08:05 -07:00
Dean Gardiner 2bb2e28f91 Updated rTorrent library and fixed some issues with ratio setup. 2013-08-12 15:32:15 +12:00
Dean Gardiner 0bdffc5036 Change to ratio group setup to ensure everything is set correctly. 2013-08-12 15:32:14 +12:00
Dean Gardiner 7202fbf084 Removed stop_complete option, Can instead be disabled by setting seed_ratio to zero on the provider. 2013-08-12 15:32:13 +12:00
Dean Gardiner 317c3afb7a Few minor fixes and implemented delete_files option via shutil.rmtree 2013-08-12 15:32:13 +12:00
Dean Gardiner 577baeca59 Hiding remove files in the rTorrent downloader until it's implemented. 2013-08-12 15:32:12 +12:00
Dean Gardiner 7c680cac10 Updated rTorrent downloader to set ratio stop action, added new seeding methods and updated the rTorrent library 2013-08-12 15:32:11 +12:00
Dean Gardiner 0fadbd52a3 Cleaned up imports and added support for downloading magnet torrents via sources. 2013-08-12 15:32:10 +12:00
Dean Gardiner 38e204dfe8 Added support for labels on the rtorrent downloader. 2013-08-12 15:32:10 +12:00
Dean Gardiner bf62653531 Added missing 'folder' parameter on the rtorrent downloader to fix moving/linking issues. 2013-08-12 15:32:09 +12:00
Dean Gardiner d851be41d3 Updated rtorrent-python library. 2013-08-12 15:32:08 +12:00
Dean Gardiner 3bd1875321 Added initial rtorrent downloader, currently testing, possibly has some bugs. 2013-08-12 15:32:00 +12:00
mano3m 448c1d69a7 Regard torrents and torrent_magnet the same
When sorting the torrents and torrent_magnets were sorted, by taking
only the three first characters (as 'nzb; is three chars), the score
prevails. Fixes #2004
2013-08-11 00:06:07 +02:00
Ruud c99a5cb535 Don't autoadd when already in wanted 2013-08-07 20:06:30 +02:00
mano3m 0492e90d6f XBMC: properly check if host is local
And added option to scan if remote
2013-08-03 01:52:20 +02:00
Micah James 4ffda9f705 Made code more python-y per mano3ms recommendation. 2013-08-01 23:15:36 -04:00
mano3m b32d4fc42d Fix NZBGet url issue 2013-08-01 23:24:25 +02:00
Micah James 4330dc39bf Changed description to be better suited for this. 2013-07-31 23:14:58 -04:00
Micah James da50b19b6b Added custom url code handling 2013-07-31 23:06:12 -04:00
Micah James 797018fb8a Revert "Adding more code."
This reverts commit 3a8f891c7d.
2013-07-31 22:47:52 -04:00
Micah James 3a8f891c7d Adding more code. 2013-07-31 22:45:48 -04:00
Micah James 56a788286c Adding code for custom urls UI 2013-07-31 22:41:49 -04:00
mano3m fd95364d5f uTorrent ratio issue fixed
The tryFloat function returns 0 if it is fed with a float(!). This resulted in the seed_ratio being set to 0 on first/automatic download. When manually downloading, it did work as the ratio is stored as a string.
2013-07-31 15:04:48 +02:00
mano3m 470fde0890 Unset the uTorrent read only flags
Fix for #1871

Note that this is a fix for Windows only. I am unaware if this issue
arises on Linux/Mac and what happens with this fix on those systems.
2013-07-23 19:07:36 +02:00
Ruud f12d878c0b Select category for search, suggest & edit 2013-07-22 21:57:13 +02:00
Ruud e8993932c1 Check isMac function 2013-07-22 21:56:33 +02:00
Ruud e3933e4ddc Proper meta tag 2013-07-22 21:56:22 +02:00
Ruud dd67239b6e Add categories to settings 2013-07-21 19:12:53 +02:00
Ruud 1ea0d3bd8b Move providers to main searcher tab in settings 2013-07-21 19:12:32 +02:00
Ruud 8b952d4be6 Combine global and category words 2013-07-19 16:58:49 +02:00
Ruud 9e8a3bc701 Movie category migrate 2013-07-15 22:51:53 +02:00
Ruud 76807176fb Merge branch 'develop-categories' of git://github.com/clinton-hall/CouchPotatoServer into clinton-hall-develop-categories
Conflicts:
	couchpotato/core/plugins/score/main.py
2013-07-15 20:47:29 +02:00
iguyking 3650624e4b Update contributing.md
Fixed to say what was intended
2013-07-15 20:44:42 +02:00
Ruud Burger 585c509aba Merge pull request #1950 from mano3m/develop_rpc-url
Add rpc_url to Transmission options
2013-07-15 04:20:25 -07:00
mano3m 046c7e732f Add rpc_url to Transmission options
Fixes  #1832
2013-07-14 23:43:07 +02:00
mano3m 564a27461d XBMC: Only add directory if XBMC is on localhost 2013-07-14 23:30:37 +02:00
mano3m 4ebbc1a01d XBMC: Only scan the new movie folder 2013-07-14 02:19:35 +02:00
Ruud 4ec32a6403 Merge branch 'develop_seed_fixes' of git://github.com/mano3m/CouchPotatoServer into mano3m-develop_seed_fixes 2013-07-13 17:56:07 +02:00
Ruud 412627aab0 Move rating and genres to suggestions only 2013-07-13 17:52:40 +02:00
mano3m 2584abda0e Several fixes and increased readability 2013-07-13 17:06:59 +02:00
dkboy 7692322fba Expand IMDB automation provider to include charts
Expand IMDB automation provider to include certain top charts, this
includes the 'in theaters' list, as well as the top 250 list. They both
respect the minimum requirement settings.
2013-07-13 16:45:39 +12:00
Ruud 954018fea2 Youtube trailer search in https 2013-07-12 21:03:03 +02:00
Ruud ebf37f7310 Cleanup plex urls 2013-07-12 20:52:41 +02:00
Ruud f22b836ede Combine adopt 2013-07-12 14:42:59 +02:00
Ruud 1cea786d66 Style rating and genres 2013-07-12 14:36:04 +02:00
dkboy 9be10f7b79 Add Rating / Genre to Dashboard Suggestions
Add Rating and up to 3 Genres to movie suggestions, to avoid constantly
jumping through to IMDB site.
2013-07-12 21:49:24 +12:00
Ruud 1f35d0ec2f Remove debug print 2013-07-11 17:36:27 +02:00
Ruud 9fcf36a2ff Add WEB-DL and WEB-Rip. fix #1913 2013-07-11 17:34:55 +02:00
Ruud 30f5a66487 AwesomeHD: Log wrong passkey. fix #1912 2013-07-11 15:24:20 +02:00
Ruud 60e0ad1f5d Add Windows Media Center / Explorer folder.jpg creation. closes #1932 2013-07-11 15:05:08 +02:00
Ruud ed60b4670e Move root creation to metadata base 2013-07-11 15:04:39 +02:00
Ruud 318daaf083 Cleanup BitSoup 2013-07-09 23:31:43 +02:00
Ruud 182987218b Merge branch 'develop' of git://github.com/dkboy/CouchPotatoServer into dkboy-develop 2013-07-09 23:13:15 +02:00
Ruud 5ff8c7302f Sabnzbd prio description 2013-07-09 23:08:33 +02:00
Ruud 398712403b Merge branch 'develop' of git://github.com/gthicks/CouchPotatoServer into gthicks-develop 2013-07-09 23:04:28 +02:00
Ruud 63f72eb23b Merge branch 'refs/heads/seeding' into develop 2013-07-09 22:53:14 +02:00
Ruud 9dea6d7200 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2013-07-09 22:52:53 +02:00
Ruud 36f63bdf99 Seeding cleanup and better defaults 2013-07-09 22:52:32 +02:00
Ruud a09fc14625 Twitter DM didn't work 2013-07-09 20:32:29 +02:00
dkboy 71e280238d Fixed missing detail_url 2013-07-10 01:48:11 +12:00
Ruud e20bb13649 Delete NZBx 2013-07-08 11:31:13 +02:00
Ruud ed8108a9d8 Remove NZBsRus 2013-07-08 11:30:55 +02:00
Ruud c0b3c9a330 Make description a bit shorter 2013-07-07 13:44:49 +02:00
Ruud 8a252bff64 Don't use parentdir for tagging 2013-07-07 13:00:38 +02:00
Ruud d3d3106fc9 Merge branch 'develop_seed' of git://github.com/mano3m/CouchPotatoServer into mano3m-develop_seed 2013-07-07 11:37:53 +02:00
dkboy 1ebb09226d Add Bitsoup provider 2013-07-07 14:23:15 +12:00
Ruud 52163428e9 Break if media headers are corrupt. fix #1828 2013-07-07 00:09:22 +02:00
Ruud da9dda2c2b Make minimal movie automation clearer. fix #1923 2013-07-06 23:39:34 +02:00
Ruud a4a14cae96 Use forwarded host when provided. fix #1922 2013-07-06 23:26:46 +02:00
Garret 989d6c55c4 Added priority setting for SABnzbd
Includes ability to add nzb to queue paused.
2013-07-06 10:28:32 -07:00
Ruud 3b7376fd18 One up 2013-07-06 01:01:26 +02:00
Ruud c31b10c798 Ignore current suggested results 2013-07-06 00:49:11 +02:00
Ruud 1c3e6ba930 Ignore current suggested results 2013-07-06 00:24:57 +02:00
Ruud acda664686 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2013-07-05 22:43:54 +02:00
Ruud 99123ad1c3 Remove version on branch 2013-07-05 22:17:43 +02:00
Ruud cdf9cf5cf4 Yifi: don't search empty results. fix #1900 2013-07-05 21:54:55 +02:00
Ruud 797dedfcbb Remove cdX from subname. fix #1524 2013-07-05 21:28:07 +02:00
Ruud b61de4866c Make subliminal work with Requests 1.0+ 2013-07-05 20:40:27 +02:00
Ruud 931951ff37 Change default min size for 720p and 1080p 2013-06-30 15:57:58 +02:00
Ruud 6f42b4c316 Don't show coming soon when no dvd release is set 2013-06-30 15:21:06 +02:00
Ruud 58c446de2d Make string param boolean 2013-06-30 15:20:02 +02:00
Ruud 74bf6bc411 Always set info dict on library 2013-06-30 13:17:56 +02:00
Ruud ad3c24f950 Improved "too early to search" calculations 2013-06-30 13:17:43 +02:00
mano3m 998e487fe8 NZBs are not torrents :) 2013-06-30 10:14:08 +02:00
Ruud 93346b0c63 Properly update release dates 2013-06-30 01:16:13 +02:00
mano3m 7d9920691f Fix uTorrent settings automatically
Note that this might not be the way we want to go?
2013-06-29 22:50:25 +02:00
Ruud b1942678b4 Add hash and date to update available notification. fix #1883 2013-06-29 22:20:35 +02:00
Ruud 8c77d0d775 Add advanced option to search on launch. fix #1887 2013-06-29 22:03:29 +02:00
Ruud 3e667ee39a Couldn't press letter in movie filter. fix #1888 2013-06-29 21:56:24 +02:00
Ruud 52b2858ac2 Don't enable yifi by default 2013-06-29 21:39:53 +02:00
Ruud 6fcb4c2058 Change default automation interval 2013-06-29 21:07:07 +02:00
mano3m 7411670e22 Added complete download removal to SabNZBd 2013-06-29 10:36:02 +02:00
mano3m cfd23c395a Add failed download handling to Transmission 2013-06-29 10:23:08 +02:00
Ruud 2e8f670e94 Remove import 2013-06-28 23:32:38 +02:00
mano3m 18a88eab51 Textual change 2013-06-26 20:02:25 +02:00
mano3m 84e9f9794d Add awesomehd torrent provider 2013-06-26 19:53:28 +02:00
mano3m 628c0e5dcc Add yify torrent provider 2013-06-26 19:52:39 +02:00
mano3m cdee08bd36 Add status colours in dashboard 2013-06-26 19:49:05 +02:00
mano3m 7ed43da425 Also set seeding status in case nothing is done 2013-06-26 19:49:05 +02:00
mano3m 461a0b3645 Seeding support
Design intent:
- Option to turn seeding support on or off
- After torrent downloading is complete the seeding phase starts, seeding parameters can be set per torrent provide (0 disables them)
- When the seeding phase starts the checkSnatched function renames all files if (sym)linking/copying is used. The movie is set to done (!), the release to seeding status.
- Note that Direct symlink functionality is removed as the original file needs to end up in the movies store and not the downloader store (if the downloader cleans up his files, the original is deleted and the symlinks are useless)
- checkSnatched waits until downloader sets the download to completed (met the seeding parameters)
- When completed, checkSnatched intiates the renamer if move is used, or if linking is used asks the downloader to remove the torrent and clean-up it's files and sets the release to downloaded
- Updated some of the .ignore file behavior to allow the downloader to remove its files

Known items/issues:
- only implemented for uTorrent and Transmission
- text in downloader settings is too long and messes up the layout...

To do (after this PR):
- implement for other torrent downloaders
- complete download removal for NZBs (remove from history in sabNZBd)
- failed download management for torrents (no seeders, takes too long, etc.)
- unrar support

Updates:
- Added transmission support
- Simplified uTorrent
- Added checkSnatched to renamer to make sure the poller is always first
- Updated default values and removed advanced option tag for providers
- Updated the tagger to allow removing of ignore tags and tagging when the group is not known
- Added tagging of downloading torrents
- fixed subtitles being leftover after seeding
2013-06-26 19:49:04 +02:00
Ruud bd56539103 Yifi cleanup 2013-06-24 22:31:50 +02:00
Ruud 9bcd3de69b Merge branch 'develop' of git://github.com/Mochaka/CouchPotatoServer into Mochaka-develop 2013-06-24 22:08:00 +02:00
Ruud d8f57963a1 NZBIndex: Search for year inside brackets. closes #1874 2013-06-24 22:07:21 +02:00
Ruud 5328f7fe69 Allow unknown keywords for all api calls. fix #1881 2013-06-24 21:21:49 +02:00
Ruud 9eea42b121 Get array arguments as list. fix #1875 2013-06-24 00:26:00 +02:00
Ruud 374f8ba1de Allow non trailing slash API calls 2013-06-23 23:28:13 +02:00
Ruud 74c984dec3 Send CP headers to suggestion call. fix #1872 2013-06-23 20:44:11 +02:00
Ruud 52ea0215f0 Use done for suggestion also 2013-06-23 19:14:11 +02:00
Ruud ea3d719b32 Suggest on wrong dev port 2013-06-23 19:09:07 +02:00
Ruud fd1e655075 Initial suggestion support 2013-06-23 19:07:03 +02:00
Ruud 9f8d439780 Add limit to CP search api 2013-06-22 17:01:24 +02:00
Aaron Florey 7e1bdc99eb Add Yify Torrent Provider 2013-06-23 00:11:01 +10:00
Ruud dac36d7f55 IPTorrents ignore empty results 2013-06-22 14:16:02 +02:00
Ruud 9d495a10ec Unicode static folder 2013-06-22 01:38:07 +02:00
Ruud 9bb99319ba SplitString don't clean 2013-06-22 01:37:27 +02:00
Ruud bc8d8dcd04 Update guessit with unicode fix 2013-06-22 00:34:58 +02:00
Ruud b2d9a7675d Add version to SAB description 2013-06-22 00:33:12 +02:00
Ruud 2477197656 Don't use unicode in repo 2013-06-22 00:33:00 +02:00
Ruud 171083b2f1 Remove empty values from splitString. fix #1795 2013-06-21 13:44:14 +02:00
Ruud e592eb969f NZBget error when downloadrate is 0. fix #1849 2013-06-21 13:00:58 +02:00
Ruud db1493f138 Update pytwitter library. fix #1847 2013-06-21 12:50:58 +02:00
Ruud 57c270f8fa Don't break while sending messages to listeners 2013-06-21 11:32:45 +02:00
Ruud bfe8bc89c0 IMDB description csv link 2013-06-19 23:39:00 +02:00
Ruud 0a00862495 Show csv imdb export in image 2013-06-19 23:34:58 +02:00
sax 7dd53d93cd Added nzb support to Synology downloader. 2013-06-19 22:43:11 +02:00
theorem21 abe65d4064 Update README.md
added FreeBSD installation instructions.  Requires additional FreeBSD init script (pending creation)
2013-06-19 22:36:56 +02:00
Ruud 4977b31ba6 Use failed status to ignore releases too 2013-06-16 00:21:33 +02:00
Ruud c1beb85ba5 Add spotter to name for scoring 2013-06-15 23:32:44 +02:00
Ruud ca9a78eea4 Advanced option for XBMC to only update first in list
Thanks @cliffordwhansen
2013-06-15 22:17:23 +02:00
Ruud 9bf006f4d3 Return if api is not found 2013-06-15 21:43:14 +02:00
Ruud 3bb2a082b7 AwesomeHD provider
Thanks @jrsdead
2013-06-15 20:41:23 +02:00
Ruud 92d11522d2 Use id for HDBits torrent name 2013-06-15 00:06:14 +02:00
Ruud 44cfdc1503 Include full requests lib 2013-06-15 00:04:15 +02:00
Ruud 2fdcbedea8 Use has_key for events check 2013-06-15 00:02:37 +02:00
Ruud 787c7fd966 Codestyle cleanup 2013-06-14 23:35:28 +02:00
sax 09b4ad6937 Fixed torrent support for Synology downloader to work properly with torrent files passed directly by CouchPotato. 2013-06-14 23:31:56 +02:00
sax 580d43aeaf Updated requests library to version 1.2.3 2013-06-14 23:31:47 +02:00
sax a1a7fec15f Added torrent support for Synology downloader. 2013-06-14 23:31:40 +02:00
Ruud 6dcd74d116 Re-use code for ignore toggle 2013-06-14 23:21:41 +02:00
Ruud 187f5a8a93 Merge branch 'develop' of git://github.com/mano3m/CouchPotatoServer into mano3m-develop 2013-06-14 22:43:19 +02:00
Ruud 2eb938147a Move login downloads to default list item 2013-06-14 22:08:25 +02:00
Ruud deffb75c14 TorrentByte provider
Thanks @StealthGod
2013-06-14 21:52:15 +02:00
Ruud f91707bfbe Uncomment debug code 2013-06-14 21:51:41 +02:00
Ruud 8aba7825dc Only show to_early when it has items 2013-06-14 21:24:38 +02:00
Joel Kåberg b8b5b2fef2 dont spam the log damnit! 2013-06-14 21:17:45 +02:00
Ruud f4d6d69184 Check if handler has parent 2013-06-14 20:48:20 +02:00
Ben Fox-Moore a5b1c685e1 Allow IPTorrents provider to read results across multiple pages
Conflicts:
	couchpotato/core/providers/torrent/iptorrents/main.py
2013-06-14 20:47:05 +02:00
Ruud 609805b84d Don't allow keyerror in event 2013-06-14 20:04:49 +02:00
Ruud 00d1da7c01 Bind quickscan to class 2013-06-14 19:51:31 +02:00
Ruud 7335726c7d Add handler aswell 2013-06-14 19:51:20 +02:00
Ruud 02779939f0 Catch im_self error 2013-06-14 19:51:14 +02:00
Ruud 6c6f015f40 Use str not unicode in minification 2013-06-14 19:47:54 +02:00
Ruud f087d38b86 Cleanup 2013-06-14 17:36:26 +02:00
Ruud c78957f55c Don't try to run event without beforeCall 2013-06-14 17:24:34 +02:00
Ruud 9ce0c47cd4 More login fixes 2013-06-14 16:03:02 +02:00
clinton-hall 60034f2c96 add category preffered words and partial ignore. 2013-06-14 21:56:26 +09:30
Ruud c9a4af218e Send port with referer. fix #1827 2013-06-14 13:54:01 +02:00
Ruud c5c2e61e06 Log startup errors 2013-06-14 11:22:29 +02:00
Ruud b2930dd6a7 Encode used path on startup. fix #1797 fix #1297 2013-06-14 11:11:34 +02:00
Ruud 4aa6700ceb Update SQLAlchemy 2013-06-14 11:00:06 +02:00
Ruud 267ecfacab Status check in ubuntu init script
Thanks @LeonB
2013-06-14 08:57:37 +02:00
clinton-hall 007597239f add categories 2013-06-14 15:06:59 +09:30
Ruud 5699abf1be Use new KickAss domain 2013-06-14 00:43:03 +02:00
Ruud a6ccd037e2 Login check for SceneHD 2013-06-14 00:37:55 +02:00
Ruud 009991ce4c Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2013-06-14 00:04:44 +02:00
Ruud 6ef788a8f4 Check login after 1 hour 2013-06-14 00:03:48 +02:00
Ruud fa37f7d40a Add some logging to core messaging 2013-06-13 12:15:59 +02:00
Ruud b195cebac7 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2013-06-12 23:46:35 +02:00
Ruud 8aeea60888 Update Tornado 2013-06-12 23:42:38 +02:00
Ruud 6e0857c6c1 Remove Flask dependencies 2013-06-12 23:37:08 +02:00
Ruud Burger 260fdbe3b3 Merge pull request #1836 from clinton-hall/develop-extra-logging
Add logging when no rating available
2013-06-12 02:05:21 -07:00
mano3m 2f30c6c781 fix failed issues
As reported in issue #1822 I broke try next release when failed. This
commit adds the failed status to several items.
2013-06-11 21:43:01 +02:00
Clinton Hall d5b4da655a add logging when no rating available 2013-06-11 21:51:56 +09:30
Ruud 1694ed7758 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2013-06-10 21:14:02 +02:00
Ruud ee6cc6d319 PTP torrent id in lowercases 2013-06-10 21:10:58 +02:00
Ruud Burger 7670e320ba Merge pull request #1799 from clinton-hall/develop-audiochannels
add audio channels to renamer
2013-06-10 11:39:34 -07:00
Ruud 15ab745bd0 Don't assume imdb key. fixes #1819 2013-06-08 18:04:46 +02:00
Ruud 7468b33991 Send along ignored movies 2013-06-08 17:32:45 +02:00
Ruud 750e02f38a Close zipfile. fixes #1798 2013-06-08 16:04:14 +02:00
Ruud e2852407ea One up 2013-06-03 22:22:44 +02:00
Ruud 88e738c6cd Don't show double updater name 2013-06-03 22:22:35 +02:00
Ruud eaae8bdb0b Merge branch 'refs/heads/develop' into desktop 2013-06-03 22:00:21 +02:00
Ruud 95d146fea2 Send referer with scheme 2013-06-02 14:22:59 +02:00
Ruud dc20b68a37 See if need to login on "belongs_to" check. fix #1190 2013-05-31 16:32:03 +02:00
clinton-hall 563e3072a5 add audio channels to renamer 2013-05-31 13:44:40 +09:30
Ruud b3ba4db00b Append instead of add for subtitle file list 2013-05-29 19:30:51 +02:00
Ruud a4c1480a1a Force update check from dropdown 2013-05-29 19:03:49 +02:00
Ruud 91e0452320 Torrentshack cleanup 2013-05-29 19:03:28 +02:00
Ruud ad80ea7885 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2013-05-29 18:33:36 +02:00
Ruud 1c20cda389 Set updater crons on start. 2013-05-29 14:50:22 +02:00
sax 631759d833 Added configuration option to search over scene releases only. Fixed release name issue (removed ­ element). 2013-05-28 22:52:22 +02:00
sax ca02c66f26 Fixed login success detection. 2013-05-28 22:52:13 +02:00
sax 3ac095d359 Added support for Torrent Shack provider. 2013-05-28 22:52:05 +02:00
Ruud e1bc223de0 Get year with default 2013-05-28 21:13:15 +02:00
Ruud e065ead9b3 Api on subdomain 2013-05-26 21:50:20 +02:00
Ruud f9471f9b9b HDBits cleanup 2013-05-26 15:16:55 +02:00
Ronald Pompa 2612b50d06 created hdbits torrent provider 2013-05-26 14:30:10 +02:00
Ruud d9ce2906a0 Fix line ending 2013-05-26 14:24:59 +02:00
Joel Kåberg b76397f98e addApiView explenation 2013-05-26 14:22:31 +02:00
Joel Kåberg fcad9e0be5 fireAsync made optional 2013-05-26 14:22:23 +02:00
Ruud 2934347865 Send user-agent on login 2013-05-26 14:20:05 +02:00
Ruud 315f1b0207 Add r6 to quality list 2013-05-23 07:35:11 +02:00
Ruud 965bd79a86 Cleanup import 2013-05-19 23:20:32 +02:00
Ruud c18563e34b uTorrent cleanup 2013-05-19 23:12:22 +02:00
Ruud 161e0de8d5 Don't need makedir in Transmission 2013-05-19 22:51:32 +02:00
Ruud 40aeca0740 PTP extra scoring 2013-05-19 22:18:01 +02:00
Ruud 63dd7fa7c0 New PTP-config for more accurate hits
Conflicts:
	couchpotato/core/providers/torrent/passthepopcorn/main.py
2013-05-19 22:10:51 +02:00
Ruud 509b49caf1 Deepcopy and merge movie info results 2013-05-19 01:12:09 +02:00
Ruud 38c51cf79c Import cleanup 2013-05-19 00:57:50 +02:00
Ruud 0b693bba4e Add "on snatch" options to XBMC & Plex notifications
fix #1379
2013-05-19 00:30:56 +02:00
Ruud 1258f34c78 Update counter on movie add / delete
fix #1383
2013-05-19 00:22:44 +02:00
Ruud 510c0d5f56 Remove size_check in quality guess
fix #1393
2013-05-19 00:12:19 +02:00
Ruud cdb630e580 More touch fixes 2013-05-18 23:59:37 +02:00
Ruud 65fbd38105 Make buttons more touch friendly
fix #1416
2013-05-18 23:37:49 +02:00
Ruud 1570132a55 Don't try to rss parse empty string
fix #1418
2013-05-18 22:52:02 +02:00
Ruud 7b5b748d23 Failed joining unicode and none unicode paths
fix #1447
2013-05-18 22:45:09 +02:00
Ruud 041601c4a5 Change TPB search string
fix #1451
2013-05-18 22:12:43 +02:00
Ruud f692fd0202 Make sure info isn't not overwriten by none
fix #1724
2013-05-18 21:53:17 +02:00
Ruud e7b4de56f2 Only run updater if enabled.
fix #1756
2013-05-18 20:30:48 +02:00
Ruud 4a616a0c04 Placeholder styling 2013-05-18 19:40:26 +02:00
Ruud 0814675d2a Remove prints in clientscript 2013-05-18 17:28:25 +02:00
Ruud 13df35462b Force expire database objects 2013-05-17 21:36:23 +02:00
Ruud 899868f51e Don't show empty message when search 2013-05-17 21:32:46 +02:00
Ruud ee466aebce Easily reset search 2013-05-17 18:32:20 +02:00
Ruud 687ef2662e Switch filter and view 2013-05-17 17:52:19 +02:00
Ruud 5aa29acbd3 Logging fixes 2013-05-17 17:51:15 +02:00
Ruud 1c2b3d063b Empty wanted list background 2013-05-17 15:40:27 +02:00
Ruud 551a000893 Incorrect marking as BD-Rip
Fixes #1643
2013-05-17 15:12:13 +02:00
Ruud 0d82d425cc Show original message when log is failing
closes #1735
2013-05-17 12:41:31 +02:00
Ruud 0e1cea1034 Simplify minifier
fixes #1744
2013-05-17 12:30:48 +02:00
Ruud 2b75153148 Don't limit snatched & wanted
fixes #1747
2013-05-17 12:11:53 +02:00
Ruud c170615fb3 Ignore temp updater files on cleanup 2013-05-15 14:49:45 +02:00
Ruud f6e84b6a35 Remove view after update 2013-05-14 00:22:19 +02:00
Ruud 6144f09a1f Make lists of sorted movies files also 2013-05-14 00:15:28 +02:00
Ruud de142e8050 Goodfilm fixes. closes #1723
Thanks @qooplmao
2013-05-14 00:13:42 +02:00
Ruud d0c1a119fd Use list for leftover files 2013-05-13 23:48:02 +02:00
Ruud 8fd80d3185 Update instead of extend 2013-05-13 23:44:01 +02:00
Ruud ae28c82858 Cleanup 2013-05-13 23:27:46 +02:00
Ruud 1766764c7d Skip available movies in "still not available" view. fix #1687 2013-05-13 23:07:57 +02:00
Ruud 129f8d72bd API movie.list didn't return proper total. fix #1727 2013-05-13 21:37:54 +02:00
Ruud 7314b5ecae Run async event in thread so the on_complete is fired properly 2013-05-11 00:04:59 +02:00
Ruud 7b0806355f Thumbnail list action position 2013-05-10 19:36:48 +02:00
Ruud 49cf72e058 Load notification after window load 2013-05-10 18:28:52 +02:00
Ruud a11cad619d Don't unicode css 2013-05-10 18:06:10 +02:00
Ruud c1d35e8a57 Stop blinking text when scrolling in webkit 2013-05-10 15:19:39 +02:00
Ruud fede348fbd Icon replacements 2013-05-10 15:14:47 +02:00
Ruud f3c60e8fa6 Added TPB proxies 2013-05-10 12:00:12 +02:00
Ruud 00e53439ed Don't wait between xbmc calls 2013-05-10 00:07:06 +02:00
Ruud 368fced0c4 Cancel autocomplete searches when starting new one 2013-05-10 00:03:42 +02:00
Ruud 666771fb0f Notification is empty styling 2013-05-10 00:02:11 +02:00
Ruud 9e3f978677 Styling fixes 2013-05-09 23:36:54 +02:00
Ruud f467d1c4f7 Dashboard thumbnails height not set properly. fix #1698 2013-05-08 12:54:34 +02:00
Ruud d8fc9d937e Filmweb userscript fix 2013-05-08 12:47:17 +02:00
749 changed files with 35927 additions and 85947 deletions
+1 -1
View File
@@ -81,7 +81,7 @@ class TaskBarIcon(wx.TaskBarIcon):
webbrowser.open(url) webbrowser.open(url)
def onSettings(self, event): def onSettings(self, event):
url = self.frame.parent.getSetting('base_url') + '/settings/' url = self.frame.parent.getSetting('base_url') + 'settings/about/'
webbrowser.open(url) webbrowser.open(url)
def onTaskBarClose(self, evt): def onTaskBarClose(self, evt):
+20
View File
@@ -40,3 +40,23 @@ Linux (ubuntu / debian):
* Make it executable. `sudo chmod +x /etc/init.d/couchpotato` * Make it executable. `sudo chmod +x /etc/init.d/couchpotato`
* Add it to defaults. `sudo update-rc.d couchpotato defaults` * Add it to defaults. `sudo update-rc.d couchpotato defaults`
* Open your browser and go to: `http://localhost:5050/` * Open your browser and go to: `http://localhost:5050/`
FreeBSD :
* Update your ports tree `sudo portsnap fetch update`
* Install Python 2.6+ [lang/python](http://www.freshports.org/lang/python) with `cd /usr/ports/lang/python; sudo make install clean`
* Install port [databases/py-sqlite3](http://www.freshports.org/databases/py-sqlite3) with `cd /usr/ports/databases/py-sqlite3; sudo make install clean`
* Add a symlink to 'python2' `sudo ln -s /usr/local/bin/python /usr/local/bin/python2`
* Install port [ftp/libcurl](http://www.freshports.org/ftp/libcurl) with `cd /usr/ports/ftp/fpc-libcurl; sudo make install clean`
* Install port [ftp/curl](http://www.freshports.org/ftp/bcurl), deselect 'Asynchronous DNS resolution via c-ares' when prompted as part of config `cd /usr/ports/ftp/fpc-libcurl; sudo make install clean`
* Install port [textproc/docbook-xml-450](http://www.freshports.org/textproc/docbook-xml-450) with `cd /usr/ports/textproc/docbook-xml-450; sudo make install clean`
* Install port [GIT](http://git-scm.com/) with `cd /usr/ports/devel/git; sudo make install clean`
* 'cd' to the folder of your choosing.
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
* Then run `sudo python CouchPotatoServer/CouchPotato.py` to start for the first time
* To run on boot copy the init script. `sudo cp CouchPotatoServer/init/freebsd /etc/rc.d/couchpotato`
* Change the paths inside the init script. `sudo vim /etc/init.d/couchpotato`
* Make init script executable. `sudo chmod +x /etc/rc.d/couchpotato`
* Add init to startup. `sudo echo 'couchpotato_enable="YES"' >> /etc/rc.conf`
* Open your browser and go to: `http://server:5050/`
+1 -1
View File
@@ -12,4 +12,4 @@
* What hardware / OS are you using and what are the limits? NAS can be slow and maybe have a different python installed then when you use CP on OSX or Windows for example. * What hardware / OS are you using and what are the limits? NAS can be slow and maybe have a different python installed then when you use CP on OSX or Windows for example.
* I will mark issues with the "can't reproduce" tag. Don't go asking me "why closed" if it clearly says the issue in the tag ;) * I will mark issues with the "can't reproduce" tag. Don't go asking me "why closed" if it clearly says the issue in the tag ;)
**If I don't get enough info, the change of the issue getting closed is a lot bigger ;)** **If I don't get enough info, the chance of the issue getting closed is a lot bigger ;)**
+106 -51
View File
@@ -1,84 +1,139 @@
from couchpotato.api import api_docs, api_docs_missing from couchpotato.api import api_docs, api_docs_missing, api
from couchpotato.core.auth import requires_auth
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.request import getParams, jsonified from couchpotato.core.helpers.variable import md5, tryInt
from couchpotato.core.helpers.variable import md5
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.environment import Env from couchpotato.environment import Env
from flask.app import Flask from tornado import template
from flask.blueprints import Blueprint from tornado.web import RequestHandler, authenticated
from flask.globals import request
from flask.helpers import url_for
from flask.templating import render_template
from sqlalchemy.engine import create_engine
from sqlalchemy.orm import scoped_session
from sqlalchemy.orm.session import sessionmaker
from werkzeug.utils import redirect
import os import os
import time import time
import traceback
log = CPLog(__name__) log = CPLog(__name__)
app = Flask(__name__, static_folder = 'nope')
web = Blueprint('web', __name__)
views = {}
template_loader = template.Loader(os.path.join(os.path.dirname(__file__), 'templates'))
class BaseHandler(RequestHandler):
def get_current_user(self):
username = Env.setting('username')
password = Env.setting('password')
if username and password:
return self.get_secure_cookie('user')
else: # Login when no username or password are set
return True
# Main web handler
class WebHandler(BaseHandler):
@authenticated
def get(self, route, *args, **kwargs):
route = route.strip('/')
if not views.get(route):
page_not_found(self)
return
try:
self.write(views[route]())
except:
log.error("Failed doing web request '%s': %s", (route, traceback.format_exc()))
self.write({'success': False, 'error': 'Failed returning results'})
def addView(route, func, static = False):
views[route] = func
def get_session(engine = None): def get_session(engine = None):
return Env.getSession(engine) return Env.getSession(engine)
def addView(route, func, static = False):
web.add_url_rule(route + ('' if static else '/'), endpoint = route if route else 'index', view_func = func)
""" Web view """ # Web view
@web.route('/')
@requires_auth
def index(): def index():
return render_template('index.html', sep = os.sep, fireEvent = fireEvent, env = Env) return template_loader.load('index.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env)
addView('', index)
""" Api view """ # API docs
@web.route('docs/')
@requires_auth
def apiDocs(): def apiDocs():
from couchpotato import app
routes = [] routes = []
for route, x in sorted(app.view_functions.iteritems()):
if route[0:4] == 'api.': for route in api.iterkeys():
routes += [route[4:].replace('::', '.')] routes.append(route)
if api_docs.get(''): if api_docs.get(''):
del api_docs[''] del api_docs['']
del api_docs_missing[''] del api_docs_missing['']
return render_template('api.html', fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing))
@web.route('getkey/') return template_loader.load('api.html').generate(fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing), Env = Env)
def getApiKey():
api = None addView('docs', apiDocs)
params = getParams()
username = Env.setting('username')
password = Env.setting('password')
if (params.get('u') == md5(username) or not username) and (params.get('p') == password or not password): # Make non basic auth option to get api key
api = Env.setting('api_key') class KeyHandler(RequestHandler):
def get(self, *args, **kwargs):
api = None
return jsonified({ try:
'success': api is not None, username = Env.setting('username')
'api_key': api password = Env.setting('password')
})
@app.errorhandler(404) if (self.get_argument('u') == md5(username) or not username) and (self.get_argument('p') == password or not password):
def page_not_found(error): api = Env.setting('api_key')
index_url = url_for('web.index')
url = request.path[len(index_url):] self.write({
'success': api is not None,
'api_key': api
})
except:
log.error('Failed doing key request: %s', (traceback.format_exc()))
self.write({'success': False, 'error': 'Failed returning results'})
class LoginHandler(BaseHandler):
def get(self, *args, **kwargs):
if self.get_current_user():
self.redirect(Env.get('web_base'))
else:
self.write(template_loader.load('login.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env))
def post(self, *args, **kwargs):
api = None
username = Env.setting('username')
password = Env.setting('password')
if (self.get_argument('username') == username or not username) and (md5(self.get_argument('password')) == password or not password):
api = Env.setting('api_key')
if api:
remember_me = tryInt(self.get_argument('remember_me', default = 0))
self.set_secure_cookie('user', api, expires_days = 30 if remember_me > 0 else None)
self.redirect(Env.get('web_base'))
class LogoutHandler(BaseHandler):
def get(self, *args, **kwargs):
self.clear_cookie('user')
self.redirect('%slogin/' % Env.get('web_base'))
def page_not_found(rh):
index_url = Env.get('web_base')
url = rh.request.uri[len(index_url):]
if url[:3] != 'api': if url[:3] != 'api':
if request.path != '/': r = index_url + '#' + url.lstrip('/')
r = request.url.replace(request.path, index_url + '#' + url) rh.redirect(r)
else:
r = '%s%s' % (request.url.rstrip('/'), index_url + '#' + url)
return redirect(r)
else: else:
if not Env.get('dev'): if not Env.get('dev'):
time.sleep(0.1) time.sleep(0.1)
return 'Wrong API key used', 404
rh.set_status(404)
rh.write('Wrong API key used')
+105 -27
View File
@@ -1,44 +1,65 @@
from flask.blueprints import Blueprint from couchpotato.core.helpers.request import getParams
from flask.helpers import url_for from couchpotato.core.logger import CPLog
from functools import wraps
from threading import Thread
from tornado.gen import coroutine
from tornado.web import RequestHandler, asynchronous from tornado.web import RequestHandler, asynchronous
from werkzeug.utils import redirect import json
import threading
import tornado
import traceback
import urllib
api = Blueprint('api', __name__) log = CPLog(__name__)
api_docs = {}
api_docs_missing = []
api = {}
api_locks = {}
api_nonblock = {} api_nonblock = {}
api_docs = {}
api_docs_missing = []
def run_async(func):
@wraps(func)
def async_func(*args, **kwargs):
func_hl = Thread(target = func, args = args, kwargs = kwargs)
func_hl.start()
return func_hl
return async_func
# NonBlock API handler
class NonBlockHandler(RequestHandler): class NonBlockHandler(RequestHandler):
stoppers = [] stopper = None
@asynchronous @asynchronous
def get(self, route): def get(self, route, *args, **kwargs):
route = route.strip('/')
start, stop = api_nonblock[route] start, stop = api_nonblock[route]
self.stoppers.append(stop) self.stopper = stop
start(self.onNewMessage, last_id = self.get_argument("last_id", None)) start(self.onNewMessage, last_id = self.get_argument('last_id', None))
def onNewMessage(self, response): def onNewMessage(self, response):
if self.request.connection.stream.closed(): if self.request.connection.stream.closed():
self.on_connection_close()
return return
self.finish(response)
try:
self.finish(response)
except:
log.debug('Failed doing nonblock request, probably already closed: %s', (traceback.format_exc()))
try: self.finish({'success': False, 'error': 'Failed returning results'})
except: pass
def on_connection_close(self): def on_connection_close(self):
for stop in self.stoppers: if self.stopper:
stop(self.onNewMessage) self.stopper(self.onNewMessage)
self.stoppers = [] self.stopper = None
def addApiView(route, func, static = False, docs = None, **kwargs):
api.add_url_rule(route + ('' if static else '/'), endpoint = route.replace('.', '::') if route else 'index', view_func = func, **kwargs)
if docs:
api_docs[route[4:] if route[0:4] == 'api.' else route] = docs
else:
api_docs_missing.append(route)
def addNonBlockApiView(route, func_tuple, docs = None, **kwargs): def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
api_nonblock[route] = func_tuple api_nonblock[route] = func_tuple
@@ -48,9 +69,66 @@ def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
else: else:
api_docs_missing.append(route) api_docs_missing.append(route)
""" Api view """ # Blocking API handler
def index(): class ApiHandler(RequestHandler):
index_url = url_for('web.index')
return redirect(index_url + 'docs/')
addApiView('', index) @coroutine
def get(self, route, *args, **kwargs):
route = route.strip('/')
if not api.get(route):
self.write('API call doesn\'t seem to exist')
return
api_locks[route].acquire()
try:
kwargs = {}
for x in self.request.arguments:
kwargs[x] = urllib.unquote(self.get_argument(x))
# Split array arguments
kwargs = getParams(kwargs)
# Remove t random string
try: del kwargs['t']
except: pass
# Add async callback handler
@run_async
def run_handler(callback):
try:
result = api[route](**kwargs)
callback(result)
except:
log.error('Failed doing api request "%s": %s', (route, traceback.format_exc()))
callback({'success': False, 'error': 'Failed returning results'})
result = yield tornado.gen.Task(run_handler)
# Check JSONP callback
jsonp_callback = self.get_argument('callback_func', default = None)
if jsonp_callback:
self.write(str(jsonp_callback) + '(' + json.dumps(result) + ')')
elif isinstance(result, tuple) and result[0] == 'redirect':
self.redirect(result[1])
else:
self.write(result)
except:
log.error('Failed doing api request "%s": %s', (route, traceback.format_exc()))
self.write({'success': False, 'error': 'Failed returning results'})
api_locks[route].release()
def addApiView(route, func, static = False, docs = None, **kwargs):
if static: func(route)
else:
api[route] = func
api_locks[route] = threading.Lock()
if docs:
api_docs[route[4:] if route[0:4] == 'api.' else route] = docs
else:
api_docs_missing.append(route)
+12 -13
View File
@@ -1,6 +1,5 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.helpers.variable import cleanHost, md5 from couchpotato.core.helpers.variable import cleanHost, md5
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
@@ -57,7 +56,7 @@ class Core(Plugin):
self.signalHandler() self.signalHandler()
def md5Password(self, value): def md5Password(self, value):
return md5(value.encode(Env.get('encoding'))) if value else '' return md5(value) if value else ''
def checkApikey(self, value): def checkApikey(self, value):
return value if value and len(value) > 3 else uuid4().hex return value if value and len(value) > 3 else uuid4().hex
@@ -68,12 +67,12 @@ class Core(Plugin):
return True return True
def available(self): def available(self, **kwargs):
return jsonified({ return {
'success': True 'success': True
}) }
def shutdown(self): def shutdown(self, **kwargs):
if self.shutdown_started: if self.shutdown_started:
return False return False
@@ -83,7 +82,7 @@ class Core(Plugin):
return 'shutdown' return 'shutdown'
def restart(self): def restart(self, **kwargs):
if self.shutdown_started: if self.shutdown_started:
return False return False
@@ -125,7 +124,7 @@ class Core(Plugin):
time.sleep(1) time.sleep(1)
log.debug('Save to shutdown/restart') log.debug('Safe to shutdown/restart')
try: try:
IOLoop.current().stop() IOLoop.current().stop()
@@ -156,10 +155,10 @@ class Core(Plugin):
host = 'localhost' host = 'localhost'
port = Env.setting('port') port = Env.setting('port')
return '%s:%d%s' % (cleanHost(host).rstrip('/'), int(port), '/' + Env.setting('url_base').lstrip('/') if Env.setting('url_base') else '') return '%s:%d%s' % (cleanHost(host).rstrip('/'), int(port), Env.get('web_base'))
def createApiUrl(self): def createApiUrl(self):
return '%s/api/%s' % (self.createBaseUrl(), Env.setting('api_key')) return '%sapi/%s' % (self.createBaseUrl(), Env.setting('api_key'))
def version(self): def version(self):
ver = fireEvent('updater.info', single = True) ver = fireEvent('updater.info', single = True)
@@ -170,10 +169,10 @@ class Core(Plugin):
return '%s - %s-%s - v2' % (platf, ver.get('version')['type'], ver.get('version')['hash']) return '%s - %s-%s - v2' % (platf, ver.get('version')['type'], ver.get('version')['hash'])
def versionView(self): def versionView(self, **kwargs):
return jsonified({ return {
'version': self.version() 'version': self.version()
}) }
def signalHandler(self): def signalHandler(self):
if Env.get('daemonized'): return if Env.get('daemonized'): return
+45 -8
View File
@@ -4,9 +4,11 @@ from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
from minify.cssmin import cssmin
from minify.jsmin import jsmin from minify.jsmin import jsmin
import cssprefixer from tornado.web import StaticFileHandler
import os import os
import re
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -79,7 +81,7 @@ class ClientScript(Plugin):
for static_type in self.core_static: for static_type in self.core_static:
for rel_path in self.core_static.get(static_type): for rel_path in self.core_static.get(static_type):
file_path = os.path.join(Env.get('app_dir'), 'couchpotato', 'static', rel_path) file_path = os.path.join(Env.get('app_dir'), 'couchpotato', 'static', rel_path)
core_url = 'api/%s/static/%s?%s' % (Env.setting('api_key'), rel_path, tryInt(os.path.getmtime(file_path))) core_url = 'static/%s' % rel_path
if static_type == 'script': if static_type == 'script':
self.registerScript(core_url, file_path, position = 'front') self.registerScript(core_url, file_path, position = 'front')
@@ -89,6 +91,13 @@ class ClientScript(Plugin):
def minify(self): def minify(self):
# Create cache dir
cache = Env.get('cache_dir')
parent_dir = os.path.join(cache, 'minified')
self.makeDir(parent_dir)
Env.get('app').add_handlers(".*$", [(Env.get('web_base') + 'minified/(.*)', StaticFileHandler, {'path': parent_dir})])
for file_type in ['style', 'script']: for file_type in ['style', 'script']:
ext = 'js' if file_type is 'script' else 'css' ext = 'js' if file_type is 'script' else 'css'
positions = self.paths.get(file_type, {}) positions = self.paths.get(file_type, {})
@@ -99,8 +108,8 @@ class ClientScript(Plugin):
def _minify(self, file_type, files, position, out): def _minify(self, file_type, files, position, out):
cache = Env.get('cache_dir') cache = Env.get('cache_dir')
out_name = 'minified_' + out out_name = out
out = os.path.join(cache, out_name) out = os.path.join(cache, 'minified', out_name)
raw = [] raw = []
for file_path in files: for file_path in files:
@@ -109,7 +118,8 @@ class ClientScript(Plugin):
if file_type == 'script': if file_type == 'script':
data = jsmin(f) data = jsmin(f)
else: else:
data = cssprefixer.process(f, debug = False, minify = True) data = self.prefix(f)
data = cssmin(data)
data = data.replace('../images/', '../static/images/') data = data.replace('../images/', '../static/images/')
data = data.replace('../fonts/', '../static/fonts/') data = data.replace('../fonts/', '../static/fonts/')
data = data.replace('../../static/', '../static/') # Replace inside plugins data = data.replace('../../static/', '../static/') # Replace inside plugins
@@ -119,17 +129,17 @@ class ClientScript(Plugin):
# Combine all files together with some comments # Combine all files together with some comments
data = '' data = ''
for r in raw: for r in raw:
data += self.comment.get(file_type) % (r.get('file'), r.get('date')) data += self.comment.get(file_type) % (ss(r.get('file')), r.get('date'))
data += r.get('data') + '\n\n' data += r.get('data') + '\n\n'
self.createFile(out, ss(data.strip())) self.createFile(out, data.strip())
if not self.minified.get(file_type): if not self.minified.get(file_type):
self.minified[file_type] = {} self.minified[file_type] = {}
if not self.minified[file_type].get(position): if not self.minified[file_type].get(position):
self.minified[file_type][position] = [] self.minified[file_type][position] = []
minified_url = 'api/%s/file.cache/%s?%s' % (Env.setting('api_key'), out_name, tryInt(os.path.getmtime(out))) minified_url = 'minified/%s?%s' % (out_name, tryInt(os.path.getmtime(out)))
self.minified[file_type][position].append(minified_url) self.minified[file_type][position].append(minified_url)
def getStyles(self, *args, **kwargs): def getStyles(self, *args, **kwargs):
@@ -163,6 +173,8 @@ class ClientScript(Plugin):
def register(self, api_path, file_path, type, location): def register(self, api_path, file_path, type, location):
api_path = '%s?%s' % (api_path, tryInt(os.path.getmtime(file_path)))
if not self.urls[type].get(location): if not self.urls[type].get(location):
self.urls[type][location] = [] self.urls[type][location] = []
self.urls[type][location].append(api_path) self.urls[type][location].append(api_path)
@@ -170,3 +182,28 @@ class ClientScript(Plugin):
if not self.paths[type].get(location): if not self.paths[type].get(location):
self.paths[type][location] = [] self.paths[type][location] = []
self.paths[type][location].append(file_path) self.paths[type][location].append(file_path)
prefix_properties = ['border-radius', 'transform', 'transition', 'box-shadow']
prefix_tags = ['ms', 'moz', 'webkit']
def prefix(self, data):
trimmed_data = re.sub('(\t|\n|\r)+', '', data)
new_data = ''
colon_split = trimmed_data.split(';')
for splt in colon_split:
curl_split = splt.strip().split('{')
for curly in curl_split:
curly = curly.strip()
for prop in self.prefix_properties:
if curly[:len(prop) + 1] == prop + ':':
for tag in self.prefix_tags:
new_data += ' -%s-%s; ' % (tag, curly)
new_data += curly + (' { ' if len(curl_split) > 1 else ' ')
new_data += '; '
new_data = new_data.replace('{ ;', '; ').replace('} ;', '} ')
return new_data
+18 -21
View File
@@ -1,7 +1,6 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import ss from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
@@ -33,10 +32,10 @@ class Updater(Plugin):
else: else:
self.updater = SourceUpdater() self.updater = SourceUpdater()
addEvent('app.load', self.autoUpdate) addEvent('app.load', self.setCrons)
addEvent('updater.info', self.info) addEvent('updater.info', self.info)
addApiView('updater.info', self.getInfo, docs = { addApiView('updater.info', self.info, docs = {
'desc': 'Get updater information', 'desc': 'Get updater information',
'return': { 'return': {
'type': 'object', 'type': 'object',
@@ -62,7 +61,7 @@ class Updater(Plugin):
self.autoUpdate() # Check after enabling self.autoUpdate() # Check after enabling
def autoUpdate(self): def autoUpdate(self):
if self.check() and self.conf('automatic') and not self.updater.update_failed: if self.isEnabled() and self.check() and self.conf('automatic') and not self.updater.update_failed:
if self.updater.doUpdate(): if self.updater.doUpdate():
# Notify before restarting # Notify before restarting
@@ -80,31 +79,30 @@ class Updater(Plugin):
return False return False
def check(self): def check(self, force = False):
if self.isDisabled(): if not force and self.isDisabled():
return return
if self.updater.check(): if self.updater.check():
if not self.available_notified and self.conf('notification') and not self.conf('automatic'): if not self.available_notified and self.conf('notification') and not self.conf('automatic'):
fireEvent('updater.available', message = 'A new update is available', data = self.updater.info()) info = self.updater.info()
version_date = datetime.fromtimestamp(info['update_version']['date'])
fireEvent('updater.available', message = 'A new update with hash "%s" is available, this version is from %s' % (info['update_version']['hash'], version_date), data = info)
self.available_notified = True self.available_notified = True
return True return True
return False return False
def info(self): def info(self, **kwargs):
return self.updater.info() return self.updater.info()
def getInfo(self): def checkView(self, **kwargs):
return jsonified(self.updater.info()) return {
'update_available': self.check(force = True),
def checkView(self):
return jsonified({
'update_available': self.check(),
'info': self.updater.info() 'info': self.updater.info()
}) }
def doUpdateView(self): def doUpdateView(self, **kwargs):
self.check() self.check()
if not self.updater.update_version: if not self.updater.update_version:
@@ -119,9 +117,9 @@ class Updater(Plugin):
if not success: if not success:
success = True success = True
return jsonified({ return {
'success': success 'success': success
}) }
class BaseUpdater(Plugin): class BaseUpdater(Plugin):
@@ -134,13 +132,11 @@ class BaseUpdater(Plugin):
update_failed = False update_failed = False
update_version = None update_version = None
last_check = 0 last_check = 0
auto_register_static = False
def doUpdate(self): def doUpdate(self):
pass pass
def getInfo(self):
return jsonified(self.info())
def info(self): def info(self):
return { return {
'last_check': self.last_check, 'last_check': self.last_check,
@@ -279,6 +275,7 @@ class SourceUpdater(BaseUpdater):
if download_data.get('type') == 'zip': if download_data.get('type') == 'zip':
zip = zipfile.ZipFile(destination) zip = zipfile.ZipFile(destination)
zip.extractall(extracted_path) zip.extractall(extracted_path)
zip.close()
else: else:
tar = tarfile.open(destination) tar = tarfile.open(destination)
tar.extractall(path = extracted_path) tar.extractall(path = extracted_path)
-26
View File
@@ -1,26 +0,0 @@
from couchpotato.core.helpers.variable import md5
from couchpotato.environment import Env
from flask import request, Response
from functools import wraps
def check_auth(username, password):
return username == Env.setting('username') and password == Env.setting('password')
def authenticate():
return Response(
'This is not the page you are looking for. *waves hand*', 401,
{'WWW-Authenticate': 'Basic realm="CouchPotato Login"'}
)
def requires_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = getattr(request, 'authorization')
if Env.setting('username') and Env.setting('password'):
if (not auth or not check_auth(auth.username.decode('latin1'), md5(auth.password.decode('latin1').encode(Env.get('encoding'))))):
return authenticate()
return f(*args, **kwargs)
return decorated
+59 -20
View File
@@ -11,12 +11,12 @@ log = CPLog(__name__)
class Downloader(Provider): class Downloader(Provider):
type = [] protocol = []
http_time_between_calls = 0 http_time_between_calls = 0
torrent_sources = [ torrent_sources = [
'http://torrage.com/torrent/%s.torrent', 'http://torrage.com/torrent/%s.torrent',
'http://torcache.net/torrent/%s.torrent', 'https://torcache.net/torrent/%s.torrent',
] ]
torrent_trackers = [ torrent_trackers = [
@@ -36,18 +36,23 @@ class Downloader(Provider):
def __init__(self): def __init__(self):
addEvent('download', self._download) addEvent('download', self._download)
addEvent('download.enabled', self._isEnabled) addEvent('download.enabled', self._isEnabled)
addEvent('download.enabled_types', self.getEnabledDownloadType) addEvent('download.enabled_protocols', self.getEnabledProtocol)
addEvent('download.status', self._getAllDownloadStatus) addEvent('download.status', self._getAllDownloadStatus)
addEvent('download.remove_failed', self._removeFailed) addEvent('download.remove_failed', self._removeFailed)
addEvent('download.pause', self._pause)
addEvent('download.process_complete', self._processComplete)
def getEnabledDownloadType(self): def getEnabledProtocol(self):
for download_type in self.type: for download_protocol in self.protocol:
if self.isEnabled(manual = True, data = {'type': download_type}): if self.isEnabled(manual = True, data = {'protocol': download_protocol}):
return self.type return self.protocol
return [] return []
def _download(self, data = {}, movie = {}, manual = False, filedata = None): def _download(self, data = None, movie = None, manual = False, filedata = None):
if not movie: movie = {}
if not data: data = {}
if self.isDisabled(manual, data): if self.isDisabled(manual, data):
return return
return self.download(data = data, movie = movie, filedata = filedata) return self.download(data = data, movie = movie, filedata = filedata)
@@ -65,19 +70,35 @@ class Downloader(Provider):
if self.isDisabled(manual = True, data = {}): if self.isDisabled(manual = True, data = {}):
return return
if self.conf('delete_failed', default = True): if item and item.get('downloader') == self.getName():
return self.removeFailed(item) if self.conf('delete_failed'):
return self.removeFailed(item)
return False return False
return
def removeFailed(self, item): def removeFailed(self, item):
return return
def isCorrectType(self, item_type): def _processComplete(self, item):
is_correct = item_type in self.type if self.isDisabled(manual = True, data = {}):
return
if item and item.get('downloader') == self.getName():
if self.conf('remove_complete', default = False):
return self.processComplete(item = item, delete_files = self.conf('delete_files', default = False))
return False
return
def processComplete(self, item, delete_files):
return
def isCorrectProtocol(self, item_protocol):
is_correct = item_protocol in self.protocol
if not is_correct: if not is_correct:
log.debug("Downloader doesn't support this type") log.debug("Downloader doesn't support this protocol")
return is_correct return is_correct
@@ -101,7 +122,7 @@ class Downloader(Provider):
except: except:
log.debug('Torrent hash "%s" wasn\'t found on: %s', (torrent_hash, source)) log.debug('Torrent hash "%s" wasn\'t found on: %s', (torrent_hash, source))
log.error('Failed converting magnet url to torrent: %s', (torrent_hash)) log.error('Failed converting magnet url to torrent: %s', torrent_hash)
return False return False
def downloadReturnId(self, download_id): def downloadReturnId(self, download_id):
@@ -110,20 +131,38 @@ class Downloader(Provider):
'id': download_id 'id': download_id
} }
def isDisabled(self, manual, data): def isDisabled(self, manual = False, data = None):
if not data: data = {}
return not self.isEnabled(manual, data) return not self.isEnabled(manual, data)
def _isEnabled(self, manual, data = {}): def _isEnabled(self, manual, data = None):
if not data: data = {}
if not self.isEnabled(manual, data): if not self.isEnabled(manual, data):
return return
return True return True
def isEnabled(self, manual, data = {}): def isEnabled(self, manual = False, data = None):
if not data: data = {}
d_manual = self.conf('manual', default = False) d_manual = self.conf('manual', default = False)
return super(Downloader, self).isEnabled() and \ return super(Downloader, self).isEnabled() and \
((d_manual and manual) or (d_manual is False)) and \ (d_manual and manual or d_manual is False) and \
(not data or self.isCorrectType(data.get('type'))) (not data or self.isCorrectProtocol(data.get('protocol')))
def _pause(self, item, pause = True):
if self.isDisabled(manual = True, data = {}):
return
if item and item.get('downloader') == self.getName():
self.pause(item, pause)
return True
return False
def pause(self, item, pause):
return
class StatusList(list): class StatusList(list):
+18 -14
View File
@@ -7,22 +7,25 @@ import traceback
log = CPLog(__name__) log = CPLog(__name__)
class Blackhole(Downloader): class Blackhole(Downloader):
type = ['nzb', 'torrent', 'torrent_magnet'] protocol = ['nzb', 'torrent', 'torrent_magnet']
def download(self, data = {}, movie = {}, filedata = None): def download(self, data = None, movie = None, filedata = None):
if not movie: movie = {}
if not data: data = {}
directory = self.conf('directory') directory = self.conf('directory')
if not directory or not os.path.isdir(directory): if not directory or not os.path.isdir(directory):
log.error('No directory set for blackhole %s download.', data.get('type')) log.error('No directory set for blackhole %s download.', data.get('protocol'))
else: else:
try: try:
if not filedata or len(filedata) < 50: if not filedata or len(filedata) < 50:
try: try:
if data.get('type') == 'torrent_magnet': if data.get('protocol') == 'torrent_magnet':
filedata = self.magnetToTorrent(data.get('url')) filedata = self.magnetToTorrent(data.get('url'))
data['type'] = 'torrent' data['protocol'] = 'torrent'
except: except:
log.error('Failed download torrent via magnet url: %s', traceback.format_exc()) log.error('Failed download torrent via magnet url: %s', traceback.format_exc())
@@ -34,7 +37,7 @@ class Blackhole(Downloader):
try: try:
if not os.path.isfile(fullPath): if not os.path.isfile(fullPath):
log.info('Downloading %s to %s.', (data.get('type'), fullPath)) log.info('Downloading %s to %s.', (data.get('protocol'), fullPath))
with open(fullPath, 'wb') as f: with open(fullPath, 'wb') as f:
f.write(filedata) f.write(filedata)
os.chmod(fullPath, Env.getPermission('file')) os.chmod(fullPath, Env.getPermission('file'))
@@ -53,20 +56,21 @@ class Blackhole(Downloader):
return False return False
def getEnabledDownloadType(self): def getEnabledProtocol(self):
if self.conf('use_for') == 'both': if self.conf('use_for') == 'both':
return super(Blackhole, self).getEnabledDownloadType() return super(Blackhole, self).getEnabledProtocol()
elif self.conf('use_for') == 'torrent': elif self.conf('use_for') == 'torrent':
return ['torrent', 'torrent_magnet'] return ['torrent', 'torrent_magnet']
else: else:
return ['nzb'] return ['nzb']
def isEnabled(self, manual, data = {}): def isEnabled(self, manual = False, data = None):
for_type = ['both'] if not data: data = {}
if data and 'torrent' in data.get('type'): for_protocol = ['both']
for_type.append('torrent') if data and 'torrent' in data.get('protocol'):
for_protocol.append('torrent')
elif data: elif data:
for_type.append(data.get('type')) for_protocol.append(data.get('protocol'))
return super(Blackhole, self).isEnabled(manual, data) and \ return super(Blackhole, self).isEnabled(manual, data) and \
((self.conf('use_for') in for_type)) ((self.conf('use_for') in for_protocol))
@@ -0,0 +1,90 @@
from .main import Deluge
def start():
return Deluge()
config = [{
'name': 'deluge',
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'deluge',
'label': 'Deluge',
'description': 'Use <a href="http://www.deluge-torrent.org/" target="_blank">Deluge</a> to download torrents.',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
'radio_group': 'torrent',
},
{
'name': 'host',
'default': 'localhost:58846',
'description': 'Hostname with port. Usually <strong>localhost:58846</strong>',
},
{
'name': 'username',
},
{
'name': 'password',
'type': 'password',
},
{
'name': 'directory',
'type': 'directory',
'description': 'Download to this directory. Keep empty for default Deluge download directory.',
},
{
'name': 'completed_directory',
'type': 'directory',
'description': 'Move completed torrent to this directory. Keep empty for default Deluge options.',
'advanced': True,
},
{
'name': 'label',
'description': 'Label to add to torrents in the Deluge UI.',
},
{
'name': 'remove_complete',
'label': 'Remove torrent',
'type': 'bool',
'default': True,
'advanced': True,
'description': 'Remove the torrent from Deluge after it has finished seeding.',
},
{
'name': 'delete_files',
'label': 'Remove files',
'default': True,
'type': 'bool',
'advanced': True,
'description': 'Also remove the leftover files.',
},
{
'name': 'paused',
'type': 'bool',
'advanced': True,
'default': False,
'description': 'Add the torrent paused.',
},
{
'name': 'manual',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
},
{
'name': 'delete_failed',
'default': True,
'advanced': True,
'type': 'bool',
'description': 'Delete a release after the download has failed.',
},
],
}
],
}]
+244
View File
@@ -0,0 +1,244 @@
from base64 import b64encode
from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import isInt, ss
from couchpotato.core.helpers.variable import tryFloat
from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
from datetime import timedelta
from synchronousdeluge import DelugeClient
import os.path
import traceback
log = CPLog(__name__)
class Deluge(Downloader):
protocol = ['torrent', 'torrent_magnet']
log = CPLog(__name__)
drpc = None
def connect(self):
# Load host from config and split out port.
host = self.conf('host').split(':')
if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.')
return False
if not self.drpc:
self.drpc = DelugeRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
return self.drpc
def download(self, data, movie, filedata = None):
log.info('Sending "%s" (%s) to Deluge.', (data.get('name'), data.get('protocol')))
if not self.connect():
return False
if not filedata and data.get('protocol') == 'torrent':
log.error('Failed sending torrent, no data')
return False
# Set parameters for Deluge
options = {
'add_paused': self.conf('paused', default = 0),
'label': self.conf('label')
}
if self.conf('directory'):
if os.path.isdir(self.conf('directory')):
options['download_location'] = self.conf('directory')
else:
log.error('Download directory from Deluge settings: %s doesn\'t exist', self.conf('directory'))
if self.conf('completed_directory'):
if os.path.isdir(self.conf('completed_directory')):
options['move_completed'] = 1
options['move_completed_path'] = self.conf('completed_directory')
else:
log.error('Download directory from Deluge settings: %s doesn\'t exist', self.conf('directory'))
if data.get('seed_ratio'):
options['stop_at_ratio'] = 1
options['stop_ratio'] = tryFloat(data.get('seed_ratio'))
# Deluge only has seed time as a global option. Might be added in
# in a future API release.
# if data.get('seed_time'):
# Send request to Deluge
if data.get('protocol') == 'torrent_magnet':
remote_torrent = self.drpc.add_torrent_magnet(data.get('url'), options)
else:
filename = self.createFileName(data, filedata, movie)
remote_torrent = self.drpc.add_torrent_file(filename, b64encode(filedata), options)
if not remote_torrent:
log.error('Failed sending torrent to Deluge')
return False
log.info('Torrent sent to Deluge successfully.')
return self.downloadReturnId(remote_torrent)
def getAllDownloadStatus(self):
log.debug('Checking Deluge download status.')
if not os.path.isdir(Env.setting('from', 'renamer')):
log.error('Renamer "from" folder doesn\'t to exist.')
return
if not self.connect():
return False
statuses = StatusList(self)
queue = self.drpc.get_alltorrents()
if not queue:
log.debug('Nothing in queue or error')
return False
for torrent_id in queue:
item = queue[torrent_id]
log.debug('name=%s / id=%s / save_path=%s / move_completed_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / stop_ratio=%s / is_seed=%s / is_finished=%s / paused=%s', (item['name'], item['hash'], item['save_path'], item['move_completed_path'], item['hash'], item['progress'], item['state'], item['eta'], item['ratio'], item['stop_ratio'], item['is_seed'], item['is_finished'], item['paused']))
# Deluge has no easy way to work out if a torrent is stalled or failing.
#status = 'failed'
status = 'busy'
if item['is_seed'] and tryFloat(item['ratio']) < tryFloat(item['stop_ratio']):
# We have item['seeding_time'] to work out what the seeding time is, but we do not
# have access to the downloader seed_time, as with deluge we have no way to pass it
# when the torrent is added. So Deluge will only look at the ratio.
# See above comment in download().
status = 'seeding'
elif item['is_seed'] and item['is_finished'] and item['paused'] and item['state'] == 'Paused':
status = 'completed'
download_dir = item['save_path']
if item['move_on_completed']:
download_dir = item['move_completed_path']
statuses.append({
'id': item['hash'],
'name': item['name'],
'status': status,
'original_status': item['state'],
'seed_ratio': item['ratio'],
'timeleft': str(timedelta(seconds = item['eta'])),
'folder': ss(os.path.join(download_dir, item['name'])),
})
return statuses
def pause(self, item, pause = True):
if pause:
return self.drpc.pause_torrent([item['id']])
else:
return self.drpc.resume_torrent([item['id']])
def removeFailed(self, item):
log.info('%s failed downloading, deleting...', item['name'])
return self.drpc.remove_torrent(item['id'], True)
def processComplete(self, item, delete_files = False):
log.debug('Requesting Deluge to remove the torrent %s%s.', (item['name'], ' and cleanup the downloaded files' if delete_files else ''))
return self.drpc.remove_torrent(item['id'], remove_local_data = delete_files)
class DelugeRPC(object):
host = 'localhost'
port = 58846
username = None
password = None
client = None
def __init__(self, host = 'localhost', port = 58846, username = None, password = None):
super(DelugeRPC, self).__init__()
self.host = host
self.port = port
self.username = username
self.password = password
def connect(self):
self.client = DelugeClient()
self.client.connect(self.host, int(self.port), self.username, self.password)
def add_torrent_magnet(self, torrent, options):
torrent_id = False
try:
self.connect()
torrent_id = self.client.core.add_torrent_magnet(torrent, options).get()
if options['label']:
self.client.label.set_torrent(torrent_id, options['label']).get()
except Exception, err:
log.error('Failed to add torrent magnet %s: %s %s', (torrent, err, traceback.format_exc()))
finally:
if self.client:
self.disconnect()
return torrent_id
def add_torrent_file(self, filename, torrent, options):
torrent_id = False
try:
self.connect()
torrent_id = self.client.core.add_torrent_file(filename, torrent, options).get()
if options['label']:
self.client.label.set_torrent(torrent_id, options['label']).get()
except Exception, err:
log.error('Failed to add torrent file %s: %s %s', (filename, err, traceback.format_exc()))
finally:
if self.client:
self.disconnect()
return torrent_id
def get_alltorrents(self):
ret = False
try:
self.connect()
ret = self.client.core.get_torrents_status({}, {}).get()
except Exception, err:
log.error('Failed to get all torrents: %s %s', (err, traceback.format_exc()))
finally:
if self.client:
self.disconnect()
return ret
def pause_torrent(self, torrent_ids):
try:
self.connect()
self.client.core.pause_torrent(torrent_ids).get()
except Exception, err:
log.error('Failed to pause torrent: %s %s', (err, traceback.format_exc()))
finally:
if self.client:
self.disconnect()
def resume_torrent(self, torrent_ids):
try:
self.connect()
self.client.core.resume_torrent(torrent_ids).get()
except Exception, err:
log.error('Failed to resume torrent: %s %s', (err, traceback.format_exc()))
finally:
if self.client:
self.disconnect()
def remove_torrent(self, torrent_id, remove_local_data):
ret = False
try:
self.connect()
ret = self.client.core.remove_torrent(torrent_id, remove_local_data).get()
except Exception, err:
log.error('Failed to remove torrent: %s %s', (err, traceback.format_exc()))
finally:
if self.client:
self.disconnect()
return ret
def disconnect(self):
self.client.disconnect()
@@ -42,6 +42,7 @@ config = [{
}, },
{ {
'name': 'priority', 'name': 'priority',
'advanced': True,
'default': '0', 'default': '0',
'type': 'dropdown', 'type': 'dropdown',
'values': [('Very Low', -100), ('Low', -50), ('Normal', 0), ('High', 50), ('Very High', 100)], 'values': [('Very Low', -100), ('Low', -50), ('Normal', 0), ('High', 50), ('Very High', 100)],
@@ -57,6 +58,7 @@ config = [{
{ {
'name': 'delete_failed', 'name': 'delete_failed',
'default': True, 'default': True,
'advanced': True,
'type': 'bool', 'type': 'bool',
'description': 'Delete a release after the download has failed.', 'description': 'Delete a release after the download has failed.',
}, },
+30 -13
View File
@@ -12,13 +12,16 @@ import xmlrpclib
log = CPLog(__name__) log = CPLog(__name__)
class NZBGet(Downloader): class NZBGet(Downloader):
type = ['nzb'] protocol = ['nzb']
url = 'http://%(username)s:%(password)s@%(host)s/xmlrpc' url = 'http://%(username)s:%(password)s@%(host)s/xmlrpc'
def download(self, data = {}, movie = {}, filedata = None): def download(self, data = None, movie = None, filedata = None):
if not movie: movie = {}
if not data: data = {}
if not filedata: if not filedata:
log.error('Unable to get NZB file: %s', traceback.format_exc()) log.error('Unable to get NZB file: %s', traceback.format_exc())
@@ -32,7 +35,7 @@ class NZBGet(Downloader):
rpc = xmlrpclib.ServerProxy(url) rpc = xmlrpclib.ServerProxy(url)
try: try:
if rpc.writelog('INFO', 'CouchPotato connected to drop off %s.' % nzb_name): if rpc.writelog('INFO', 'CouchPotato connected to drop off %s.' % nzb_name):
log.info('Successfully connected to NZBGet') log.debug('Successfully connected to NZBGet')
else: else:
log.info('Successfully connected to NZBGet, but unable to send a message') log.info('Successfully connected to NZBGet, but unable to send a message')
except socket.error: except socket.error:
@@ -73,7 +76,7 @@ class NZBGet(Downloader):
rpc = xmlrpclib.ServerProxy(url) rpc = xmlrpclib.ServerProxy(url)
try: try:
if rpc.writelog('INFO', 'CouchPotato connected to check status'): if rpc.writelog('INFO', 'CouchPotato connected to check status'):
log.info('Successfully connected to NZBGet') log.debug('Successfully connected to NZBGet')
else: else:
log.info('Successfully connected to NZBGet, but unable to send a message') log.info('Successfully connected to NZBGet, but unable to send a message')
except socket.error: except socket.error:
@@ -104,12 +107,21 @@ class NZBGet(Downloader):
nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0] nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0]
except: except:
nzb_id = item['NZBID'] nzb_id = item['NZBID']
timeleft = -1
try:
if item['ActiveDownloads'] > 0 and item['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']):
timeleft = str(timedelta(seconds = item['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20))
except:
pass
statuses.append({ statuses.append({
'id': nzb_id, 'id': nzb_id,
'name': item['NZBFilename'], 'name': item['NZBFilename'],
'original_status': 'DOWNLOADING' if item['ActiveDownloads'] > 0 else 'QUEUED', 'original_status': 'DOWNLOADING' if item['ActiveDownloads'] > 0 else 'QUEUED',
# Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item # Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item
'timeleft': str(timedelta(seconds = item['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20)) if item['ActiveDownloads'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']) else -1, 'timeleft': timeleft,
}) })
for item in queue: # 'Parameters' is not passed in rpc.postqueue for item in queue: # 'Parameters' is not passed in rpc.postqueue
@@ -130,10 +142,10 @@ class NZBGet(Downloader):
statuses.append({ statuses.append({
'id': nzb_id, 'id': nzb_id,
'name': item['NZBFilename'], 'name': item['NZBFilename'],
'status': 'completed' if item['ParStatus'] == 'SUCCESS' and item['ScriptStatus'] == 'SUCCESS' else 'failed', 'status': 'completed' if item['ParStatus'] in ['SUCCESS','NONE'] and item['ScriptStatus'] in ['SUCCESS','NONE'] else 'failed',
'original_status': item['ParStatus'] + ', ' + item['ScriptStatus'], 'original_status': item['ParStatus'] + ', ' + item['ScriptStatus'],
'timeleft': str(timedelta(seconds = 0)), 'timeleft': str(timedelta(seconds = 0)),
'folder': item['DestDir'] 'folder': ss(item['DestDir'])
}) })
return statuses return statuses
@@ -142,12 +154,12 @@ class NZBGet(Downloader):
log.info('%s failed downloading, deleting...', item['name']) log.info('%s failed downloading, deleting...', item['name'])
url = self.url % {'host': self.conf('host'), 'password': self.conf('password')} url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
rpc = xmlrpclib.ServerProxy(url) rpc = xmlrpclib.ServerProxy(url)
try: try:
if rpc.writelog('INFO', 'CouchPotato connected to delete some history'): if rpc.writelog('INFO', 'CouchPotato connected to delete some history'):
log.info('Successfully connected to NZBGet') log.debug('Successfully connected to NZBGet')
else: else:
log.info('Successfully connected to NZBGet, but unable to send a message') log.info('Successfully connected to NZBGet, but unable to send a message')
except socket.error: except socket.error:
@@ -162,11 +174,16 @@ class NZBGet(Downloader):
try: try:
history = rpc.history() history = rpc.history()
nzb_id = None
path = None
for hist in history: for hist in history:
if hist['Parameters'] and hist['Parameters']['couchpotato'] and hist['Parameters']['couchpotato'] == item['id']: for param in hist['Parameters']:
nzb_id = hist['ID'] if param['Name'] == 'couchpotato' and param['Value'] == item['id']:
path = hist['DestDir'] nzb_id = hist['ID']
if rpc.editqueue('HistoryDelete', 0, "", [tryInt(nzb_id)]): path = hist['DestDir']
if nzb_id and path and rpc.editqueue('HistoryDelete', 0, "", [tryInt(nzb_id)]):
shutil.rmtree(path, True) shutil.rmtree(path, True)
except: except:
log.error('Failed deleting: %s', traceback.format_exc(0)) log.error('Failed deleting: %s', traceback.format_exc(0))
@@ -38,6 +38,7 @@ config = [{
{ {
'name': 'delete_failed', 'name': 'delete_failed',
'default': True, 'default': True,
'advanced': True,
'type': 'bool', 'type': 'bool',
'description': 'Delete a release after the download has failed.', 'description': 'Delete a release after the download has failed.',
}, },
+12 -7
View File
@@ -16,13 +16,16 @@ import urllib2
log = CPLog(__name__) log = CPLog(__name__)
class NZBVortex(Downloader): class NZBVortex(Downloader):
type = ['nzb'] protocol = ['nzb']
api_level = None api_level = None
session_id = None session_id = None
def download(self, data = {}, movie = {}, filedata = None): def download(self, data = None, movie = None, filedata = None):
if not movie: movie = {}
if not data: data = {}
# Send the nzb # Send the nzb
try: try:
@@ -55,8 +58,8 @@ class NZBVortex(Downloader):
'name': item['uiTitle'], 'name': item['uiTitle'],
'status': status, 'status': status,
'original_status': item['state'], 'original_status': item['state'],
'timeleft': -1, 'timeleft':-1,
'folder': item['destinationPath'], 'folder': ss(item['destinationPath']),
}) })
return statuses return statuses
@@ -96,9 +99,10 @@ class NZBVortex(Downloader):
return False return False
def call(self, call, parameters = {}, repeat = False, auth = True, *args, **kwargs): def call(self, call, parameters = None, repeat = False, auth = True, *args, **kwargs):
# Login first # Login first
if not parameters: parameters = {}
if not self.session_id and auth: if not self.session_id and auth:
self.login() self.login()
@@ -121,7 +125,7 @@ class NZBVortex(Downloader):
# Try login and do again # Try login and do again
if not repeat: if not repeat:
self.login() self.login()
return self.call(call, parameters = parameters, repeat = True, *args, **kwargs) return self.call(call, parameters = parameters, repeat = True, **kwargs)
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc())) log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
except: except:
@@ -147,7 +151,8 @@ class NZBVortex(Downloader):
return self.api_level return self.api_level
def isEnabled(self, manual, data): def isEnabled(self, manual = False, data = None):
if not data: data = {}
return super(NZBVortex, self).isEnabled(manual, data) and self.getApiLevel() return super(NZBVortex, self).isEnabled(manual, data) and self.getApiLevel()
@@ -6,12 +6,15 @@ import traceback
log = CPLog(__name__) log = CPLog(__name__)
class Pneumatic(Downloader): class Pneumatic(Downloader):
type = ['nzb'] protocol = ['nzb']
strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s' strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s'
def download(self, data = {}, movie = {}, filedata = None): def download(self, data = None, movie = None, filedata = None):
if not movie: movie = {}
if not data: data = {}
directory = self.conf('directory') directory = self.conf('directory')
if not directory or not os.path.isdir(directory): if not directory or not os.path.isdir(directory):
@@ -26,7 +29,7 @@ class Pneumatic(Downloader):
try: try:
if not os.path.isfile(fullPath): if not os.path.isfile(fullPath):
log.info('Downloading %s to %s.', (data.get('type'), fullPath)) log.info('Downloading %s to %s.', (data.get('protocol'), fullPath))
with open(fullPath, 'wb') as f: with open(fullPath, 'wb') as f:
f.write(filedata) f.write(filedata)
+71
View File
@@ -0,0 +1,71 @@
from .main import rTorrent
def start():
return rTorrent()
config = [{
'name': 'rtorrent',
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'rtorrent',
'label': 'rTorrent',
'description': '',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
'radio_group': 'torrent',
},
{
'name': 'url',
'default': 'http://localhost:80/RPC2',
},
{
'name': 'username',
},
{
'name': 'password',
'type': 'password',
},
{
'name': 'label',
'description': 'Label to apply on added torrents.',
},
{
'name': 'remove_complete',
'label': 'Remove torrent',
'default': False,
'advanced': True,
'type': 'bool',
'description': 'Remove the torrent after it finishes seeding.',
},
{
'name': 'delete_files',
'label': 'Remove files',
'default': True,
'type': 'bool',
'advanced': True,
'description': 'Also remove the leftover files.',
},
{
'name': 'paused',
'type': 'bool',
'advanced': True,
'default': False,
'description': 'Add the torrent paused.',
},
{
'name': 'manual',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
},
],
}
],
}]
+201
View File
@@ -0,0 +1,201 @@
from base64 import b16encode, b32decode
from bencode import bencode, bdecode
from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import ss
from couchpotato.core.logger import CPLog
from datetime import timedelta
from hashlib import sha1
from rtorrent import RTorrent
from rtorrent.err import MethodError
import shutil
log = CPLog(__name__)
class rTorrent(Downloader):
protocol = ['torrent', 'torrent_magnet']
rt = None
def connect(self):
# Already connected?
if self.rt is not None:
return self.rt
# Ensure url is set
if not self.conf('url'):
log.error('Config properties are not filled in correctly, url is missing.')
return False
if self.conf('username') and self.conf('password'):
self.rt = RTorrent(
self.conf('url'),
self.conf('username'),
self.conf('password')
)
else:
self.rt = RTorrent(self.conf('url'))
return self.rt
def _update_provider_group(self, name, data):
if data.get('seed_time'):
log.info('seeding time ignored, not supported')
if not name:
return False
if not self.connect():
return False
views = self.rt.get_views()
if name not in views:
self.rt.create_group(name)
group = self.rt.get_group(name)
try:
if data.get('seed_ratio'):
ratio = int(float(data.get('seed_ratio')) * 100)
log.debug('Updating provider ratio to %s, group name: %s', (ratio, name))
# Explicitly set all group options to ensure it is setup correctly
group.set_upload('1M')
group.set_min(ratio)
group.set_max(ratio)
group.set_command('d.stop')
group.enable()
else:
# Reset group action and disable it
group.set_command()
group.disable()
except MethodError, err:
log.error('Unable to set group options: %s', err.message)
return False
return True
def download(self, data, movie, filedata = None):
log.debug('Sending "%s" to rTorrent.', (data.get('name')))
if not self.connect():
return False
group_name = 'cp_' + data.get('provider').lower()
if not self._update_provider_group(group_name, data):
return False
torrent_params = {}
if self.conf('label'):
torrent_params['label'] = self.conf('label')
if not filedata and data.get('protocol') == 'torrent':
log.error('Failed sending torrent, no data')
return False
# Try download magnet torrents
if data.get('protocol') == 'torrent_magnet':
filedata = self.magnetToTorrent(data.get('url'))
if filedata is False:
return False
data['protocol'] = 'torrent'
info = bdecode(filedata)["info"]
torrent_hash = sha1(bencode(info)).hexdigest().upper()
# Convert base 32 to hex
if len(torrent_hash) == 32:
torrent_hash = b16encode(b32decode(torrent_hash))
# Send request to rTorrent
try:
# Send torrent to rTorrent
torrent = self.rt.load_torrent(filedata)
# Set label
if self.conf('label'):
torrent.set_custom(1, self.conf('label'))
# Set Ratio Group
torrent.set_visible(group_name)
# Start torrent
if not self.conf('paused', default = 0):
torrent.start()
return self.downloadReturnId(torrent_hash)
except Exception, err:
log.error('Failed to send torrent to rTorrent: %s', err)
return False
def getAllDownloadStatus(self):
log.debug('Checking rTorrent download status.')
if not self.connect():
return False
try:
torrents = self.rt.get_torrents()
statuses = StatusList(self)
for item in torrents:
status = 'busy'
if item.complete:
if item.active:
status = 'seeding'
else:
status = 'completed'
statuses.append({
'id': item.info_hash,
'name': item.name,
'status': status,
'seed_ratio': item.ratio,
'original_status': item.state,
'timeleft': str(timedelta(seconds = float(item.left_bytes) / item.down_rate)) if item.down_rate > 0 else -1,
'folder': ss(item.directory)
})
return statuses
except Exception, err:
log.error('Failed to get status from rTorrent: %s', err)
return False
def pause(self, download_info, pause = True):
if not self.connect():
return False
torrent = self.rt.find_torrent(download_info['id'])
if torrent is None:
return False
if pause:
return torrent.pause()
return torrent.resume()
def removeFailed(self, item):
log.info('%s failed downloading, deleting...', item['name'])
return self.processComplete(item, delete_files = True)
def processComplete(self, item, delete_files):
log.debug('Requesting rTorrent to remove the torrent %s%s.',
(item['name'], ' and cleanup the downloaded files' if delete_files else ''))
if not self.connect():
return False
torrent = self.rt.find_torrent(item['id'])
if torrent is None:
return False
torrent.erase() # just removes the torrent, doesn't delete data
if delete_files:
shutil.rmtree(item['folder'], True)
return True
@@ -11,7 +11,7 @@ config = [{
'list': 'download_providers', 'list': 'download_providers',
'name': 'sabnzbd', 'name': 'sabnzbd',
'label': 'Sabnzbd', 'label': 'Sabnzbd',
'description': 'Use <a href="http://sabnzbd.org/" target="_blank">SABnzbd</a> to download NZBs.', 'description': 'Use <a href="http://sabnzbd.org/" target="_blank">SABnzbd</a> (0.7+) to download NZBs.',
'wizard': True, 'wizard': True,
'options': [ 'options': [
{ {
@@ -34,6 +34,15 @@ config = [{
'label': 'Category', 'label': 'Category',
'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>', 'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
}, },
{
'name': 'priority',
'label': 'Priority',
'type': 'dropdown',
'default': '0',
'advanced': True,
'values': [('Paused', -2), ('Low', -1), ('Normal', 0), ('High', 1), ('Forced', 2)],
'description': 'Add to the queue with this priority.',
},
{ {
'name': 'manual', 'name': 'manual',
'default': False, 'default': False,
@@ -41,9 +50,18 @@ config = [{
'advanced': True, 'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
}, },
{
'name': 'remove_complete',
'advanced': True,
'label': 'Remove NZB',
'default': False,
'type': 'bool',
'description': 'Remove the NZB from history after it completed.',
},
{ {
'name': 'delete_failed', 'name': 'delete_failed',
'default': True, 'default': True,
'advanced': True,
'type': 'bool', 'type': 'bool',
'description': 'Delete a release after the download has failed.', 'description': 'Delete a release after the download has failed.',
}, },
+26 -5
View File
@@ -10,11 +10,14 @@ import traceback
log = CPLog(__name__) log = CPLog(__name__)
class Sabnzbd(Downloader): class Sabnzbd(Downloader):
type = ['nzb'] protocol = ['nzb']
def download(self, data = {}, movie = {}, filedata = None): def download(self, data = None, movie = None, filedata = None):
if not movie: movie = {}
if not data: data = {}
log.info('Sending "%s" to SABnzbd.', data.get('name')) log.info('Sending "%s" to SABnzbd.', data.get('name'))
@@ -22,11 +25,13 @@ class Sabnzbd(Downloader):
'cat': self.conf('category'), 'cat': self.conf('category'),
'mode': 'addurl', 'mode': 'addurl',
'nzbname': self.createNzbName(data, movie), 'nzbname': self.createNzbName(data, movie),
'priority': self.conf('priority'),
} }
nzb_filename = None
if filedata: if filedata:
if len(filedata) < 50: if len(filedata) < 50:
log.error('No proper nzb available: %s', (filedata)) log.error('No proper nzb available: %s', filedata)
return False return False
# If it's a .rar, it adds the .rar extension, otherwise it stays .nzb # If it's a .rar, it adds the .rar extension, otherwise it stays .nzb
@@ -36,7 +41,7 @@ class Sabnzbd(Downloader):
req_params['name'] = data.get('url') req_params['name'] = data.get('url')
try: try:
if req_params.get('mode') is 'addfile': if nzb_filename and req_params.get('mode') is 'addfile':
sab_data = self.call(req_params, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True) sab_data = self.call(req_params, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True)
else: else:
sab_data = self.call(req_params) sab_data = self.call(req_params)
@@ -107,7 +112,7 @@ class Sabnzbd(Downloader):
'status': status, 'status': status,
'original_status': item['status'], 'original_status': item['status'],
'timeleft': str(timedelta(seconds = 0)), 'timeleft': str(timedelta(seconds = 0)),
'folder': item['storage'], 'folder': ss(item['storage']),
}) })
return statuses return statuses
@@ -129,6 +134,22 @@ class Sabnzbd(Downloader):
return True return True
def processComplete(self, item, delete_files = False):
log.debug('Requesting SabNZBd to remove the NZB %s.', item['name'])
try:
self.call({
'mode': 'history',
'name': 'delete',
'del_files': '0',
'value': item['id']
}, use_json = False)
except:
log.error('Failed removing: %s', traceback.format_exc(0))
return False
return True
def call(self, request_params, use_json = True, **kwargs): def call(self, request_params, use_json = True, **kwargs):
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(request_params, { url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(request_params, {
@@ -18,7 +18,7 @@ config = [{
'name': 'enabled', 'name': 'enabled',
'default': 0, 'default': 0,
'type': 'enabler', 'type': 'enabler',
'radio_group': 'torrent', 'radio_group': 'nzb,torrent',
}, },
{ {
'name': 'host', 'name': 'host',
@@ -32,6 +32,13 @@ config = [{
'name': 'password', 'name': 'password',
'type': 'password', 'type': 'password',
}, },
{
'name': 'use_for',
'label': 'Use for',
'default': 'both',
'type': 'dropdown',
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
},
{ {
'name': 'manual', 'name': 'manual',
'default': 0, 'default': 0,
+87 -46
View File
@@ -1,22 +1,23 @@
from couchpotato.core.downloaders.base import Downloader from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import isInt from couchpotato.core.helpers.encoding import isInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
import httplib
import json import json
import urllib import requests
import urllib2
log = CPLog(__name__) log = CPLog(__name__)
class Synology(Downloader): class Synology(Downloader):
type = ['torrent_magnet'] protocol = ['nzb', 'torrent', 'torrent_magnet']
log = CPLog(__name__) log = CPLog(__name__)
def download(self, data, movie, filedata = None): def download(self, data = None, movie = None, filedata = None):
if not movie: movie = {}
if not data: data = {}
log.error('Sending "%s" (%s) to Synology.', (data.get('name'), data.get('type'))) response = False
log.error('Sending "%s" (%s) to Synology.', (data['name'], data['protocol']))
# Load host from config and split out port. # Load host from config and split out port.
host = self.conf('host').split(':') host = self.conf('host').split(':')
@@ -24,24 +25,47 @@ class Synology(Downloader):
log.error('Config properties are not filled in correctly, port is missing.') log.error('Config properties are not filled in correctly, port is missing.')
return False return False
if data.get('type') == 'torrent':
log.error('Can\'t add binary torrent file')
return False
try: try:
# Send request to Transmission # Send request to Synology
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password')) srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
remote_torrent = srpc.add_torrent_uri(data.get('url')) if data['protocol'] == 'torrent_magnet':
log.info('Response: %s', remote_torrent) log.info('Adding torrent URL %s', data['url'])
return remote_torrent['success'] response = srpc.create_task(url = data['url'])
elif data['protocol'] in ['nzb', 'torrent']:
log.info('Adding %s' % data['protocol'])
if not filedata:
log.error('No %s data found' % data['protocol'])
else:
filename = data['name'] + '.' + data['protocol']
response = srpc.create_task(filename = filename, filedata = filedata)
except Exception, err: except Exception, err:
log.error('Exception while adding torrent: %s', err) log.error('Exception while adding torrent: %s', err)
return False finally:
return response
def getEnabledProtocol(self):
if self.conf('use_for') == 'both':
return super(Synology, self).getEnabledProtocol()
elif self.conf('use_for') == 'torrent':
return ['torrent', 'torrent_magnet']
else:
return ['nzb']
def isEnabled(self, manual = False, data = None):
if not data: data = {}
for_protocol = ['both']
if data and 'torrent' in data.get('protocol'):
for_protocol.append('torrent')
elif data:
for_protocol.append(data.get('protocol'))
return super(Synology, self).isEnabled(manual, data) and\
((self.conf('use_for') in for_protocol))
class SynologyRPC(object): class SynologyRPC(object):
'''SynologyRPC lite library''' """SynologyRPC lite library"""
def __init__(self, host = 'localhost', port = 5000, username = None, password = None): def __init__(self, host = 'localhost', port = 5000, username = None, password = None):
@@ -58,11 +82,13 @@ class SynologyRPC(object):
args = {'api': 'SYNO.API.Auth', 'account': self.username, 'passwd': self.password, 'version': 2, args = {'api': 'SYNO.API.Auth', 'account': self.username, 'passwd': self.password, 'version': 2,
'method': 'login', 'session': self.session_name, 'format': 'sid'} 'method': 'login', 'session': self.session_name, 'format': 'sid'}
response = self._req(self.auth_url, args) response = self._req(self.auth_url, args)
if response['success'] == True: if response['success']:
self.sid = response['data']['sid'] self.sid = response['data']['sid']
log.debug('Sid=%s', self.sid) log.debug('sid=%s', self.sid)
return response else:
elif self.username or self.password: log.error('Couldn\'t login to Synology, %s', response)
return response['success']
else:
log.error('User or password missing, not using authentication.') log.error('User or password missing, not using authentication.')
return False return False
@@ -70,36 +96,51 @@ class SynologyRPC(object):
args = {'api':'SYNO.API.Auth', 'version':1, 'method':'logout', 'session':self.session_name, '_sid':self.sid} args = {'api':'SYNO.API.Auth', 'version':1, 'method':'logout', 'session':self.session_name, '_sid':self.sid}
return self._req(self.auth_url, args) return self._req(self.auth_url, args)
def _req(self, url, args): def _req(self, url, args, files = None):
req_url = url + '?' + urllib.urlencode(args) response = {'success': False}
try: try:
req_open = urllib2.urlopen(req_url) req = requests.post(url, data = args, files = files)
response = json.loads(req_open.read()) req.raise_for_status()
if response['success'] == True: response = json.loads(req.text)
if response['success']:
log.info('Synology action successfull') log.info('Synology action successfull')
return response return response
except httplib.InvalidURL, err: except requests.ConnectionError, err:
log.error('Invalid Transmission host, check your config %s', err) log.error('Synology connection error, check your config %s', err)
return False except requests.HTTPError, err:
except urllib2.HTTPError, err:
log.error('SynologyRPC HTTPError: %s', err) log.error('SynologyRPC HTTPError: %s', err)
return False except Exception, err:
except urllib2.URLError, err: log.error('Exception: %s', err)
log.error('Unable to connect to Synology %s', err) finally:
return False return response
def add_torrent_uri(self, torrent): def create_task(self, url = None, filename = None, filedata = None):
log.info('Adding torrent URL %s', torrent) """ Creates new download task in Synology DownloadStation. Either specify
response = {} url or pair (filename, filedata).
Returns True if task was created, False otherwise
"""
result = False
# login # login
login = self._login() if self._login():
if len(login) > 0 and login['success'] == True: args = {'api': 'SYNO.DownloadStation.Task',
log.info('Login success, adding torrent') 'version': '1',
args = {'api':'SYNO.DownloadStation.Task', 'version':1, 'method':'create', 'uri':torrent, '_sid':self.sid} 'method': 'create',
response = self._req(self.download_url, args) '_sid': self.sid}
if url:
log.info('Login success, adding torrent URI')
args['uri'] = url
response = self._req(self.download_url, args = args)
log.info('Response: %s', response)
result = response['success']
elif filename and filedata:
log.info('Login success, adding torrent')
files = {'file': (filename, filedata)}
response = self._req(self.download_url, args = args, files = files)
log.info('Response: %s', response)
result = response['success']
else:
log.error('Invalid use of SynologyRPC.create_task: either url or filename+filedata must be specified')
self._logout() self._logout()
else:
log.error('Couldn\'t login to Synology, %s', login)
return response
return result
@@ -25,6 +25,13 @@ config = [{
'default': 'localhost:9091', 'default': 'localhost:9091',
'description': 'Hostname with port. Usually <strong>localhost:9091</strong>', 'description': 'Hostname with port. Usually <strong>localhost:9091</strong>',
}, },
{
'name': 'rpc_url',
'type': 'string',
'default': 'transmission',
'advanced': True,
'description': 'Change if you don\'t run Transmission RPC at the default url.',
},
{ {
'name': 'username', 'name': 'username',
}, },
@@ -32,30 +39,33 @@ config = [{
'name': 'password', 'name': 'password',
'type': 'password', 'type': 'password',
}, },
{
'name': 'paused',
'type': 'bool',
'default': False,
'description': 'Add the torrent paused.',
},
{ {
'name': 'directory', 'name': 'directory',
'type': 'directory', 'type': 'directory',
'description': 'Download to this directory. Keep empty for default Transmission download directory.', 'description': 'Download to this directory. Keep empty for default Transmission download directory.',
}, },
{ {
'name': 'ratio', 'name': 'remove_complete',
'default': 10, 'label': 'Remove torrent',
'type': 'float', 'default': True,
'advanced': True, 'advanced': True,
'description': 'Stop transfer when reaching ratio', 'type': 'bool',
'description': 'Remove the torrent from Transmission after it finished seeding.',
}, },
{ {
'name': 'ratiomode', 'name': 'delete_files',
'default': 0, 'label': 'Remove files',
'type': 'int', 'default': True,
'type': 'bool',
'advanced': True, 'advanced': True,
'description': '0 = Use session limit, 1 = Use transfer limit, 2 = Disable limit.', 'description': 'Also remove the leftover files.',
},
{
'name': 'paused',
'type': 'bool',
'advanced': True,
'default': False,
'description': 'Add the torrent paused.',
}, },
{ {
'name': 'manual', 'name': 'manual',
@@ -64,6 +74,20 @@ config = [{
'advanced': True, 'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
}, },
{
'name': 'stalled_as_failed',
'default': True,
'advanced': True,
'type': 'bool',
'description': 'Consider a stalled torrent as failed',
},
{
'name': 'delete_failed',
'default': True,
'advanced': True,
'type': 'bool',
'description': 'Delete a release after the download has failed.',
},
], ],
} }
], ],
+100 -113
View File
@@ -1,6 +1,7 @@
from base64 import b64encode from base64 import b64encode
from couchpotato.core.downloaders.base import Downloader, StatusList from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import isInt from couchpotato.core.helpers.encoding import isInt, ss
from couchpotato.core.helpers.variable import tryInt, tryFloat
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.environment import Env from couchpotato.environment import Env
from datetime import timedelta from datetime import timedelta
@@ -8,8 +9,6 @@ import httplib
import json import json
import os.path import os.path
import re import re
import shutil
import traceback
import urllib2 import urllib2
log = CPLog(__name__) log = CPLog(__name__)
@@ -17,154 +16,140 @@ log = CPLog(__name__)
class Transmission(Downloader): class Transmission(Downloader):
type = ['torrent', 'torrent_magnet'] protocol = ['torrent', 'torrent_magnet']
log = CPLog(__name__) log = CPLog(__name__)
trpc = None
def download(self, data, movie, filedata = None): def connect(self):
log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type')))
# Load host from config and split out port. # Load host from config and split out port.
host = self.conf('host').split(':') host = self.conf('host').split(':')
if not isInt(host[1]): if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.') log.error('Config properties are not filled in correctly, port is missing.')
return False return False
# Set parameters for Transmission if not self.trpc:
params = { self.trpc = TransmissionRPC(host[0], port = host[1], rpc_url = self.conf('rpc_url'), username = self.conf('username'), password = self.conf('password'))
'paused': self.conf('paused', default = 0),
}
if len(self.conf('directory', default = '')) > 0: return self.trpc
folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1]
folder_path = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep)
# Create the empty folder to download too def download(self, data, movie, filedata = None):
self.makeDir(folder_path)
params['download-dir'] = folder_path log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('protocol')))
torrent_params = {} if not self.connect():
if self.conf('ratio'): return False
torrent_params = {
'seedRatioLimit': self.conf('ratio'),
'seedRatioMode': self.conf('ratiomode')
}
if not filedata and data.get('type') == 'torrent': if not filedata and data.get('protocol') == 'torrent':
log.error('Failed sending torrent, no data') log.error('Failed sending torrent, no data')
return False return False
# Send request to Transmission # Set parameters for adding torrent
try: params = {
trpc = TransmissionRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password')) 'paused': self.conf('paused', default = False)
if data.get('type') == 'torrent_magnet': }
remote_torrent = trpc.add_torrent_uri(data.get('url'), arguments = params)
torrent_params['trackerAdd'] = self.torrent_trackers if self.conf('directory'):
if os.path.isdir(self.conf('directory')):
params['download-dir'] = self.conf('directory')
else: else:
remote_torrent = trpc.add_torrent_file(b64encode(filedata), arguments = params) log.error('Download directory from Transmission settings: %s doesn\'t exist', self.conf('directory'))
if not remote_torrent: # Change parameters of torrent
return False torrent_params = {}
if data.get('seed_ratio'):
torrent_params['seedRatioLimit'] = tryFloat(data.get('seed_ratio'))
torrent_params['seedRatioMode'] = 1
# Change settings of added torrents if data.get('seed_time'):
elif torrent_params: torrent_params['seedIdleLimit'] = tryInt(data.get('seed_time')) * 60
trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params) torrent_params['seedIdleMode'] = 1
log.info('Torrent sent to Transmission successfully.') # Send request to Transmission
return self.downloadReturnId(remote_torrent['torrent-added']['hashString']) if data.get('protocol') == 'torrent_magnet':
except: remote_torrent = self.trpc.add_torrent_uri(data.get('url'), arguments = params)
log.error('Failed to change settings for transfer: %s', traceback.format_exc()) torrent_params['trackerAdd'] = self.torrent_trackers
else:
remote_torrent = self.trpc.add_torrent_file(b64encode(filedata), arguments = params)
if not remote_torrent:
log.error('Failed sending torrent to Transmission')
return False return False
# Change settings of added torrents
if torrent_params:
self.trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
log.info('Torrent sent to Transmission successfully.')
return self.downloadReturnId(remote_torrent['torrent-added']['hashString'])
def getAllDownloadStatus(self): def getAllDownloadStatus(self):
log.debug('Checking Transmission download status.') log.debug('Checking Transmission download status.')
# Load host from config and split out port. if not self.connect():
host = self.conf('host').split(':')
if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.')
return False
# Go through Queue
try:
trpc = TransmissionRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
return_params = {
'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isFinished', 'downloadDir', 'uploadRatio']
}
queue = trpc.get_alltorrents(return_params)
except Exception, err:
log.error('Failed getting queue: %s', err)
return False return False
statuses = StatusList(self) statuses = StatusList(self)
# Get torrents status return_params = {
# CouchPotato Status 'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isStalled', 'isFinished', 'downloadDir', 'uploadRatio', 'secondsSeeding', 'seedIdleLimit']
#status = 'busy' }
#status = 'failed'
#status = 'completed' queue = self.trpc.get_alltorrents(return_params)
# Transmission Status if not (queue and queue.get('torrents')):
#status = 0 => "Torrent is stopped" log.debug('Nothing in queue or error')
#status = 1 => "Queued to check files" return False
#status = 2 => "Checking files"
#status = 3 => "Queued to download"
#status = 4 => "Downloading"
#status = 4 => "Queued to seed"
#status = 6 => "Seeding"
#To do :
# add checking file
# manage no peer in a range time => fail
for item in queue['torrents']: for item in queue['torrents']:
log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / confRatio=%s / isFinished=%s', (item['name'], item['id'], item['downloadDir'], item['hashString'], item['percentDone'], item['status'], item['eta'], item['uploadRatio'], self.conf('ratio'), item['isFinished'])) log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / isFinished=%s',
(item['name'], item['id'], item['downloadDir'], item['hashString'], item['percentDone'], item['status'], item['eta'], item['uploadRatio'], item['isFinished']))
if not os.path.isdir(Env.setting('from', 'renamer')): if not os.path.isdir(Env.setting('from', 'renamer')):
log.error('Renamer "from" folder doesn\'t to exist.') log.error('Renamer "from" folder doesn\'t to exist.')
return return
if (item['percentDone'] * 100) >= 100 and (item['status'] == 6 or item['status'] == 0) and item['uploadRatio'] > self.conf('ratio'): status = 'busy'
try: if item['isStalled'] and self.conf('stalled_as_failed'):
trpc.stop_torrent(item['hashString'], {}) status = 'failed'
statuses.append({ elif item['status'] == 0 and item['percentDone'] == 1:
'id': item['hashString'], status = 'completed'
'name': item['name'], elif item['status'] in [5, 6]:
'status': 'completed', status = 'seeding'
'original_status': item['status'],
'timeleft': str(timedelta(seconds = 0)), statuses.append({
'folder': os.path.join(item['downloadDir'], item['name']), 'id': item['hashString'],
}) 'name': item['name'],
except Exception, err: 'status': status,
log.error('Failed to stop and remove torrent "%s" with error: %s', (item['name'], err)) 'original_status': item['status'],
statuses.append({ 'seed_ratio': item['uploadRatio'],
'id': item['hashString'], 'timeleft': str(timedelta(seconds = item['eta'])),
'name': item['name'], 'folder': ss(os.path.join(item['downloadDir'], item['name'])),
'status': 'failed', })
'original_status': item['status'],
'timeleft': str(timedelta(seconds = 0)),
})
else:
statuses.append({
'id': item['hashString'],
'name': item['name'],
'status': 'busy',
'original_status': item['status'],
'timeleft': str(timedelta(seconds = item['eta'])), # Is ETA in seconds??
})
return statuses return statuses
def pause(self, item, pause = True):
if pause:
return self.trpc.stop_torrent(item['id'])
else:
return self.trpc.start_torrent(item['id'])
def removeFailed(self, item):
log.info('%s failed downloading, deleting...', item['name'])
return self.trpc.remove_torrent(item['hashString'], True)
def processComplete(self, item, delete_files = False):
log.debug('Requesting Transmission to remove the torrent %s%s.', (item['name'], ' and cleanup the downloaded files' if delete_files else ''))
return self.trpc.remove_torrent(item['hashString'], delete_files)
class TransmissionRPC(object): class TransmissionRPC(object):
"""TransmissionRPC lite library""" """TransmissionRPC lite library"""
def __init__(self, host = 'localhost', port = 9091, rpc_url = 'transmission', username = None, password = None):
def __init__(self, host = 'localhost', port = 9091, username = None, password = None):
super(TransmissionRPC, self).__init__() super(TransmissionRPC, self).__init__()
self.url = 'http://' + host + ':' + str(port) + '/transmission/rpc' self.url = 'http://' + host + ':' + str(port) + '/' + rpc_url + '/rpc'
self.tag = 0 self.tag = 0
self.session_id = 0 self.session_id = 0
self.session = {} self.session = {}
@@ -188,7 +173,7 @@ class TransmissionRPC(object):
log.debug('request: %s', json.dumps(ojson)) log.debug('request: %s', json.dumps(ojson))
log.debug('response: %s', json.dumps(response)) log.debug('response: %s', json.dumps(response))
if response['result'] == 'success': if response['result'] == 'success':
log.debug('Transmission action successfull') log.debug('Transmission action successful')
return response['arguments'] return response['arguments']
else: else:
log.debug('Unknown failure sending command to Transmission. Return text is: %s', response['result']) log.debug('Unknown failure sending command to Transmission. Return text is: %s', response['result'])
@@ -240,13 +225,15 @@ class TransmissionRPC(object):
post_data = {'arguments': arguments, 'method': 'torrent-get', 'tag': self.tag} post_data = {'arguments': arguments, 'method': 'torrent-get', 'tag': self.tag}
return self._request(post_data) return self._request(post_data)
def stop_torrent(self, torrent_id, arguments): def stop_torrent(self, torrent_id):
arguments['ids'] = torrent_id post_data = {'arguments': {'ids': torrent_id}, 'method': 'torrent-stop', 'tag': self.tag}
post_data = {'arguments': arguments, 'method': 'torrent-stop', 'tag': self.tag}
return self._request(post_data) return self._request(post_data)
def remove_torrent(self, torrent_id, remove_local_data, arguments): def start_torrent(self, torrent_id):
arguments['ids'] = torrent_id post_data = {'arguments': {'ids': torrent_id}, 'method': 'torrent-start', 'tag': self.tag}
arguments['delete-local-data'] = remove_local_data
post_data = {'arguments': arguments, 'method': 'torrent-remove', 'tag': self.tag}
return self._request(post_data) return self._request(post_data)
def remove_torrent(self, torrent_id, delete_local_data):
post_data = {'arguments': {'ids': torrent_id, 'delete-local-data': delete_local_data}, 'method': 'torrent-remove', 'tag': self.tag}
return self._request(post_data)
@@ -11,7 +11,7 @@ config = [{
'list': 'download_providers', 'list': 'download_providers',
'name': 'utorrent', 'name': 'utorrent',
'label': 'uTorrent', 'label': 'uTorrent',
'description': 'Use <a href="http://www.utorrent.com/" target="_blank">uTorrent</a> to download torrents.', 'description': 'Use <a href="http://www.utorrent.com/" target="_blank">uTorrent</a> (3.0+) to download torrents.',
'wizard': True, 'wizard': True,
'options': [ 'options': [
{ {
@@ -36,9 +36,26 @@ config = [{
'name': 'label', 'name': 'label',
'description': 'Label to add torrent as.', 'description': 'Label to add torrent as.',
}, },
{
'name': 'remove_complete',
'label': 'Remove torrent',
'default': True,
'advanced': True,
'type': 'bool',
'description': 'Remove the torrent from uTorrent after it finished seeding.',
},
{
'name': 'delete_files',
'label': 'Remove files',
'default': True,
'type': 'bool',
'advanced': True,
'description': 'Also remove the leftover files.',
},
{ {
'name': 'paused', 'name': 'paused',
'type': 'bool', 'type': 'bool',
'advanced': True,
'default': False, 'default': False,
'description': 'Add the torrent paused.', 'description': 'Add the torrent paused.',
}, },
@@ -49,6 +66,13 @@ config = [{
'advanced': True, 'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
}, },
{
'name': 'delete_failed',
'default': True,
'advanced': True,
'type': 'bool',
'description': 'Delete a release after the download has failed.',
},
], ],
} }
], ],
+184 -88
View File
@@ -1,166 +1,212 @@
from base64 import b16encode, b32decode from base64 import b16encode, b32decode
from bencode import bencode, bdecode from bencode import bencode as benc, bdecode
from couchpotato.core.downloaders.base import Downloader, StatusList from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import isInt, ss from couchpotato.core.helpers.encoding import isInt, ss
from couchpotato.core.helpers.variable import tryInt, tryFloat
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from datetime import timedelta
from hashlib import sha1 from hashlib import sha1
from multipartpost import MultipartPostHandler from multipartpost import MultipartPostHandler
from datetime import timedelta
import os
import cookielib import cookielib
import httplib import httplib
import json import json
import os
import re import re
import stat
import time import time
import urllib import urllib
import urllib2 import urllib2
log = CPLog(__name__) log = CPLog(__name__)
class uTorrent(Downloader): class uTorrent(Downloader):
type = ['torrent', 'torrent_magnet'] protocol = ['torrent', 'torrent_magnet']
utorrent_api = None utorrent_api = None
def download(self, data, movie, filedata = None): def connect(self):
log.debug('Sending "%s" (%s) to uTorrent.', (data.get('name'), data.get('type')))
# Load host from config and split out port. # Load host from config and split out port.
host = self.conf('host').split(':') host = self.conf('host').split(':')
if not isInt(host[1]): if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.') log.error('Config properties are not filled in correctly, port is missing.')
return False return False
self.utorrent_api = uTorrentAPI(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
return self.utorrent_api
def download(self, data = None, movie = None, filedata = None):
if not movie: movie = {}
if not data: data = {}
log.debug('Sending "%s" (%s) to uTorrent.', (data.get('name'), data.get('protocol')))
if not self.connect():
return False
settings = self.utorrent_api.get_settings()
if not settings:
return False
#Fix settings in case they are not set for CPS compatibility
new_settings = {}
if not (settings.get('seed_prio_limitul') == 0 and settings['seed_prio_limitul_flag']):
new_settings['seed_prio_limitul'] = 0
new_settings['seed_prio_limitul_flag'] = True
log.info('Updated uTorrent settings to set a torrent to complete after it the seeding requirements are met.')
if settings.get('bt.read_only_on_complete'): #This doesn't work as this option seems to be not available through the api. Mitigated with removeReadOnly function
new_settings['bt.read_only_on_complete'] = False
log.info('Updated uTorrent settings to not set the files to read only after completing.')
if new_settings:
self.utorrent_api.set_settings(new_settings)
torrent_params = {} torrent_params = {}
if self.conf('label'): if self.conf('label'):
torrent_params['label'] = self.conf('label') torrent_params['label'] = self.conf('label')
if not filedata and data.get('type') == 'torrent': if not filedata and data.get('protocol') == 'torrent':
log.error('Failed sending torrent, no data') log.error('Failed sending torrent, no data')
return False return False
if data.get('type') == 'torrent_magnet': if data.get('protocol') == 'torrent_magnet':
torrent_hash = re.findall('urn:btih:([\w]{32,40})', data.get('url'))[0].upper() torrent_hash = re.findall('urn:btih:([\w]{32,40})', data.get('url'))[0].upper()
torrent_params['trackers'] = '%0D%0A%0D%0A'.join(self.torrent_trackers) torrent_params['trackers'] = '%0D%0A%0D%0A'.join(self.torrent_trackers)
else: else:
info = bdecode(filedata)["info"] info = bdecode(filedata)["info"]
torrent_hash = sha1(bencode(info)).hexdigest().upper() torrent_hash = sha1(benc(info)).hexdigest().upper()
torrent_filename = self.createFileName(data, filedata, movie) torrent_filename = self.createFileName(data, filedata, movie)
if data.get('seed_ratio'):
torrent_params['seed_override'] = 1
torrent_params['seed_ratio'] = tryInt(tryFloat(data['seed_ratio']) * 1000)
if data.get('seed_time'):
torrent_params['seed_override'] = 1
torrent_params['seed_time'] = tryInt(data['seed_time']) * 3600
# Convert base 32 to hex # Convert base 32 to hex
if len(torrent_hash) == 32: if len(torrent_hash) == 32:
torrent_hash = b16encode(b32decode(torrent_hash)) torrent_hash = b16encode(b32decode(torrent_hash))
# Send request to uTorrent # Send request to uTorrent
try: if data.get('protocol') == 'torrent_magnet':
if not self.utorrent_api: self.utorrent_api.add_torrent_uri(torrent_filename, data.get('url'))
self.utorrent_api = uTorrentAPI(host[0], port = host[1], username = self.conf('username'), password = self.conf('password')) else:
self.utorrent_api.add_torrent_file(torrent_filename, filedata)
if data.get('type') == 'torrent_magnet': # Change settings of added torrent
self.utorrent_api.add_torrent_uri(data.get('url')) self.utorrent_api.set_torrent(torrent_hash, torrent_params)
if self.conf('paused', default = 0):
self.utorrent_api.pause_torrent(torrent_hash)
count = 0
while True:
count += 1
# Check if torrent is saved in subfolder of torrent name
data = self.utorrent_api.get_files(torrent_hash)
torrent_files = json.loads(data)
if torrent_files.get('error'):
log.error('Error getting data from uTorrent: %s', torrent_files.get('error'))
return False
if (torrent_files.get('files') and len(torrent_files['files'][1]) > 0) or count > 60:
break
time.sleep(1)
# Torrent has only one file, so uTorrent wont create a folder for it
if len(torrent_files['files'][1]) == 1:
# Remove torrent and try again
self.utorrent_api.remove_torrent(torrent_hash, remove_data = True)
# Send request to uTorrent
if data.get('protocol') == 'torrent_magnet':
self.utorrent_api.add_torrent_uri(torrent_filename, data.get('url'), add_folder = True)
else: else:
self.utorrent_api.add_torrent_file(torrent_filename, filedata) self.utorrent_api.add_torrent_file(torrent_filename, filedata, add_folder = True)
# Change settings of added torrents # Change settings of added torrent
self.utorrent_api.set_torrent(torrent_hash, torrent_params) self.utorrent_api.set_torrent(torrent_hash, torrent_params)
if self.conf('paused', default = 0): if self.conf('paused', default = 0):
self.utorrent_api.pause_torrent(torrent_hash) self.utorrent_api.pause_torrent(torrent_hash)
return self.downloadReturnId(torrent_hash)
except Exception, err: return self.downloadReturnId(torrent_hash)
log.error('Failed to send torrent to uTorrent: %s', err)
return False
def getAllDownloadStatus(self): def getAllDownloadStatus(self):
log.debug('Checking uTorrent download status.') log.debug('Checking uTorrent download status.')
# Load host from config and split out port. if not self.connect():
host = self.conf('host').split(':')
if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.')
return False
try:
self.utorrent_api = uTorrentAPI(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
except Exception, err:
log.error('Failed to get uTorrent object: %s', err)
return False
data = ''
try:
data = self.utorrent_api.get_status()
queue = json.loads(data)
if queue.get('error'):
log.error('Error getting data from uTorrent: %s', queue.get('error'))
return False
except Exception, err:
log.error('Failed to get status from uTorrent: %s', err)
return False
if queue.get('torrents', []) == []:
log.debug('Nothing in queue')
return False return False
statuses = StatusList(self) statuses = StatusList(self)
download_folder = ''
settings_dict = {}
try: data = self.utorrent_api.get_status()
data = self.utorrent_api.get_settings() if not data:
utorrent_settings = json.loads(data) log.error('Error getting data from uTorrent')
return False
# Create settings dict queue = json.loads(data)
for item in utorrent_settings['settings']: if queue.get('error'):
if item[1] == 0: # int log.error('Error getting data from uTorrent: %s', queue.get('error'))
settings_dict[item[0]] = int(item[2] if not item[2].strip() == '' else '0') return False
elif item[1] == 1: # bool
settings_dict[item[0]] = True if item[2] == 'true' else False
elif item[1] == 2: # string
settings_dict[item[0]] = item[2]
log.debug('uTorrent settings: %s', settings_dict)
# Get the download path from the uTorrent settings if not queue.get('torrents'):
if settings_dict['dir_completed_download_flag']: log.debug('Nothing in queue')
download_folder = settings_dict['dir_completed_download'] return False
elif settings_dict['dir_active_download_flag']:
download_folder = settings_dict['dir_active_download']
else:
log.info('No download folder set in uTorrent. Please set a download folder')
return False
except Exception, err:
log.error('Failed to get settings from uTorrent: %s', err)
return False
# Get torrents # Get torrents
for item in queue.get('torrents', []): for item in queue['torrents']:
# item[21] = Paused | Downloading | Seeding | Finished # item[21] = Paused | Downloading | Seeding | Finished
status = 'busy' status = 'busy'
if item[21] == 'Finished' or item[21] == 'Seeding': if 'Finished' in item[21]:
status = 'completed' status = 'completed'
self.removeReadOnly(item[26])
if settings_dict['dir_add_label']: elif 'Seeding' in item[21]:
release_folder = os.path.join(download_folder, item[11], item[2]) status = 'seeding'
else: self.removeReadOnly(item[26])
release_folder = os.path.join(download_folder, item[2])
statuses.append({ statuses.append({
'id': item[0], 'id': item[0],
'name': item[2], 'name': item[2],
'status': status, 'status': status,
'seed_ratio': float(item[7]) / 1000,
'original_status': item[1], 'original_status': item[1],
'timeleft': str(timedelta(seconds = item[10])), 'timeleft': str(timedelta(seconds = item[10])),
'folder': release_folder, 'folder': ss(item[26]),
}) })
return statuses return statuses
def pause(self, item, pause = True):
if not self.connect():
return False
return self.utorrent_api.pause_torrent(item['id'], pause)
def removeFailed(self, item):
log.info('%s failed downloading, deleting...', item['name'])
if not self.connect():
return False
return self.utorrent_api.remove_torrent(item['id'], remove_data = True)
def processComplete(self, item, delete_files = False):
log.debug('Requesting uTorrent to remove the torrent %s%s.', (item['name'], ' and cleanup the downloaded files' if delete_files else ''))
if not self.connect():
return False
return self.utorrent_api.remove_torrent(item['id'], remove_data = delete_files)
def removeReadOnly(self, folder):
#Removes all read-only flags in a folder
if folder and os.path.isdir(folder):
for root, folders, filenames in os.walk(folder):
for filename in filenames:
os.chmod(os.path.join(root, filename), stat.S_IWRITE)
class uTorrentAPI(object): class uTorrentAPI(object):
@@ -211,12 +257,16 @@ class uTorrentAPI(object):
token = re.findall("<div.*?>(.*?)</", request.read())[0] token = re.findall("<div.*?>(.*?)</", request.read())[0]
return token return token
def add_torrent_uri(self, torrent): def add_torrent_uri(self, filename, torrent, add_folder = False):
action = "action=add-url&s=%s" % urllib.quote(torrent) action = "action=add-url&s=%s" % urllib.quote(torrent)
if add_folder:
action += "&path=%s" % urllib.quote(filename)
return self._request(action) return self._request(action)
def add_torrent_file(self, filename, filedata): def add_torrent_file(self, filename, filedata, add_folder = False):
action = "action=add-file" action = "action=add-file"
if add_folder:
action += "&path=%s" % urllib.quote(filename)
return self._request(action, {"torrent_file": (ss(filename), filedata)}) return self._request(action, {"torrent_file": (ss(filename), filedata)})
def set_torrent(self, hash, params): def set_torrent(self, hash, params):
@@ -225,8 +275,22 @@ class uTorrentAPI(object):
action += "&s=%s&v=%s" % (k, v) action += "&s=%s&v=%s" % (k, v)
return self._request(action) return self._request(action)
def pause_torrent(self, hash): def pause_torrent(self, hash, pause = True):
action = "action=pause&hash=%s" % hash if pause:
action = "action=pause&hash=%s" % hash
else:
action = "action=unpause&hash=%s" % hash
return self._request(action)
def stop_torrent(self, hash):
action = "action=stop&hash=%s" % hash
return self._request(action)
def remove_torrent(self, hash, remove_data = False):
if remove_data:
action = "action=removedata&hash=%s" % hash
else:
action = "action=remove&hash=%s" % hash
return self._request(action) return self._request(action)
def get_status(self): def get_status(self):
@@ -235,4 +299,36 @@ class uTorrentAPI(object):
def get_settings(self): def get_settings(self):
action = "action=getsettings" action = "action=getsettings"
settings_dict = {}
try:
utorrent_settings = json.loads(self._request(action))
# Create settings dict
for item in utorrent_settings['settings']:
if item[1] == 0: # int
settings_dict[item[0]] = int(item[2] if not item[2].strip() == '' else '0')
elif item[1] == 1: # bool
settings_dict[item[0]] = True if item[2] == 'true' else False
elif item[1] == 2: # string
settings_dict[item[0]] = item[2]
#log.debug('uTorrent settings: %s', settings_dict)
except Exception, err:
log.error('Failed to get settings from uTorrent: %s', err)
return settings_dict
def set_settings(self, settings_dict = None):
if not settings_dict: settings_dict = {}
for key in settings_dict:
if isinstance(settings_dict[key], bool):
settings_dict[key] = 1 if settings_dict[key] else 0
action = 'action=setsetting' + ''.join(['&s=%s&v=%s' % (key, value) for (key, value) in settings_dict.items()])
return self._request(action)
def get_files(self, hash):
action = "action=getfiles&hash=%s" % hash
return self._request(action) return self._request(action)
+49 -22
View File
@@ -21,15 +21,25 @@ def addEvent(name, handler, priority = 100):
def createHandle(*args, **kwargs): def createHandle(*args, **kwargs):
h = None
try: try:
parent = handler.im_self # Open handler
bc = hasattr(parent, 'beforeCall') has_parent = hasattr(handler, 'im_self')
if bc: parent.beforeCall(handler) parent = None
if has_parent:
parent = handler.im_self
bc = hasattr(parent, 'beforeCall')
if bc: parent.beforeCall(handler)
# Main event
h = runHandler(name, handler, *args, **kwargs) h = runHandler(name, handler, *args, **kwargs)
ac = hasattr(parent, 'afterCall')
if ac: parent.afterCall(handler) # Close handler
if parent and has_parent:
ac = hasattr(parent, 'afterCall')
if ac: parent.afterCall(handler)
except: except:
h = runHandler(name, handler, *args, **kwargs) log.error('Failed creating handler %s %s: %s', (name, handler, traceback.format_exc()))
return h return h
@@ -43,12 +53,7 @@ def removeEvent(name, handler):
e -= handler e -= handler
def fireEvent(name, *args, **kwargs): def fireEvent(name, *args, **kwargs):
if not events.get(name): return if not events.has_key(name): return
e = Event(name = name, threads = 10, asynch = kwargs.get('async', False), exc_info = True, traceback = True, lock = threading.RLock())
for event in events[name]:
e.handle(event['handler'], priority = event['priority'])
#log.debug('Firing event %s', name) #log.debug('Firing event %s', name)
try: try:
@@ -59,7 +64,6 @@ def fireEvent(name, *args, **kwargs):
'single': False, # Return single handler 'single': False, # Return single handler
'merge': False, # Merge items 'merge': False, # Merge items
'in_order': False, # Fire them in specific order, waits for the other to finish 'in_order': False, # Fire them in specific order, waits for the other to finish
'async': False
} }
# Do options # Do options
@@ -70,12 +74,32 @@ def fireEvent(name, *args, **kwargs):
options[x] = val options[x] = val
except: pass except: pass
# Make sure only 1 event is fired at a time when order is wanted if len(events[name]) == 1:
kwargs['event_order_lock'] = threading.RLock() if options['in_order'] or options['single'] else None
kwargs['event_return_on_result'] = options['single']
# Fire single = None
result = e(*args, **kwargs) try:
single = events[name][0]['handler'](*args, **kwargs)
except:
log.error('Failed running single event: %s', traceback.format_exc())
# Don't load thread for single event
result = {
'single': (single is not None, single),
}
else:
e = Event(name = name, threads = 10, exc_info = True, traceback = True, lock = threading.RLock())
for event in events[name]:
e.handle(event['handler'], priority = event['priority'])
# Make sure only 1 event is fired at a time when order is wanted
kwargs['event_order_lock'] = threading.RLock() if options['in_order'] or options['single'] else None
kwargs['event_return_on_result'] = options['single']
# Fire
result = e(*args, **kwargs)
if options['single'] and not options['merge']: if options['single'] and not options['merge']:
results = None results = None
@@ -133,14 +157,17 @@ def fireEvent(name, *args, **kwargs):
options['on_complete']() options['on_complete']()
return results return results
except KeyError, e:
pass
except Exception: except Exception:
log.error('%s: %s', (name, traceback.format_exc())) log.error('%s: %s', (name, traceback.format_exc()))
def fireEventAsync(*args, **kwargs): def fireEventAsync(*args, **kwargs):
kwargs['async'] = True try:
fireEvent(*args, **kwargs) t = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
t.setDaemon(True)
t.start()
return True
except Exception, e:
log.error('%s: %s', (args[0], e))
def errorHandler(error): def errorHandler(error):
etype, value, tb = error etype, value, tb = error
+11 -4
View File
@@ -11,7 +11,8 @@ log = CPLog(__name__)
def toSafeString(original): def toSafeString(original):
valid_chars = "-_.() %s%s" % (ascii_letters, digits) valid_chars = "-_.() %s%s" % (ascii_letters, digits)
cleanedFilename = unicodedata.normalize('NFKD', toUnicode(original)).encode('ASCII', 'ignore') cleanedFilename = unicodedata.normalize('NFKD', toUnicode(original)).encode('ASCII', 'ignore')
return ''.join(c for c in cleanedFilename if c in valid_chars) valid_string = ''.join(c for c in cleanedFilename if c in valid_chars)
return ' '.join(valid_string.split())
def simplifyString(original): def simplifyString(original):
string = stripAccents(original.lower()) string = stripAccents(original.lower())
@@ -37,8 +38,14 @@ def toUnicode(original, *args):
return toUnicode(ascii_text) return toUnicode(ascii_text)
def ss(original, *args): def ss(original, *args):
from couchpotato.environment import Env
return toUnicode(original, *args).encode(Env.get('encoding')) u_original = toUnicode(original, *args)
try:
from couchpotato.environment import Env
return u_original.encode(Env.get('encoding'))
except Exception, e:
log.debug('Failed ss encoding char, force UTF8: %s', e)
return u_original.encode('UTF-8')
def ek(original, *args): def ek(original, *args):
if isinstance(original, (str, unicode)): if isinstance(original, (str, unicode)):
@@ -62,7 +69,7 @@ def stripAccents(s):
def tryUrlencode(s): def tryUrlencode(s):
new = u'' new = u''
if isinstance(s, (dict)): if isinstance(s, dict):
for key, value in s.iteritems(): for key, value in s.iteritems():
new += u'&%s=%s' % (key, tryUrlencode(value)) new += u'&%s=%s' % (key, tryUrlencode(value))
+5 -33
View File
@@ -1,18 +1,14 @@
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import natcmp from couchpotato.core.helpers.variable import natcmp
from flask.globals import current_app
from flask.helpers import json, make_response
from urllib import unquote from urllib import unquote
from werkzeug.urls import url_decode
import flask
import re import re
def getParams():
params = url_decode(getattr(flask.request, 'environ').get('QUERY_STRING', '')) def getParams(params):
reg = re.compile('^[a-z0-9_\.]+$') reg = re.compile('^[a-z0-9_\.]+$')
current = temp = {} temp = {}
for param, value in sorted(params.iteritems()): for param, value in sorted(params.iteritems()):
nest = re.split("([\[\]]+)", param) nest = re.split("([\[\]]+)", param)
@@ -36,6 +32,8 @@ def getParams():
current = current[item] current = current[item]
else: else:
temp[param] = toUnicode(unquote(value)) temp[param] = toUnicode(unquote(value))
if temp[param].lower() in ['true', 'false']:
temp[param] = temp[param].lower() != 'false'
return dictToList(temp) return dictToList(temp)
@@ -54,29 +52,3 @@ def dictToList(params):
new = params new = params
return new return new
def getParam(attr, default = None):
try:
return getParams().get(attr, default)
except:
return default
def padded_jsonify(callback, *args, **kwargs):
content = str(callback) + '(' + json.dumps(dict(*args, **kwargs)) + ')'
return getattr(current_app, 'response_class')(content, mimetype = 'text/javascript')
def jsonify(mimetype, *args, **kwargs):
content = json.dumps(dict(*args, **kwargs))
return getattr(current_app, 'response_class')(content, mimetype = mimetype)
def jsonified(*args, **kwargs):
callback = getParam('callback_func', None)
if callback:
content = padded_jsonify(callback, *args, **kwargs)
else:
content = jsonify('application/json', *args, **kwargs)
response = make_response(content)
response.cache_control.no_cache = True
return response
+3 -3
View File
@@ -6,7 +6,7 @@ log = CPLog(__name__)
class RSS(object): class RSS(object):
def getTextElements(self, xml, path): def getTextElements(self, xml, path):
''' Find elements and return tree''' """ Find elements and return tree"""
textelements = [] textelements = []
try: try:
@@ -28,7 +28,7 @@ class RSS(object):
return elements return elements
def getElement(self, xml, path): def getElement(self, xml, path):
''' Find element and return text''' """ Find element and return text"""
try: try:
return xml.find(path) return xml.find(path)
@@ -36,7 +36,7 @@ class RSS(object):
return return
def getTextElement(self, xml, path): def getTextElement(self, xml, path):
''' Find element and return text''' """ Find element and return text"""
try: try:
return xml.find(path).text return xml.find(path).text
+37 -15
View File
@@ -1,4 +1,4 @@
from couchpotato.core.helpers.encoding import simplifyString, toSafeString from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
import hashlib import hashlib
import os.path import os.path
@@ -101,11 +101,16 @@ def flattenList(l):
return l return l
def md5(text): def md5(text):
return hashlib.md5(text).hexdigest() return hashlib.md5(ss(text)).hexdigest()
def sha1(text): def sha1(text):
return hashlib.sha1(text).hexdigest() return hashlib.sha1(text).hexdigest()
def isLocalIP(ip):
ip = ip.lstrip('htps:/')
regex = '/(^127\.)|(^192\.168\.)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^::1)$/'
return re.search(regex, ip) is not None or 'localhost' in ip or ip[:4] == '127.'
def getExt(filename): def getExt(filename):
return os.path.splitext(filename)[1][1:] return os.path.splitext(filename)[1][1:]
@@ -113,12 +118,17 @@ def cleanHost(host):
if not host.startswith(('http://', 'https://')): if not host.startswith(('http://', 'https://')):
host = 'http://' + host host = 'http://' + host
if not host.endswith('/'): host = host.rstrip('/')
host += '/' host += '/'
return host return host
def getImdb(txt, check_inside = True, multiple = False): def getImdb(txt, check_inside = False, multiple = False):
if not check_inside:
txt = simplifyString(txt)
else:
txt = ss(txt)
if check_inside and os.path.isfile(txt): if check_inside and os.path.isfile(txt):
output = open(txt, 'r') output = open(txt, 'r')
@@ -128,7 +138,7 @@ def getImdb(txt, check_inside = True, multiple = False):
try: try:
ids = re.findall('(tt\d{7})', txt) ids = re.findall('(tt\d{7})', txt)
if multiple: if multiple:
return ids if len(ids) > 0 else [] return list(set(ids)) if len(ids) > 0 else []
return ids[0] return ids[0]
except IndexError: except IndexError:
pass pass
@@ -140,7 +150,11 @@ def tryInt(s):
except: return 0 except: return 0
def tryFloat(s): def tryFloat(s):
try: return float(s) if '.' in s else tryInt(s) try:
if isinstance(s, str):
return float(s) if '.' in s else tryInt(s)
else:
return float(s)
except: return 0 except: return 0
def natsortKey(s): def natsortKey(s):
@@ -159,8 +173,11 @@ def getTitle(library_dict):
if title.default: if title.default:
return title.title return title.title
except: except:
log.error('Could not get title for %s', library_dict.identifier) try:
return None return library_dict['info']['titles'][0]
except:
log.error('Could not get title for %s', library_dict.identifier)
return None
log.error('Could not get title for %s', library_dict['identifier']) log.error('Could not get title for %s', library_dict['identifier'])
return None return None
@@ -170,16 +187,21 @@ def getTitle(library_dict):
def possibleTitles(raw_title): def possibleTitles(raw_title):
titles = [] titles = [
toSafeString(raw_title).lower(),
raw_title.lower(),
simplifyString(raw_title)
]
titles.append(toSafeString(raw_title).lower()) # replace some chars
titles.append(raw_title.lower()) new_title = raw_title.replace('&', 'and')
titles.append(simplifyString(raw_title)) titles.append(simplifyString(new_title))
return list(set(titles)) return list(set(titles))
def randomString(size = 8, chars = string.ascii_uppercase + string.digits): def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
return ''.join(random.choice(chars) for x in range(size)) return ''.join(random.choice(chars) for x in range(size))
def splitString(str, split_on = ','): def splitString(str, split_on = ',', clean = True):
return [x.strip() for x in str.split(split_on)] if str else [] list = [x.strip() for x in str.split(split_on)] if str else []
return filter(None, list) if clean else list
+38 -21
View File
@@ -6,15 +6,24 @@ import traceback
log = CPLog(__name__) log = CPLog(__name__)
class Loader(object):
class Loader(object):
plugins = {} plugins = {}
providers = {} providers = {}
modules = {} modules = {}
def preload(self, root = ''): def addPath(self, root, base_path, priority, recursive = False):
for filename in os.listdir(os.path.join(root, *base_path)):
path = os.path.join(os.path.join(root, *base_path), filename)
if os.path.isdir(path) and filename[:2] != '__':
if u'__init__.py' in os.listdir(path):
new_base_path = ''.join(s + '.' for s in base_path) + filename
self.paths[new_base_path.replace('.', '_')] = (priority, new_base_path, path)
if recursive:
self.addPath(root, base_path + [filename], priority, recursive = True)
def preload(self, root = ''):
core = os.path.join(root, 'couchpotato', 'core') core = os.path.join(root, 'couchpotato', 'core')
self.paths = { self.paths = {
@@ -25,12 +34,10 @@ class Loader(object):
} }
# Add providers to loader # Add providers to loader
provider_dir = os.path.join(root, 'couchpotato', 'core', 'providers') self.addPath(root, ['couchpotato', 'core', 'providers'], 25, recursive = False)
for provider in os.listdir(provider_dir):
path = os.path.join(provider_dir, provider)
if os.path.isdir(path):
self.paths[provider + '_provider'] = (25, 'couchpotato.core.providers.' + provider, path)
# Add media to loader
self.addPath(root, ['couchpotato', 'core', 'media'], 25, recursive = True)
for plugin_type, plugin_tuple in self.paths.iteritems(): for plugin_type, plugin_tuple in self.paths.iteritems():
priority, module, dir_name = plugin_tuple priority, module, dir_name = plugin_tuple
@@ -43,7 +50,13 @@ class Loader(object):
for module_name, plugin in sorted(self.modules[priority].iteritems()): for module_name, plugin in sorted(self.modules[priority].iteritems()):
# Load module # Load module
try: try:
m = getattr(self.loadModule(module_name), plugin.get('name')) if plugin.get('name')[:2] == '__':
continue
m = self.loadModule(module_name)
if m is None:
continue
m = getattr(m, plugin.get('name'))
log.info('Loading %s: %s', (plugin['type'], plugin['name'])) log.info('Loading %s: %s', (plugin['type'], plugin['name']))
@@ -53,7 +66,7 @@ class Loader(object):
self.loadPlugins(m, plugin.get('name')) self.loadPlugins(m, plugin.get('name'))
except ImportError as e: except ImportError as e:
# todo:: subclass ImportError for missing requirements. # todo:: subclass ImportError for missing requirements.
if (e.message.lower().startswith("missing")): if e.message.lower().startswith("missing"):
log.error(e.message) log.error(e.message)
pass pass
# todo:: this needs to be more descriptive. # todo:: this needs to be more descriptive.
@@ -73,19 +86,21 @@ class Loader(object):
splitted = module.split('.') splitted = module.split('.')
for sub in splitted[1:]: for sub in splitted[1:]:
m = getattr(m, sub) m = getattr(m, sub)
if hasattr(m, 'config'):
fireEvent('settings.options', splitted[-1] + '_config', getattr(m, 'config'))
except: except:
raise raise
for cur_file in glob.glob(os.path.join(dir_name, '*')): for cur_file in glob.glob(os.path.join(dir_name, '*')):
name = os.path.basename(cur_file) name = os.path.basename(cur_file)
if os.path.isdir(os.path.join(dir_name, name)): if os.path.isdir(os.path.join(dir_name, name)) and name != 'static' and os.path.isfile(os.path.join(cur_file, '__init__.py')):
module_name = '%s.%s' % (module, name) module_name = '%s.%s' % (module, name)
self.addModule(priority, plugin_type, module_name, name) self.addModule(priority, plugin_type, module_name, name)
def loadSettings(self, module, name, save = True): def loadSettings(self, module, name, save = True):
if not hasattr(module, 'config'):
log.debug('Skip loading settings for plugin %s as it has no config section' % module.__file__)
return False
try: try:
for section in module.config: for section in module.config:
fireEvent('settings.options', section['name'], section) fireEvent('settings.options', section['name'], section)
@@ -100,15 +115,14 @@ class Loader(object):
return False return False
def loadPlugins(self, module, name): def loadPlugins(self, module, name):
if not hasattr(module, 'start'):
log.debug('Skip startup for plugin %s as it has no start section' % module.__file__)
return False
try: try:
klass = module.start() module.start()
klass.registerPlugin()
if klass and getattr(klass, 'auto_register_static'):
klass.registerStatic(module.__file__)
return True return True
except Exception, e: except:
log.error('Failed loading plugin "%s": %s', (module.__file__, traceback.format_exc())) log.error('Failed loading plugin "%s": %s', (module.__file__, traceback.format_exc()))
return False return False
@@ -131,5 +145,8 @@ class Loader(object):
for sub in splitted[1:-1]: for sub in splitted[1:-1]:
m = getattr(m, sub) m = getattr(m, sub)
return m return m
except ImportError:
log.debug('Skip loading module plugin %s: %s', (name, traceback.format_exc()))
return None
except: except:
raise raise
+3 -4
View File
@@ -1,11 +1,10 @@
import logging import logging
import re import re
import traceback
class CPLog(object): class CPLog(object):
context = '' context = ''
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key'] replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key', 'passkey']
def __init__(self, context = ''): def __init__(self, context = ''):
if context.endswith('.main'): if context.endswith('.main'):
@@ -50,8 +49,8 @@ class CPLog(object):
msg = msg % tuple([ss(x) for x in list(replace_tuple)]) msg = msg % tuple([ss(x) for x in list(replace_tuple)])
else: else:
msg = msg % ss(replace_tuple) msg = msg % ss(replace_tuple)
except: except Exception, e:
self.logger.error(u'Failed encoding stuff to log: %s' % traceback.format_exc()) self.logger.error(u'Failed encoding stuff to log "%s": %s' % (msg, e))
if not Env.get('dev'): if not Env.get('dev'):
+13
View File
@@ -0,0 +1,13 @@
from couchpotato.core.event import addEvent
from couchpotato.core.plugins.base import Plugin
class MediaBase(Plugin):
_type = None
def initType(self):
addEvent('media.types', self.getType)
def getType(self):
return self._type
@@ -0,0 +1,13 @@
from couchpotato.core.event import addEvent
from couchpotato.core.plugins.base import Plugin
class LibraryBase(Plugin):
_type = None
def initType(self):
addEvent('library.types', self.getType)
def getType(self):
return self._type
@@ -0,0 +1,75 @@
from .main import Searcher
def start():
return Searcher()
config = [{
'name': 'searcher',
'order': 20,
'groups': [
{
'tab': 'searcher',
'name': 'searcher',
'label': 'Basics',
'description': 'General search options',
'options': [
{
'name': 'preferred_method',
'label': 'First search',
'description': 'Which of the methods do you prefer',
'default': 'both',
'type': 'dropdown',
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrents', 'torrent')],
},
],
}, {
'tab': 'searcher',
'subtab': 'category',
'subtab_label': 'Categories',
'name': 'filter',
'label': 'Global filters',
'description': 'Prefer, ignore & required words in release names',
'options': [
{
'name': 'preferred_words',
'label': 'Preferred',
'default': '',
'placeholder': 'Example: CtrlHD, Amiable, Wiki',
'description': 'Words that give the releases a higher score.'
},
{
'name': 'required_words',
'label': 'Required',
'default': '',
'placeholder': 'Example: DTS, AC3 & English',
'description': 'Release should contain at least one set of words. Sets are separated by "," and each word within a set must be separated with "&"'
},
{
'name': 'ignored_words',
'label': 'Ignored',
'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs',
'description': 'Ignores releases that match any of these sets. (Works like explained above)'
},
],
},
],
}, {
'name': 'nzb',
'groups': [
{
'tab': 'searcher',
'name': 'searcher',
'label': 'NZB',
'wizard': True,
'options': [
{
'name': 'retention',
'label': 'Usenet Retention',
'default': 1500,
'type': 'int',
'unit': 'days'
},
],
},
],
}]
@@ -0,0 +1,45 @@
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
log = CPLog(__name__)
class SearcherBase(Plugin):
in_progress = False
def __init__(self):
super(SearcherBase, self).__init__()
addEvent('searcher.progress', self.getProgress)
addEvent('%s.searcher.progress' % self.getType(), self.getProgress)
self.initCron()
def initCron(self):
""" Set the searcher cronjob
Make sure to reset cronjob after setting has changed
"""
_type = self.getType()
def setCrons():
fireEvent('schedule.cron', '%s.searcher.all' % _type, self.searchAll,
day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
addEvent('app.load', setCrons)
addEvent('setting.save.%s_searcher.cron_day.after' % _type, setCrons)
addEvent('setting.save.%s_searcher.cron_hour.after' % _type, setCrons)
addEvent('setting.save.%s_searcher.cron_minute.after' % _type, setCrons)
def getProgress(self, **kwargs):
""" Return progress of current searcher"""
progress = {
self.getType(): self.in_progress
}
return progress
@@ -0,0 +1,238 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
from couchpotato.core.helpers.variable import md5, getTitle
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.searcher.base import SearcherBase
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
from couchpotato.environment import Env
from inspect import ismethod, isfunction
import datetime
import re
import time
import traceback
log = CPLog(__name__)
class Searcher(SearcherBase):
def __init__(self):
addEvent('searcher.protocols', self.getSearchProtocols)
addEvent('searcher.contains_other_quality', self.containsOtherQuality)
addEvent('searcher.correct_year', self.correctYear)
addEvent('searcher.correct_name', self.correctName)
addEvent('searcher.download', self.download)
addApiView('searcher.full_search', self.searchAllView, docs = {
'desc': 'Starts a full search for all media',
})
addApiView('searcher.progress', self.getProgressForAll, docs = {
'desc': 'Get the progress of all media searches',
'return': {'type': 'object', 'example': """{
'movie': False || object, total & to_go,
'show': False || object, total & to_go,
}"""},
})
def searchAllView(self):
results = {}
for _type in fireEvent('media.types'):
results[_type] = fireEvent('%s.searcher.all_view' % _type)
return results
def getProgressForAll(self):
progress = fireEvent('searcher.progress', merge = True)
return progress
def download(self, data, movie, manual = False):
if not data.get('protocol'):
data['protocol'] = data['type']
data['type'] = 'movie'
# Test to see if any downloaders are enabled for this type
downloader_enabled = fireEvent('download.enabled', manual, data, single = True)
if downloader_enabled:
snatched_status = fireEvent('status.get', 'snatched', single = True)
# Download movie to temp
filedata = None
if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))):
filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
if filedata == 'try_next':
return filedata
download_result = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
log.debug('Downloader result: %s', download_result)
if download_result:
try:
# Mark release as snatched
db = get_session()
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
if rls:
renamer_enabled = Env.setting('enabled', 'renamer')
done_status = fireEvent('status.get', 'done', single = True)
rls.status_id = done_status.get('id') if not renamer_enabled else snatched_status.get('id')
# Save download-id info if returned
if isinstance(download_result, dict):
for key in download_result:
rls_info = ReleaseInfo(
identifier = 'download_%s' % key,
value = toUnicode(download_result.get(key))
)
rls.info.append(rls_info)
db.commit()
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
log.info(snatch_message)
fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict())
# If renamer isn't used, mark movie done
if not renamer_enabled:
active_status = fireEvent('status.get', 'active', single = True)
done_status = fireEvent('status.get', 'done', single = True)
try:
if movie['status_id'] == active_status.get('id'):
for profile_type in movie['profile']['types']:
if profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
log.info('Renamer disabled, marking movie as finished: %s', log_movie)
# Mark release done
rls.status_id = done_status.get('id')
rls.last_edit = int(time.time())
db.commit()
# Mark movie done
mvie = db.query(Movie).filter_by(id = movie['id']).first()
mvie.status_id = done_status.get('id')
mvie.last_edit = int(time.time())
db.commit()
except:
log.error('Failed marking movie finished, renamer disabled: %s', traceback.format_exc())
except:
log.error('Failed marking movie finished: %s', traceback.format_exc())
return True
log.info('Tried to download, but none of the "%s" downloaders are enabled or gave an error', (data.get('protocol')))
return False
def getSearchProtocols(self):
download_protocols = fireEvent('download.enabled_protocols', merge = True)
provider_protocols = fireEvent('provider.enabled_protocols', merge = True)
if download_protocols and len(list(set(provider_protocols) & set(download_protocols))) == 0:
log.error('There aren\'t any providers enabled for your downloader (%s). Check your settings.', ','.join(download_protocols))
return []
for useless_provider in list(set(provider_protocols) - set(download_protocols)):
log.debug('Provider for "%s" enabled, but no downloader.', useless_provider)
search_protocols = download_protocols
if len(search_protocols) == 0:
log.error('There aren\'t any downloaders enabled. Please pick one in settings.')
return []
return search_protocols
def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = None):
if not preferred_quality: preferred_quality = {}
name = nzb['name']
size = nzb.get('size', 0)
nzb_words = re.split('\W+', simplifyString(name))
qualities = fireEvent('quality.all', single = True)
found = {}
for quality in qualities:
# Main in words
if quality['identifier'] in nzb_words:
found[quality['identifier']] = True
# Alt in words
if list(set(nzb_words) & set(quality['alternative'])):
found[quality['identifier']] = True
# Try guessing via quality tags
guess = fireEvent('quality.guess', [nzb.get('name')], single = True)
if guess:
found[guess['identifier']] = True
# Hack for older movies that don't contain quality tag
year_name = fireEvent('scanner.name_year', name, single = True)
if len(found) == 0 and movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None):
if size > 3000: # Assume dvdr
log.info('Quality was missing in name, assuming it\'s a DVD-R based on the size: %s', size)
found['dvdr'] = True
else: # Assume dvdrip
log.info('Quality was missing in name, assuming it\'s a DVD-Rip based on the size: %s', size)
found['dvdrip'] = True
# Allow other qualities
for allowed in preferred_quality.get('allow'):
if found.get(allowed):
del found[allowed]
return not (found.get(preferred_quality['identifier']) and len(found) == 1)
def correctYear(self, haystack, year, year_range):
if not isinstance(haystack, (list, tuple, set)):
haystack = [haystack]
year_name = {}
for string in haystack:
year_name = fireEvent('scanner.name_year', string, single = True)
if year_name and ((year - year_range) <= year_name.get('year') <= (year + year_range)):
log.debug('Movie year matches range: %s looking for %s', (year_name.get('year'), year))
return True
log.debug('Movie year doesn\'t matche range: %s looking for %s', (year_name.get('year'), year))
return False
def correctName(self, check_name, movie_name):
check_names = [check_name]
# Match names between "
try: check_names.append(re.search(r'([\'"])[^\1]*\1', check_name).group(0))
except: pass
# Match longest name between []
try: check_names.append(max(check_name.split('['), key = len))
except: pass
for check_name in list(set(check_names)):
check_movie = fireEvent('scanner.name_year', check_name, single = True)
try:
check_words = filter(None, re.split('\W+', check_movie.get('name', '')))
movie_words = filter(None, re.split('\W+', simplifyString(movie_name)))
if len(check_words) > 0 and len(movie_words) > 0 and len(list(set(check_words) - set(movie_words))) == 0:
return True
except:
pass
return False
class SearchSetupError(Exception):
pass
+6
View File
@@ -0,0 +1,6 @@
from couchpotato.core.media import MediaBase
class MovieTypeBase(MediaBase):
_type = 'movie'
@@ -0,0 +1,6 @@
from .main import MovieBase
def start():
return MovieBase()
config = []
@@ -2,10 +2,10 @@ from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
from couchpotato.core.helpers.encoding import toUnicode, simplifyString from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.helpers.request import getParams, jsonified, getParam from couchpotato.core.helpers.variable import getImdb, splitString, tryInt, \
from couchpotato.core.helpers.variable import getImdb, splitString mergeDicts
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.media.movie import MovieTypeBase
from couchpotato.core.settings.model import Library, LibraryTitle, Movie, \ from couchpotato.core.settings.model import Library, LibraryTitle, Movie, \
Release Release
from couchpotato.environment import Env from couchpotato.environment import Env
@@ -17,17 +17,23 @@ import time
log = CPLog(__name__) log = CPLog(__name__)
class MoviePlugin(Plugin): class MovieBase(MovieTypeBase):
default_dict = { default_dict = {
'profile': {'types': {'quality': {}}}, 'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}}, 'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
'library': {'titles': {}, 'files':{}}, 'library': {'titles': {}, 'files':{}},
'files': {}, 'files': {},
'status': {} 'status': {},
'category': {},
} }
def __init__(self): def __init__(self):
# Initialize this type
super(MovieBase, self).__init__()
self.initType()
addApiView('movie.search', self.search, docs = { addApiView('movie.search', self.search, docs = {
'desc': 'Search the movie providers for a movie', 'desc': 'Search the movie providers for a movie',
'params': { 'params': {
@@ -118,21 +124,21 @@ class MoviePlugin(Plugin):
.filter(Movie.status_id == done_status.get('id'), Movie.last_edit < (now - week)) \ .filter(Movie.status_id == done_status.get('id'), Movie.last_edit < (now - week)) \
.all() .all()
#
for movie in movies: for movie in movies:
for rel in movie.releases: for rel in movie.releases:
if rel.status_id in [available_status.get('id'), snatched_status.get('id')]: if rel.status_id in [available_status.get('id'), snatched_status.get('id')]:
fireEvent('release.delete', id = rel.id, single = True) fireEvent('release.delete', id = rel.id, single = True)
def getView(self): db.expire_all()
movie_id = getParam('id') def getView(self, id = None, **kwargs):
movie = self.get(movie_id) if movie_id else None
return jsonified({ movie = self.get(id) if id else None
return {
'success': movie is not None, 'success': movie is not None,
'movie': movie, 'movie': movie,
}) }
def get(self, movie_id): def get(self, movie_id):
@@ -140,7 +146,7 @@ class MoviePlugin(Plugin):
imdb_id = getImdb(str(movie_id)) imdb_id = getImdb(str(movie_id))
if(imdb_id): if imdb_id:
m = db.query(Movie).filter(Movie.library.has(identifier = imdb_id)).first() m = db.query(Movie).filter(Movie.library.has(identifier = imdb_id)).first()
else: else:
m = db.query(Movie).filter_by(id = movie_id).first() m = db.query(Movie).filter_by(id = movie_id).first()
@@ -149,6 +155,7 @@ class MoviePlugin(Plugin):
if m: if m:
results = m.to_dict(self.default_dict) results = m.to_dict(self.default_dict)
db.expire_all()
return results return results
def list(self, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None): def list(self, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None):
@@ -161,21 +168,33 @@ class MoviePlugin(Plugin):
if release_status and not isinstance(release_status, (list, tuple)): if release_status and not isinstance(release_status, (list, tuple)):
release_status = [release_status] release_status = [release_status]
# query movie ids
q = db.query(Movie) \ q = db.query(Movie) \
.outerjoin(Movie.releases, Movie.library, Library.titles) \ .with_entities(Movie.id) \
.filter(LibraryTitle.default == True) \
.group_by(Movie.id) .group_by(Movie.id)
# Filter on movie status # Filter on movie status
if status and len(status) > 0: if status and len(status) > 0:
q = q.filter(or_(*[Movie.status.has(identifier = s) for s in status])) statuses = fireEvent('status.get', status, single = len(status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.filter(Movie.status_id.in_(statuses))
# Filter on release status # Filter on release status
if release_status and len(release_status) > 0: if release_status and len(release_status) > 0:
q = q.filter(or_(*[Release.status.has(identifier = s) for s in release_status])) q = q.join(Movie.releases)
total_count = q.count() statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.filter(Release.status_id.in_(statuses))
# Only join when searching / ordering
if starts_with or search or order != 'release_order':
q = q.join(Movie.library, Library.titles) \
.filter(LibraryTitle.default == True)
# Add search filters
filter_or = [] filter_or = []
if starts_with: if starts_with:
starts_with = toUnicode(starts_with.lower()) starts_with = toUnicode(starts_with.lower())
@@ -190,46 +209,79 @@ class MoviePlugin(Plugin):
if search: if search:
filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%')) filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%'))
if filter_or: if len(filter_or) > 0:
q = q.filter(or_(*filter_or)) q = q.filter(or_(*filter_or))
total_count = q.count()
if total_count == 0:
return 0, []
if order == 'release_order': if order == 'release_order':
q = q.order_by(desc(Release.last_edit)) q = q.order_by(desc(Release.last_edit))
else: else:
q = q.order_by(asc(LibraryTitle.simple_title)) q = q.order_by(asc(LibraryTitle.simple_title))
q = q.subquery() if limit_offset:
q2 = db.query(Movie).join((q, q.c.id == Movie.id)) \ splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
.options(joinedload_all('releases')) \ limit = splt[0]
.options(joinedload_all('profile.types')) \ offset = 0 if len(splt) is 1 else splt[1]
q = q.limit(limit).offset(offset)
# Get all movie_ids in sorted order
movie_ids = [m.id for m in q.all()]
# List release statuses
releases = db.query(Release) \
.filter(Release.movie_id.in_(movie_ids)) \
.all()
release_statuses = dict((m, set()) for m in movie_ids)
releases_count = dict((m, 0) for m in movie_ids)
for release in releases:
release_statuses[release.movie_id].add('%d,%d' % (release.status_id, release.quality_id))
releases_count[release.movie_id] += 1
# Get main movie data
q2 = db.query(Movie) \
.options(joinedload_all('library.titles')) \ .options(joinedload_all('library.titles')) \
.options(joinedload_all('library.files')) \ .options(joinedload_all('library.files')) \
.options(joinedload_all('status')) \ .options(joinedload_all('status')) \
.options(joinedload_all('files')) .options(joinedload_all('files'))
if limit_offset: q2 = q2.filter(Movie.id.in_(movie_ids))
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
limit = splt[0]
offset = 0 if len(splt) is 1 else splt[1]
q2 = q2.limit(limit).offset(offset)
results = q2.all() results = q2.all()
movies = []
# Create dict by movie id
movie_dict = {}
for movie in results: for movie in results:
temp = movie.to_dict({ movie_dict[movie.id] = movie
'profile': {'types': {}},
'releases': {'files':{}, 'info': {}}, # List movies based on movie_ids order
movies = []
for movie_id in movie_ids:
releases = []
for r in release_statuses.get(movie_id):
x = splitString(r)
releases.append({'status_id': x[0], 'quality_id': x[1]})
# Merge releases with movie dict
movies.append(mergeDicts(movie_dict[movie_id].to_dict({
'library': {'titles': {}, 'files':{}}, 'library': {'titles': {}, 'files':{}},
'files': {}, 'files': {},
}) }), {
movies.append(temp) 'releases': releases,
'releases_count': releases_count.get(movie_id),
}))
#db.close() db.expire_all()
return (total_count, movies) return total_count, movies
def availableChars(self, status = None, release_status = None): def availableChars(self, status = None, release_status = None):
chars = '' status = status or []
release_status = release_status or []
db = get_session() db = get_session()
@@ -239,38 +291,53 @@ class MoviePlugin(Plugin):
if release_status and not isinstance(release_status, (list, tuple)): if release_status and not isinstance(release_status, (list, tuple)):
release_status = [release_status] release_status = [release_status]
q = db.query(Movie) \ q = db.query(Movie)
.outerjoin(Movie.releases, Movie.library, Library.titles, Movie.status) \
.options(joinedload_all('library.titles'))
# Filter on movie status # Filter on movie status
if status and len(status) > 0: if status and len(status) > 0:
q = q.filter(or_(*[Movie.status.has(identifier = s) for s in status])) statuses = fireEvent('status.get', status, single = len(release_status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.filter(Movie.status_id.in_(statuses))
# Filter on release status # Filter on release status
if release_status and len(release_status) > 0: if release_status and len(release_status) > 0:
q = q.filter(or_(*[Release.status.has(identifier = s) for s in release_status]))
results = q.all() statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
statuses = [s.get('id') for s in statuses]
for movie in results: q = q.join(Movie.releases) \
char = movie.library.titles[0].simple_title[0] .filter(Release.status_id.in_(statuses))
char = char if char in ascii_lowercase else '#'
if char not in chars:
chars += str(char)
#db.close() q = q.join(Library, LibraryTitle) \
return ''.join(sorted(chars, key = str.lower)) .with_entities(LibraryTitle.simple_title) \
.filter(LibraryTitle.default == True)
def listView(self): titles = q.all()
params = getParams() chars = set()
status = splitString(params.get('status', None)) for title in titles:
release_status = splitString(params.get('release_status', None)) try:
limit_offset = params.get('limit_offset', None) char = title[0][0]
starts_with = params.get('starts_with', None) char = char if char in ascii_lowercase else '#'
search = params.get('search', None) chars.add(str(char))
order = params.get('order', None) except:
log.error('Failed getting title for %s', title.libraries_id)
if len(chars) == 25:
break
db.expire_all()
return ''.join(sorted(chars))
def listView(self, **kwargs):
status = splitString(kwargs.get('status'))
release_status = splitString(kwargs.get('release_status'))
limit_offset = kwargs.get('limit_offset')
starts_with = kwargs.get('starts_with')
search = kwargs.get('search')
order = kwargs.get('order')
total_movies, movies = self.list( total_movies, movies = self.list(
status = status, status = status,
@@ -281,32 +348,31 @@ class MoviePlugin(Plugin):
order = order order = order
) )
return jsonified({ return {
'success': True, 'success': True,
'empty': len(movies) == 0, 'empty': len(movies) == 0,
'total': total_movies, 'total': total_movies,
'movies': movies, 'movies': movies,
}) }
def charView(self): def charView(self, **kwargs):
params = getParams() status = splitString(kwargs.get('status', None))
status = splitString(params.get('status', None)) release_status = splitString(kwargs.get('release_status', None))
release_status = splitString(params.get('release_status', None))
chars = self.availableChars(status, release_status) chars = self.availableChars(status, release_status)
return jsonified({ return {
'success': True, 'success': True,
'empty': len(chars) == 0, 'empty': len(chars) == 0,
'chars': chars, 'chars': chars,
}) }
def refresh(self): def refresh(self, id = '', **kwargs):
db = get_session() db = get_session()
for id in splitString(getParam('id')): for x in splitString(id):
movie = db.query(Movie).filter_by(id = id).first() movie = db.query(Movie).filter_by(id = x).first()
if movie: if movie:
@@ -315,18 +381,16 @@ class MoviePlugin(Plugin):
for title in movie.library.titles: for title in movie.library.titles:
if title.default: default_title = title.title if title.default: default_title = title.title
fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True) fireEvent('notify.frontend', type = 'movie.busy.%s' % x, data = True)
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(id)) fireEventAsync('library.update.movie', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x))
db.expire_all()
#db.close() return {
return jsonified({
'success': True, 'success': True,
}) }
def search(self): def search(self, q = '', **kwargs):
q = getParam('q')
cache_key = u'%s/%s' % (__name__, simplifyString(q)) cache_key = u'%s/%s' % (__name__, simplifyString(q))
movies = Env.get('cache').get(cache_key) movies = Env.get('cache').get(cache_key)
@@ -338,13 +402,14 @@ class MoviePlugin(Plugin):
movies = fireEvent('movie.search', q = q, merge = True) movies = fireEvent('movie.search', q = q, merge = True)
Env.get('cache').set(cache_key, movies) Env.get('cache').set(cache_key, movies)
return jsonified({ return {
'success': True, 'success': True,
'empty': len(movies) == 0 if movies else 0, 'empty': len(movies) == 0 if movies else 0,
'movies': movies, 'movies': movies,
}) }
def add(self, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None): def add(self, params = None, force_readd = True, search_after = True, update_library = False, status_id = None):
if not params: params = {}
if not params.get('identifier'): if not params.get('identifier'):
msg = 'Can\'t add movie without imdb identifier.' msg = 'Can\'t add movie without imdb identifier.'
@@ -363,23 +428,26 @@ class MoviePlugin(Plugin):
pass pass
library = fireEvent('library.add', single = True, attrs = params, update_after = update_library) library = fireEvent('library.add.movie', single = True, attrs = params, update_after = update_library)
# Status # Status
status_active, snatched_status, ignored_status, done_status, downloaded_status = \ status_active, snatched_status, ignored_status, done_status, downloaded_status = \
fireEvent('status.get', ['active', 'snatched', 'ignored', 'done', 'downloaded'], single = True) fireEvent('status.get', ['active', 'snatched', 'ignored', 'done', 'downloaded'], single = True)
default_profile = fireEvent('profile.default', single = True) default_profile = fireEvent('profile.default', single = True)
cat_id = params.get('category_id')
db = get_session() db = get_session()
m = db.query(Movie).filter_by(library_id = library.get('id')).first() m = db.query(Movie).filter_by(library_id = library.get('id')).first()
added = True added = True
do_search = False do_search = False
search_after = search_after and self.conf('search_on_add', section = 'moviesearcher')
if not m: if not m:
m = Movie( m = Movie(
library_id = library.get('id'), library_id = library.get('id'),
profile_id = params.get('profile_id', default_profile.get('id')), profile_id = params.get('profile_id', default_profile.get('id')),
status_id = status_id if status_id else status_active.get('id'), status_id = status_id if status_id else status_active.get('id'),
category_id = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else None,
) )
db.add(m) db.add(m)
db.commit() db.commit()
@@ -388,7 +456,7 @@ class MoviePlugin(Plugin):
if search_after: if search_after:
onComplete = self.createOnComplete(m.id) onComplete = self.createOnComplete(m.id)
fireEventAsync('library.update', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete) fireEventAsync('library.update.movie', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete)
search_after = False search_after = False
elif force_readd: elif force_readd:
@@ -401,6 +469,7 @@ class MoviePlugin(Plugin):
fireEvent('release.delete', release.id, single = True) fireEvent('release.delete', release.id, single = True)
m.profile_id = params.get('profile_id', default_profile.get('id')) m.profile_id = params.get('profile_id', default_profile.get('id'))
m.category_id = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else (m.category_id or None)
else: else:
log.debug('Movie already exists, not updating: %s', params) log.debug('Movie already exists, not updating: %s', params)
added = False added = False
@@ -428,37 +497,38 @@ class MoviePlugin(Plugin):
if added: if added:
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = 'Successfully added "%s" to your wanted list.' % params.get('title', '')) fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = 'Successfully added "%s" to your wanted list.' % params.get('title', ''))
#db.close() db.expire_all()
return movie_dict return movie_dict
def addView(self): def addView(self, **kwargs):
params = getParams() movie_dict = self.add(params = kwargs)
movie_dict = self.add(params) return {
return jsonified({
'success': True, 'success': True,
'added': True if movie_dict else False, 'added': True if movie_dict else False,
'movie': movie_dict, 'movie': movie_dict,
}) }
def edit(self): def edit(self, id = '', **kwargs):
params = getParams()
db = get_session() db = get_session()
available_status = fireEvent('status.get', 'available', single = True) available_status = fireEvent('status.get', 'available', single = True)
ids = splitString(params.get('id')) ids = splitString(id)
for movie_id in ids: for movie_id in ids:
m = db.query(Movie).filter_by(id = movie_id).first() m = db.query(Movie).filter_by(id = movie_id).first()
if not m: if not m:
continue continue
m.profile_id = params.get('profile_id') m.profile_id = kwargs.get('profile_id')
cat_id = kwargs.get('category_id')
if cat_id is not None:
m.category_id = tryInt(cat_id) if tryInt(cat_id) > 0 else None
# Remove releases # Remove releases
for rel in m.releases: for rel in m.releases:
@@ -467,33 +537,31 @@ class MoviePlugin(Plugin):
db.commit() db.commit()
# Default title # Default title
if params.get('default_title'): if kwargs.get('default_title'):
for title in m.library.titles: for title in m.library.titles:
title.default = toUnicode(params.get('default_title', '')).lower() == toUnicode(title.title).lower() title.default = toUnicode(kwargs.get('default_title', '')).lower() == toUnicode(title.title).lower()
db.commit() db.commit()
fireEvent('movie.restatus', m.id) fireEvent('movie.restatus', m.id)
movie_dict = m.to_dict(self.default_dict) movie_dict = m.to_dict(self.default_dict)
fireEventAsync('searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id)) fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id))
#db.close() db.expire_all()
return jsonified({ return {
'success': True, 'success': True,
}) }
def deleteView(self): def deleteView(self, id = '', **kwargs):
params = getParams() ids = splitString(id)
ids = splitString(params.get('id'))
for movie_id in ids: for movie_id in ids:
self.delete(movie_id, delete_from = params.get('delete_from', 'all')) self.delete(movie_id, delete_from = kwargs.get('delete_from', 'all'))
return jsonified({ return {
'success': True, 'success': True,
}) }
def delete(self, movie_id, delete_from = None): def delete(self, movie_id, delete_from = None):
@@ -513,7 +581,7 @@ class MoviePlugin(Plugin):
total_deleted = 0 total_deleted = 0
new_movie_status = None new_movie_status = None
for release in movie.releases: for release in movie.releases:
if delete_from in ['wanted', 'snatched']: if delete_from in ['wanted', 'snatched', 'late']:
if release.status_id != done_status.get('id'): if release.status_id != done_status.get('id'):
db.delete(release) db.delete(release)
total_deleted += 1 total_deleted += 1
@@ -540,7 +608,7 @@ class MoviePlugin(Plugin):
if deleted: if deleted:
fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict()) fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict())
#db.close() db.expire_all()
return True return True
def restatus(self, movie_id): def restatus(self, movie_id):
@@ -554,7 +622,7 @@ class MoviePlugin(Plugin):
log.debug('Can\'t restatus movie, doesn\'t seem to exist.') log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
return False return False
log.debug('Changing status for %s', (m.library.titles[0].title)) log.debug('Changing status for %s', m.library.titles[0].title)
if not m.profile: if not m.profile:
m.status_id = done_status.get('id') m.status_id = done_status.get('id')
else: else:
@@ -568,7 +636,6 @@ class MoviePlugin(Plugin):
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id') m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
db.commit() db.commit()
#db.close()
return True return True
@@ -577,7 +644,8 @@ class MoviePlugin(Plugin):
def onComplete(): def onComplete():
db = get_session() db = get_session()
movie = db.query(Movie).filter_by(id = movie_id).first() movie = db.query(Movie).filter_by(id = movie_id).first()
fireEventAsync('searcher.single', movie.to_dict(self.default_dict), on_complete = self.createNotifyFront(movie_id)) fireEventAsync('movie.searcher.single', movie.to_dict(self.default_dict), on_complete = self.createNotifyFront(movie_id))
db.expire_all()
return onComplete return onComplete
@@ -588,5 +656,6 @@ class MoviePlugin(Plugin):
db = get_session() db = get_session()
movie = db.query(Movie).filter_by(id = movie_id).first() movie = db.query(Movie).filter_by(id = movie_id).first()
fireEvent('notify.frontend', type = 'movie.update.%s' % movie.id, data = movie.to_dict(self.default_dict)) fireEvent('notify.frontend', type = 'movie.update.%s' % movie.id, data = movie.to_dict(self.default_dict))
db.expire_all()
return notifyFront return notifyFront
@@ -14,6 +14,7 @@ var MovieList = new Class({
movies: [], movies: [],
movies_added: {}, movies_added: {},
total_movies: 0,
letters: {}, letters: {},
filter: null, filter: null,
@@ -23,7 +24,7 @@ var MovieList = new Class({
self.offset = 0; self.offset = 0;
self.filter = self.options.filter || { self.filter = self.options.filter || {
'startswith': null, 'starts_with': null,
'search': null 'search': null
} }
@@ -48,7 +49,7 @@ var MovieList = new Class({
self.changeView('list'); self.changeView('list');
else else
self.changeView(self.getSavedView() || self.options.view || 'details'); self.changeView(self.getSavedView() || self.options.view || 'details');
self.getMovies(); self.getMovies();
App.addEvent('movie.added', self.movieAdded.bind(self)) App.addEvent('movie.added', self.movieAdded.bind(self))
@@ -62,7 +63,8 @@ var MovieList = new Class({
self.movies.each(function(movie){ self.movies.each(function(movie){
if(movie.get('id') == notification.data.id){ if(movie.get('id') == notification.data.id){
movie.destroy(); movie.destroy();
delete self.movies_added[notification.data.id] delete self.movies_added[notification.data.id];
self.setCounter(self.counter_count-1);
} }
}) })
} }
@@ -76,6 +78,7 @@ var MovieList = new Class({
if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){ if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){
window.scroll(0,0); window.scroll(0,0);
self.createMovie(notification.data, 'top'); self.createMovie(notification.data, 'top');
self.setCounter(self.counter_count+1);
self.checkIfEmpty(); self.checkIfEmpty();
} }
@@ -115,7 +118,7 @@ var MovieList = new Class({
self.createMovie(movie); self.createMovie(movie);
}); });
self.total_movies = total; self.total_movies += total;
self.setCounter(total); self.setCounter(total);
}, },
@@ -125,8 +128,41 @@ var MovieList = new Class({
if(!self.navigation_counter) return; if(!self.navigation_counter) return;
self.counter_count = count;
self.navigation_counter.set('text', (count || 0) + ' movies'); self.navigation_counter.set('text', (count || 0) + ' movies');
if (self.empty_message) {
self.empty_message.destroy();
self.empty_message = null;
}
if(self.total_movies && count == 0 && !self.empty_message){
var message = (self.filter.search ? 'for "'+self.filter.search+'"' : '') +
(self.filter.starts_with ? ' in <strong>'+self.filter.starts_with+'</strong>' : '');
self.empty_message = new Element('.message', {
'html': 'No movies found ' + message + '.<br/>'
}).grab(
new Element('a', {
'text': 'Reset filter',
'events': {
'click': function(){
self.filter = {
'starts_with': null,
'search': null
};
self.navigation_search_input.set('value', '');
self.reset();
self.activateLetter();
self.getMovies(true);
self.last_search_value = '';
}
}
})
).inject(self.movie_list);
}
}, },
createMovie: function(movie, inject_at){ createMovie: function(movie, inject_at){
@@ -151,66 +187,69 @@ var MovieList = new Class({
self.el.addClass('with_navigation') self.el.addClass('with_navigation')
self.navigation = new Element('div.alph_nav').grab( self.navigation = new Element('div.alph_nav').adopt(
new Element('div').adopt( self.mass_edit_form = new Element('div.mass_edit_form').adopt(
self.navigation_alpha = new Element('ul.numbers', { new Element('span.select').adopt(
self.mass_edit_select = new Element('input[type=checkbox].inlay', {
'events': {
'change': self.massEditToggleAll.bind(self)
}
}),
self.mass_edit_selected = new Element('span.count', {'text': 0}),
self.mass_edit_selected_label = new Element('span', {'text': 'selected'})
),
new Element('div.quality').adopt(
self.mass_edit_quality = new Element('select'),
new Element('a.button.orange', {
'text': 'Change quality',
'events': {
'click': self.changeQualitySelected.bind(self)
}
})
),
new Element('div.delete').adopt(
new Element('span[text=or]'),
new Element('a.button.red', {
'text': 'Delete',
'events': {
'click': self.deleteSelected.bind(self)
}
})
),
new Element('div.refresh').adopt(
new Element('span[text=or]'),
new Element('a.button.green', {
'text': 'Refresh',
'events': {
'click': self.refreshSelected.bind(self)
}
})
)
),
new Element('div.menus').adopt(
self.navigation_counter = new Element('span.counter[title=Total]'),
self.filter_menu = new Block.Menu(self, {
'class': 'filter'
}),
self.navigation_actions = new Element('ul.actions', {
'events': { 'events': {
'click:relay(li)': function(e, el){ 'click:relay(li)': function(e, el){
self.movie_list.empty() var a = 'active';
self.activateLetter(el.get('data-letter')) self.navigation_actions.getElements('.'+a).removeClass(a);
self.getMovies() self.changeView(el.get('data-view'));
this.addClass(a);
el.inject(el.getParent(), 'top');
el.getSiblings().hide()
setTimeout(function(){
el.getSiblings().setStyle('display', null);
}, 100)
} }
} }
}), }),
self.navigation_counter = new Element('span.counter[title=Total]'), self.navigation_menu = new Block.Menu(self, {
self.navigation_actions = new Element('ul.inlay.actions.reversed'), 'class': 'extra'
self.navigation_search_input = new Element('input.search.inlay', { })
'title': 'Search through ' + self.options.identifier,
'placeholder': 'Search through ' + self.options.identifier,
'events': {
'keyup': self.search.bind(self),
'change': self.search.bind(self)
}
}),
self.navigation_menu = new Block.Menu(self),
self.mass_edit_form = new Element('div.mass_edit_form').adopt(
new Element('span.select').adopt(
self.mass_edit_select = new Element('input[type=checkbox].inlay', {
'events': {
'change': self.massEditToggleAll.bind(self)
}
}),
self.mass_edit_selected = new Element('span.count', {'text': 0}),
self.mass_edit_selected_label = new Element('span', {'text': 'selected'})
),
new Element('div.quality').adopt(
self.mass_edit_quality = new Element('select'),
new Element('a.button.orange', {
'text': 'Change quality',
'events': {
'click': self.changeQualitySelected.bind(self)
}
})
),
new Element('div.delete').adopt(
new Element('span[text=or]'),
new Element('a.button.red', {
'text': 'Delete',
'events': {
'click': self.deleteSelected.bind(self)
}
})
),
new Element('div.refresh').adopt(
new Element('span[text=or]'),
new Element('a.button.green', {
'text': 'Refresh',
'events': {
'click': self.refreshSelected.bind(self)
}
})
)
)
) )
).inject(self.el, 'top'); ).inject(self.el, 'top');
@@ -223,20 +262,56 @@ var MovieList = new Class({
}).inject(self.mass_edit_quality) }).inject(self.mass_edit_quality)
}); });
self.filter_menu.addLink(
self.navigation_search_input = new Element('input', {
'title': 'Search through ' + self.options.identifier,
'placeholder': 'Search through ' + self.options.identifier,
'events': {
'keyup': self.search.bind(self),
'change': self.search.bind(self)
}
})
).addClass('search');
var available_chars;
self.filter_menu.addEvent('open', function(){
self.navigation_search_input.focus();
// Get available chars and highlight
if(!available_chars && (self.navigation.isDisplayed() || self.navigation.isVisible()))
Api.request('movie.available_chars', {
'data': Object.merge({
'status': self.options.status
}, self.filter),
'onSuccess': function(json){
available_chars = json.chars
json.chars.split('').each(function(c){
self.letters[c.capitalize()].addClass('available')
})
}
});
});
self.filter_menu.addLink(
self.navigation_alpha = new Element('ul.numbers', {
'events': {
'click:relay(li.available)': function(e, el){
self.activateLetter(el.get('data-letter'))
self.getMovies(true)
}
}
})
);
// Actions // Actions
['mass_edit', 'details', 'list'].each(function(view){ ['mass_edit', 'details', 'list'].each(function(view){
self.navigation_actions.adopt( var current = self.current_view == view;
new Element('li.'+view+(self.current_view == view ? '.active' : '')+'[data-view='+view+']', { new Element('li', {
'events': { 'class': 'icon2 ' + view + (current ? ' active ' : ''),
'click': function(e){ 'data-view': view
var a = 'active'; }).inject(self.navigation_actions, current ? 'top' : 'bottom');
self.navigation_actions.getElements('.'+a).removeClass(a);
self.changeView(this.get('data-view'));
this.addClass(a);
}
}
}).adopt(new Element('span'))
)
}); });
// All // All
@@ -253,21 +328,6 @@ var MovieList = new Class({
}).inject(self.navigation_alpha); }).inject(self.navigation_alpha);
}); });
// Get available chars and highlight
if(self.navigation.isDisplayed() || self.navigation.isVisible())
Api.request('movie.available_chars', {
'data': Object.merge({
'status': self.options.status
}, self.filter),
'onSuccess': function(json){
json.chars.split('').each(function(c){
self.letters[c.capitalize()].addClass('available')
})
}
});
// Add menu or hide // Add menu or hide
if (self.options.menu.length > 0) if (self.options.menu.length > 0)
self.options.menu.each(function(menu_item){ self.options.menu.each(function(menu_item){
@@ -322,14 +382,14 @@ var MovieList = new Class({
self.movies.each(function(movie){ self.movies.each(function(movie){
if (movie.isSelected()){ if (movie.isSelected()){
$(movie).destroy() $(movie).destroy()
erase_movies.include(movie) erase_movies.include(movie);
} }
}); });
erase_movies.each(function(movie){ erase_movies.each(function(movie){
self.movies.erase(movie); self.movies.erase(movie);
movie.destroy();
movie.destroy() self.setCounter(self.counter_count-1);
}); });
self.calculateSelected(); self.calculateSelected();
@@ -448,8 +508,7 @@ var MovieList = new Class({
self.activateLetter(); self.activateLetter();
self.filter.search = search_value; self.filter.search = search_value;
self.movie_list.empty(); self.getMovies(true);
self.getMovies();
self.last_search_value = search_value; self.last_search_value = search_value;
@@ -461,11 +520,10 @@ var MovieList = new Class({
var self = this; var self = this;
self.reset(); self.reset();
self.movie_list.empty(); self.getMovies(true);
self.getMovies();
}, },
getMovies: function(){ getMovies: function(reset){
var self = this; var self = this;
if(self.scrollspy){ if(self.scrollspy){
@@ -492,10 +550,13 @@ var MovieList = new Class({
Api.request(self.options.api_call || 'movie.list', { Api.request(self.options.api_call || 'movie.list', {
'data': Object.merge({ 'data': Object.merge({
'status': self.options.status, 'status': self.options.status,
'limit_offset': self.options.limit + ',' + self.offset 'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null
}, self.filter), }, self.filter),
'onSuccess': function(json){ 'onSuccess': function(json){
if(reset)
self.movie_list.empty();
if(self.loader_first){ if(self.loader_first){
var lf = self.loader_first; var lf = self.loader_first;
self.loader_first.addClass('hide') self.loader_first.addClass('hide')
@@ -507,7 +568,7 @@ var MovieList = new Class({
} }
self.store(json.movies); self.store(json.movies);
self.addMovies(json.movies, json.total); self.addMovies(json.movies, json.total || json.movies.length);
if(self.scrollspy) { if(self.scrollspy) {
self.load_more.set('text', 'load more movies'); self.load_more.set('text', 'load more movies');
self.scrollspy.start(); self.scrollspy.start();
@@ -1,9 +1,13 @@
var MovieAction = new Class({ var MovieAction = new Class({
class_name: 'action icon', Implements: [Options],
initialize: function(movie){ class_name: 'action icon2',
initialize: function(movie, options){
var self = this; var self = this;
self.setOptions(options);
self.movie = movie; self.movie = movie;
self.create(); self.create();
@@ -14,11 +18,39 @@ var MovieAction = new Class({
create: function(){}, create: function(){},
disable: function(){ disable: function(){
this.el.addClass('disable') if(this.el)
this.el.addClass('disable')
}, },
enable: function(){ enable: function(){
this.el.removeClass('disable') if(this.el)
this.el.removeClass('disable')
},
getTitle: function(){
var self = this;
try {
return self.movie.getTitle();
}
catch(e){
try {
return self.movie.original_title ? self.movie.original_title : self.movie.titles[0];
}
catch(e){
return 'Unknown';
}
}
},
get: function(key){
var self = this;
try {
return self.movie.get(key)
}
catch(e){
return self.movie[key]
}
}, },
createMask: function(){ createMask: function(){
@@ -62,10 +94,10 @@ MA.IMDB = new Class({
create: function(){ create: function(){
var self = this; var self = this;
self.id = self.movie.get('identifier'); self.id = self.movie.get('imdb') || self.movie.get('identifier');
self.el = new Element('a.imdb', { self.el = new Element('a.imdb', {
'title': 'Go to the IMDB page of ' + self.movie.getTitle(), 'title': 'Go to the IMDB page of ' + self.getTitle(),
'href': 'http://www.imdb.com/title/'+self.id+'/', 'href': 'http://www.imdb.com/title/'+self.id+'/',
'target': '_blank' 'target': '_blank'
}); });
@@ -82,8 +114,8 @@ MA.Release = new Class({
create: function(){ create: function(){
var self = this; var self = this;
self.el = new Element('a.releases.icon.download', { self.el = new Element('a.releases.download', {
'title': 'Show the releases that are available for ' + self.movie.getTitle(), 'title': 'Show the releases that are available for ' + self.getTitle(),
'events': { 'events': {
'click': self.show.bind(self) 'click': self.show.bind(self)
} }
@@ -94,16 +126,54 @@ MA.Release = new Class({
else else
self.showHelper(); self.showHelper();
App.addEvent('movie.searcher.ended.'+self.movie.data.id, function(notification){
self.releases = null;
if(self.options_container){
self.options_container.destroy();
self.options_container = null;
}
});
},
show: function(e){
var self = this;
if(e)
(e).preventDefault();
if(self.releases)
self.createReleases();
else {
self.movie.busy(true);
Api.request('release.for_movie', {
'data': {
'id': self.movie.data.id
},
'onComplete': function(json){
self.movie.busy(false, 1);
if(json && json.releases){
self.releases = json.releases;
self.createReleases();
}
else
alert('Something went wrong, check the logs.');
}
});
}
}, },
createReleases: function(){ createReleases: function(){
var self = this; var self = this;
if(!self.options_container){ if(!self.options_container){
self.options_container = new Element('div.options').adopt( self.options_container = new Element('div.options').grab(
self.release_container = new Element('div.releases.table').adopt( self.release_container = new Element('div.releases.table')
self.trynext_container = new Element('div.buttons.try_container')
)
); );
// Header // Header
@@ -117,7 +187,7 @@ MA.Release = new Class({
new Element('span.provider', {'text': 'Provider'}) new Element('span.provider', {'text': 'Provider'})
).inject(self.release_container) ).inject(self.release_container)
self.movie.data.releases.sortBy('-info.score').each(function(release){ self.releases.each(function(release){
var status = Status.get(release.status_id), var status = Status.get(release.status_id),
quality = Quality.getProfile(release.quality_id) || {}, quality = Quality.getProfile(release.quality_id) || {},
@@ -138,7 +208,7 @@ MA.Release = new Class({
} }
// Create release // Create release
new Element('div', { var item = new Element('div', {
'class': 'item '+status.identifier, 'class': 'item '+status.identifier,
'id': 'release_'+release.id 'id': 'release_'+release.id
}).adopt( }).adopt(
@@ -149,11 +219,11 @@ MA.Release = new Class({
new Element('span.age', {'text': self.get(release, 'age')}), new Element('span.age', {'text': self.get(release, 'age')}),
new Element('span.score', {'text': self.get(release, 'score')}), new Element('span.score', {'text': self.get(release, 'score')}),
new Element('span.provider', { 'text': provider, 'title': provider }), new Element('span.provider', { 'text': provider, 'title': provider }),
release.info['detail_url'] ? new Element('a.info.icon', { release.info['detail_url'] ? new Element('a.info.icon2', {
'href': release.info['detail_url'], 'href': release.info['detail_url'],
'target': '_blank' 'target': '_blank'
}) : null, }) : new Element('a'),
new Element('a.download.icon', { new Element('a.download.icon2', {
'events': { 'events': {
'click': function(e){ 'click': function(e){
(e).preventDefault(); (e).preventDefault();
@@ -162,16 +232,17 @@ MA.Release = new Class({
} }
} }
}), }),
new Element('a.delete.icon', { new Element('a.delete.icon2', {
'events': { 'events': {
'click': function(e){ 'click': function(e){
(e).preventDefault(); (e).preventDefault();
self.ignore(release); self.ignore(release);
this.getParent('.item').toggleClass('ignored')
} }
} }
}) })
).inject(self.release_container) ).inject(self.release_container);
release['el'] = item;
if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){ if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){
if(!self.last_release || (self.last_release && self.last_release.status.identifier != 'snatched' && status.identifier == 'snatched')) if(!self.last_release || (self.last_release && self.last_release.status.identifier != 'snatched' && status.identifier == 'snatched'))
@@ -182,33 +253,40 @@ MA.Release = new Class({
} }
}); });
if(self.last_release){ if(self.last_release)
self.release_container.getElement('#release_'+self.last_release.id).addClass('last_release'); self.release_container.getElements('#release_'+self.last_release.id).addClass('last_release');
}
if(self.next_release){ if(self.next_release)
self.release_container.getElement('#release_'+self.next_release.id).addClass('next_release'); self.release_container.getElements('#release_'+self.next_release.id).addClass('next_release');
}
if(self.next_release || self.last_release){ if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){
self.trynext_container = new Element('div.buttons.try_container').inject(self.release_container, 'top');
var nr = self.next_release,
lr = self.last_release;
self.trynext_container.adopt( self.trynext_container.adopt(
new Element('span.or', { new Element('span.or', {
'text': 'This movie is snatched, if anything went wrong, download' 'text': 'This movie is snatched, if anything went wrong, download'
}), }),
self.last_release ? new Element('a.button.orange', { lr ? new Element('a.button.orange', {
'text': 'the same release again', 'text': 'the same release again',
'events': { 'events': {
'click': self.trySameRelease.bind(self) 'click': function(){
self.download(lr);
}
} }
}) : null, }) : null,
self.next_release && self.last_release ? new Element('span.or', { nr && lr ? new Element('span.or', {
'text': ',' 'text': ','
}) : null, }) : null,
self.next_release ? [new Element('a.button.green', { nr ? [new Element('a.button.green', {
'text': self.last_release ? 'another release' : 'the best release', 'text': lr ? 'another release' : 'the best release',
'events': { 'events': {
'click': self.tryNextRelease.bind(self) 'click': function(){
self.download(nr);
}
} }
}), }),
new Element('span.or', { new Element('span.or', {
@@ -217,18 +295,15 @@ MA.Release = new Class({
) )
} }
self.last_release = null;
self.next_release = null;
} }
}, // Show it
show: function(e){
var self = this;
if(e)
(e).preventDefault();
self.createReleases();
self.options_container.inject(self.movie, 'top'); self.options_container.inject(self.movie, 'top');
self.movie.slide('in', self.options_container); self.movie.slide('in', self.options_container);
}, },
showHelper: function(e){ showHelper: function(e){
@@ -236,19 +311,34 @@ MA.Release = new Class({
if(e) if(e)
(e).preventDefault(); (e).preventDefault();
self.createReleases(); var has_available = false,
self.trynext_container = new Element('div.buttons.trynext').inject(self.movie.info_container); has_snatched = false;
if(self.next_release || self.last_release){ self.movie.data.releases.each(function(release){
if(has_available && has_snatched) return;
var status = Status.get(release.status_id);
if(['snatched', 'downloaded', 'seeding'].contains(status.identifier))
has_snatched = true;
if(['available'].contains(status.identifier))
has_available = true;
});
if(has_available || has_snatched){
self.trynext_container = new Element('div.buttons.trynext').inject(self.movie.info_container);
self.trynext_container.adopt( self.trynext_container.adopt(
self.next_release ? [new Element('a.icon.readd', { has_available ? [new Element('a.icon2.readd', {
'text': self.last_release ? 'Download another release' : 'Download the best release', 'text': has_snatched ? 'Download another release' : 'Download the best release',
'events': { 'events': {
'click': self.tryNextRelease.bind(self) 'click': self.tryNextRelease.bind(self)
} }
}), }),
new Element('a.icon.download', { new Element('a.icon2.download', {
'text': 'pick one yourself', 'text': 'pick one yourself',
'events': { 'events': {
'click': function(){ 'click': function(){
@@ -256,27 +346,10 @@ MA.Release = new Class({
} }
} }
})] : null, })] : null,
new Element('a.icon.completed', { new Element('a.icon2.completed', {
'text': 'mark this movie done', 'text': 'mark this movie done',
'events': { 'events': {
'click': function(){ 'click': self.markMovieDone.bind(self)
Api.request('movie.delete', {
'data': {
'id': self.movie.get('id'),
'delete_from': 'wanted'
},
'onComplete': function(){
var movie = $(self.movie);
movie.set('tween', {
'duration': 300,
'onComplete': function(){
self.movie.destroy()
}
});
movie.tween('height', 0);
}
});
}
} }
}) })
) )
@@ -292,21 +365,27 @@ MA.Release = new Class({
var self = this; var self = this;
var release_el = self.release_container.getElement('#release_'+release.id), var release_el = self.release_container.getElement('#release_'+release.id),
icon = release_el.getElement('.download.icon'); icon = release_el.getElement('.download.icon2');
self.movie.busy(true); if(icon)
icon.addClass('icon spinner').removeClass('download');
Api.request('release.download', { Api.request('release.download', {
'data': { 'data': {
'id': release.id 'id': release.id
}, },
'onComplete': function(json){ 'onComplete': function(json){
self.movie.busy(false); if(icon)
icon.removeClass('icon spinner');
if(json.success) if(json.success){
icon.addClass('completed'); if(icon)
icon.addClass('completed');
release_el.getElement('.release_status').set('text', 'snatched');
}
else else
icon.addClass('attention').set('title', 'Something went wrong when downloading, please check logs.'); if(icon)
icon.addClass('attention').set('title', 'Something went wrong when downloading, please check logs.');
} }
}); });
}, },
@@ -317,29 +396,52 @@ MA.Release = new Class({
Api.request('release.ignore', { Api.request('release.ignore', {
'data': { 'data': {
'id': release.id 'id': release.id
},
'onComplete': function(){
var el = release.el;
if(el && (el.hasClass('failed') || el.hasClass('ignored'))){
el.removeClass('failed').removeClass('ignored');
el.getElement('.release_status').set('text', 'available');
}
else if(el) {
el.addClass('ignored');
el.getElement('.release_status').set('text', 'ignored');
}
} }
}) })
}, },
markMovieDone: function(){
var self = this;
Api.request('movie.delete', {
'data': {
'id': self.movie.get('id'),
'delete_from': 'wanted'
},
'onComplete': function(){
var movie = $(self.movie);
movie.set('tween', {
'duration': 300,
'onComplete': function(){
self.movie.destroy()
}
});
movie.tween('height', 0);
}
});
},
tryNextRelease: function(movie_id){ tryNextRelease: function(movie_id){
var self = this; var self = this;
self.createReleases(); Api.request('movie.searcher.try_next', {
'data': {
if(self.last_release) 'id': self.movie.get('id')
self.ignore(self.last_release); }
});
if(self.next_release)
self.download(self.next_release);
},
trySameRelease: function(movie_id){
var self = this;
if(self.last_release)
self.download(self.last_release);
} }
@@ -354,7 +456,7 @@ MA.Trailer = new Class({
var self = this; var self = this;
self.el = new Element('a.trailer', { self.el = new Element('a.trailer', {
'title': 'Watch the trailer of ' + self.movie.getTitle(), 'title': 'Watch the trailer of ' + self.getTitle(),
'events': { 'events': {
'click': self.watch.bind(self) 'click': self.watch.bind(self)
} }
@@ -365,14 +467,14 @@ MA.Trailer = new Class({
watch: function(offset){ watch: function(offset){
var self = this; var self = this;
var data_url = 'http://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18' var data_url = 'https://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18'
var url = data_url.substitute({ var url = data_url.substitute({
'title': encodeURI(self.movie.getTitle()), 'title': encodeURI(self.getTitle()),
'year': self.movie.get('year'), 'year': self.get('year'),
'offset': offset || 1 'offset': offset || 1
}), }),
size = $(self.movie).getSize(), size = $(self.movie).getSize(),
height = (size.x/16)*9, height = self.options.height || (size.x/16)*9,
id = 'trailer-'+randomString(); id = 'trailer-'+randomString();
self.player_container = new Element('div[id='+id+']'); self.player_container = new Element('div[id='+id+']');
@@ -478,6 +580,11 @@ MA.Edit = new Class({
self.profile_select = new Element('select', { self.profile_select = new Element('select', {
'name': 'profile' 'name': 'profile'
}), }),
self.category_select = new Element('select', {
'name': 'category'
}).grab(
new Element('option', {'value': -1, 'text': 'None'})
),
new Element('a.button.edit', { new Element('a.button.edit', {
'text': 'Save & Search', 'text': 'Save & Search',
'events': { 'events': {
@@ -497,7 +604,34 @@ MA.Edit = new Class({
}); });
Quality.getActiveProfiles().each(function(profile){ // Fill categories
var categories = CategoryList.getAll();
if(categories.length == 0)
self.category_select.hide();
else {
self.category_select.show();
categories.each(function(category){
var category_id = category.data.id;
new Element('option', {
'value': category_id,
'text': category.data.label
}).inject(self.category_select);
if(self.movie.category && self.movie.category.data && self.movie.category.data.id == category_id)
self.category_select.set('value', category_id);
});
}
// Fill profiles
var profiles = Quality.getActiveProfiles();
if(profiles.length == 1)
self.profile_select.hide();
profiles.each(function(profile){
var profile_id = profile.id ? profile.id : profile.data.id; var profile_id = profile.id ? profile.id : profile.data.id;
@@ -506,8 +640,9 @@ MA.Edit = new Class({
'text': profile.label ? profile.label : profile.data.label 'text': profile.label ? profile.label : profile.data.label
}).inject(self.profile_select); }).inject(self.profile_select);
if(self.movie.profile && self.movie.profile.data && self.movie.profile.data.id == profile_id) if(self.movie.get('profile_id') == profile_id)
self.profile_select.set('value', profile_id); self.profile_select.set('value', profile_id);
}); });
} }
@@ -523,7 +658,8 @@ MA.Edit = new Class({
'data': { 'data': {
'id': self.movie.get('id'), 'id': self.movie.get('id'),
'default_title': self.title_select.get('value'), 'default_title': self.title_select.get('value'),
'profile_id': self.profile_select.get('value') 'profile_id': self.profile_select.get('value'),
'category_id': self.category_select.get('value')
}, },
'useSpinner': true, 'useSpinner': true,
'spinnerTarget': $(self.movie), 'spinnerTarget': $(self.movie),
@@ -654,6 +790,7 @@ MA.Delete = new Class({
var self = this; var self = this;
(e).preventDefault(); (e).preventDefault();
self.movie.removeView();
self.movie.slide('out'); self.movie.slide('out');
}, },
@@ -702,16 +839,45 @@ MA.Files = new Class({
self.el = new Element('a.directory', { self.el = new Element('a.directory', {
'title': 'Available files', 'title': 'Available files',
'events': { 'events': {
'click': self.showFiles.bind(self) 'click': self.show.bind(self)
} }
}); });
}, },
showFiles: function(e){ show: function(e){
var self = this; var self = this;
(e).preventDefault(); (e).preventDefault();
if(self.releases)
self.showFiles();
else {
self.movie.busy(true);
Api.request('release.for_movie', {
'data': {
'id': self.movie.data.id
},
'onComplete': function(json){
self.movie.busy(false, 1);
if(json && json.releases){
self.releases = json.releases;
self.showFiles();
}
else
alert('Something went wrong, check the logs.');
}
});
}
},
showFiles: function(){
var self = this;
if(!self.options_container){ if(!self.options_container){
self.options_container = new Element('div.options').adopt( self.options_container = new Element('div.options').adopt(
self.files_container = new Element('div.files.table') self.files_container = new Element('div.files.table')
@@ -724,7 +890,7 @@ MA.Files = new Class({
new Element('span.is_available', {'text': 'Available'}) new Element('span.is_available', {'text': 'Available'})
).inject(self.files_container) ).inject(self.files_container)
Array.each(self.movie.data.releases, function(release){ Array.each(self.releases, function(release){
var rel = new Element('div.release').inject(self.files_container); var rel = new Element('div.release').inject(self.files_container);
@@ -8,12 +8,23 @@
.movies > div { .movies > div {
clear: both; clear: both;
} }
.movies > div .message {
display: block;
padding: 20px;
font-size: 20px;
color: white;
text-align: center;
}
.movies > div .message a {
padding: 20px;
display: block;
}
.movies.thumbs_list > div:not(.description) { .movies.thumbs_list > div:not(.description) {
margin-right: -4px; margin-right: -4px;
text-align: center;
} }
.movies .loading { .movies .loading {
display: block; display: block;
padding: 20px 0 0 0; padding: 20px 0 0 0;
@@ -32,26 +43,26 @@
margin-top: -20px; margin-top: -20px;
overflow: hidden; overflow: hidden;
} }
.movies .loading .spinner { .movies .loading .spinner {
display: inline-block; display: inline-block;
} }
.movies .loading .message { .movies .loading .message {
margin: 0 20px; margin: 0 20px;
} }
.movies h2 { .movies h2 {
margin-bottom: 20px; margin-bottom: 20px;
} }
@media all and (max-width: 480px) { @media all and (max-width: 480px) {
.movies h2 { .movies h2 {
font-size: 25px; font-size: 25px;
margin-bottom: 10px; margin-bottom: 10px;
} }
} }
.movies > .description { .movies > .description {
position: absolute; position: absolute;
top: 30px; top: 30px;
@@ -62,41 +73,53 @@
.movies:hover > .description { .movies:hover > .description {
opacity: 1; opacity: 1;
} }
@media all and (max-width: 860px) { @media all and (max-width: 860px) {
.movies > .description { .movies > .description {
display: none; display: none;
} }
} }
.movies.thumbs_list { .movies.thumbs_list {
padding: 20px 0 20px; padding: 20px 0 20px;
} }
.home .movies { .home .movies {
padding-top: 6px; padding-top: 6px;
} }
.movies .movie { .movies .movie {
position: relative; position: relative;
border-radius: 4px;
margin: 10px 0; margin: 10px 0;
padding-left: 20px; padding-left: 20px;
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
height: 180px; height: 180px;
transition: all 0.6s cubic-bezier(0.9,0,0.1,1); transition: all 0.6s cubic-bezier(0.9,0,0.1,1);
transition-property: width, height;
background: rgba(0,0,0,.2);
} }
.movies.mass_edit_list .movie {
padding-left: 22px;
background: none;
}
.movies.details_list .movie { .movies.details_list .movie {
padding-left: 120px; padding-left: 120px;
} }
.movies.list_list .movie:not(.details_view), .movies.list_list .movie:not(.details_view),
.movies.mass_edit_list .movie { .movies.mass_edit_list .movie {
height: 32px; height: 30px;
border-bottom: 1px solid rgba(255,255,255,.15);
} }
.movies.list_list .movie:last-child,
.movies.mass_edit_list .movie:last-child {
border: none;
}
.movies.thumbs_list .movie { .movies.thumbs_list .movie {
width: 16.66667%; width: 16.66667%;
height: auto; height: auto;
@@ -104,11 +127,8 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
vertical-align: top; vertical-align: top;
border-radius: 0;
box-shadow: none;
border: 0;
} }
@media all and (max-width: 800px) { @media all and (max-width: 800px) {
.movies.thumbs_list .movie { .movies.thumbs_list .movie {
width: 25%; width: 25%;
@@ -125,16 +145,7 @@
.movies.list_list .movie:not(.details_view), .movies.list_list .movie:not(.details_view),
.movies.mass_edit_list .movie { .movies.mass_edit_list .movie {
margin: 1px 0; margin: 0;
border-radius: 0;
background: no-repeat;
box-shadow: none;
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.movies.list_list .movie:hover:not(.details_view),
.movies.mass_edit_list .movie {
background: rgba(255,255,255,0.03);
} }
.movies .data { .movies .data {
@@ -142,19 +153,19 @@
height: 100%; height: 100%;
width: 100%; width: 100%;
position: relative; position: relative;
right: 0;
border-radius: 0;
transition: all .6s cubic-bezier(0.9,0,0.1,1); transition: all .6s cubic-bezier(0.9,0,0.1,1);
right: 0;
} }
.movies.list_list .movie:not(.details_view) .data, .movies.list_list .movie:not(.details_view) .data,
.movies.mass_edit_list .movie .data { .movies.mass_edit_list .movie .data {
height: 30px; padding: 0 0 0 10px;
padding: 3px 0 3px 10px;
box-shadow: none;
border: 0; border: 0;
background: #4e5969; background: #4e5969;
} }
.movies.mass_edit_list .movie .data {
padding-left: 8px;
}
.movies.thumbs_list .data { .movies.thumbs_list .data {
position: absolute; position: absolute;
left: 0; left: 0;
@@ -165,15 +176,7 @@
background: none; background: none;
transition: none; transition: none;
} }
.movies.thumbs_list .movie.no_thumbnail .data { background-image: linear-gradient(-30deg, rgba(255, 0, 85, .2) 0,rgba(125, 185, 235, .2) 100%);
}
.movies.thumbs_list .movie.no_thumbnail:nth-child(2n+6) .data { background-image: linear-gradient(-20deg, rgba(125, 0, 215, .2) 0, rgba(4, 55, 5, .7) 100%); }
.movies.thumbs_list .movie.no_thumbnail:nth-child(3n+6) .data { background-image: linear-gradient(-30deg, rgba(155, 0, 85, .2) 0,rgba(25, 185, 235, .7) 100%); }
.movies.thumbs_list .movie.no_thumbnail:nth-child(4n+6) .data { background-image: linear-gradient(-30deg, rgba(115, 5, 235, .2) 0, rgba(55, 180, 5, .7) 100%); }
.movies.thumbs_list .movie.no_thumbnail:nth-child(5n+6) .data { background-image: linear-gradient(-30deg, rgba(35, 15, 215, .2) 0, rgba(135, 215, 115, .7) 100%); }
.movies.thumbs_list .movie.no_thumbnail:nth-child(6n+6) .data { background-image: linear-gradient(-30deg, rgba(35, 15, 215, .2) 0, rgba(135, 15, 115, .7) 100%); }
.movies.thumbs_list .movie:hover .data { .movies.thumbs_list .movie:hover .data {
background: rgba(0,0,0,0.9); background: rgba(0,0,0,0.9);
} }
@@ -201,28 +204,32 @@
line-height: 0; line-height: 0;
overflow: hidden; overflow: hidden;
height: 100%; height: 100%;
border-radius: 4px 0 0 4px;
transition: all .6s cubic-bezier(0.9,0,0.1,1); transition: all .6s cubic-bezier(0.9,0,0.1,1);
background: rgba(0,0,0,.1);
} }
.movies.thumbs_list .poster { .movies.thumbs_list .poster {
position: relative; position: relative;
border-radius: 0;
} }
.movies.list_list .movie:not(.details_view) .poster, .movies.list_list .movie:not(.details_view) .poster,
.movies.mass_edit_list .poster { .movies.mass_edit_list .poster {
width: 20px; width: 20px;
height: 30px; height: 30px;
border-radius: 1px 0 0 1px;
} }
.movies.mass_edit_list .poster { .movies.mass_edit_list .poster {
display: none; display: none;
} }
.movies.thumbs_list .poster { .movies.thumbs_list .poster {
width: 100%; width: 100%;
height: 100%; height: 100%;
transition: none; transition: none;
background: no-repeat center;
background-size: cover;
} }
.movies.thumbs_list .no_thumbnail .empty_file {
width: 100%;
height: 100%;
}
.movies .poster img, .movies .poster img,
.options .poster img { .options .poster img {
@@ -234,9 +241,9 @@
width: 100%; width: 100%;
top: 0; top: 0;
bottom: 0; bottom: 0;
border-radius: 0; opacity: 0;
} }
.movies .info { .movies .info {
position: relative; position: relative;
height: 100%; height: 100%;
@@ -248,31 +255,41 @@
font-weight: bold; font-weight: bold;
margin-bottom: 10px; margin-bottom: 10px;
margin-top: 2px; margin-top: 2px;
left: 0;
top: 0;
width: 100%; width: 100%;
padding-right: 80px; padding-right: 80px;
transition: all 0.2s linear; transition: all 0.2s linear;
height: 35px;
top: -5px;
position: relative;
} }
.movies.list_list .info .title,
.movies.mass_edit_list .info .title {
height: 100%;
top: 0;
margin: 0;
}
.touch_enabled .movies.list_list .info .title {
display: inline-block;
padding-right: 55px;
}
.movies .info .title span { .movies .info .title span {
display: block; display: inline-block;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
width: 100%; width: 100%;
height: 30px; height: 100%;
line-height: 30px; line-height: 30px;
top: -5px;
position: relative;
} }
.movies.thumbs_list .info .title span { .movies.thumbs_list .info .title span {
white-space: normal; white-space: normal;
overflow: auto; overflow: auto;
height: auto; height: auto;
text-align: left; text-align: left;
} }
@media all and (max-width: 480px) { @media all and (max-width: 480px) {
.movies.thumbs_list .movie .info .title span, .movies.thumbs_list .movie .info .title span,
.movies.thumbs_list .movie .info .year { .movies.thumbs_list .movie .info .year {
@@ -281,32 +298,33 @@
overflow: hidden; overflow: hidden;
} }
} }
.movies.list_list .movie:not(.details_view) .info .title, .movies.list_list .movie:not(.details_view) .info .title,
.movies.mass_edit_list .info .title { .movies.mass_edit_list .info .title {
font-size: 16px; font-size: 16px;
font-weight: normal; font-weight: normal;
width: auto; width: auto;
} }
.movies.thumbs_list .movie:not(.no_thumbnail) .info { .movies.thumbs_list .movie:not(.no_thumbnail) .info {
display: none; display: none;
} }
.movies.thumbs_list .movie:hover .info { .movies.thumbs_list .movie:hover .info {
display: block; display: block;
} }
.movies.thumbs_list .info .title { .movies.thumbs_list .info .title {
font-size: 21px; font-size: 21px;
word-wrap: break-word; word-wrap: break-word;
padding: 0; padding: 0;
height: 100%;
} }
.movies .info .year { .movies .info .year {
position: absolute; position: absolute;
color: #bbb; color: #bbb;
right: 0; right: 0;
top: 1px; top: 6px;
text-align: right; text-align: right;
transition: all 0.2s linear; transition: all 0.2s linear;
font-weight: normal; font-weight: normal;
@@ -316,7 +334,7 @@
font-size: 1.25em; font-size: 1.25em;
right: 10px; right: 10px;
} }
.movies.thumbs_list .info .year { .movies.thumbs_list .info .year {
font-size: 23px; font-size: 23px;
margin: 0; margin: 0;
@@ -327,6 +345,10 @@
color: #FFF; color: #FFF;
} }
.touch_enabled .movies.list_list .movie .info .year {
font-size: 1em;
}
.movies .info .description { .movies .info .description {
top: 30px; top: 30px;
clear: both; clear: both;
@@ -349,13 +371,24 @@
display: block; display: block;
min-height: 20px; min-height: 20px;
} }
.movies.list_list .movie:hover .data .quality {
display: none;
}
.touch_enabled .movies.list_list .movie .data .quality {
position: relative;
display: inline-block;
margin: 0;
top: -4px;
}
@media all and (max-width: 480px) { @media all and (max-width: 480px) {
.movies .data .quality { .movies .data .quality {
display: none; display: none;
} }
} }
.movies .status_suggest .data .quality, .movies .status_suggest .data .quality,
.movies.thumbs_list .data .quality { .movies.thumbs_list .data .quality {
display: none; display: none;
@@ -382,17 +415,19 @@
right: 0; right: 0;
margin-right: 60px; margin-right: 60px;
z-index: 1; z-index: 1;
top: 5px;
} }
.movies .data .quality .available, .movies .data .quality .available,
.movies .data .quality .snatched { .movies .data .quality .snatched {
opacity: 1; opacity: 1;
box-shadow: 1px 1px 0 rgba(0,0,0,0.2);
cursor: pointer; cursor: pointer;
} }
.movies .data .quality .available { background-color: #578bc3; } .movies .data .quality .available { background-color: #578bc3; }
.movies .data .quality .snatched { background-color: #369545; } .movies .data .quality .failed { background-color: #a43d34; }
.movies .data .quality .snatched { background-color: #a2a232; }
.movies .data .quality .seeding { background-color: #0a6819; }
.movies .data .quality .done { .movies .data .quality .done {
background-color: #369545; background-color: #369545;
opacity: 1; opacity: 1;
@@ -407,59 +442,71 @@
.movies .data .actions { .movies .data .actions {
position: absolute; position: absolute;
bottom: 20px; bottom: 17px;
right: 20px; right: 20px;
line-height: 0; line-height: 0;
top: 0; top: 0;
display: block;
width: auto;
opacity: 0; opacity: 0;
display: none; display: none;
width: 0;
} }
@media all and (max-width: 480px) { @media all and (max-width: 480px) {
.movies .data .actions { .movies .data .actions {
display: none !important; display: none !important;
} }
} }
.movies .movie:hover .data .actions { .movies .movie:hover .data .actions,
.touch_enabled .movies .movie .data .actions {
opacity: 1; opacity: 1;
display: inline-block; display: inline-block;
width: auto;
} }
.movies.details_list .data .actions { .movies.details_list .data .actions {
top: auto; top: auto;
bottom: 18px; bottom: 18px;
} }
.movies .movie:hover .actions { .movies .movie:hover .actions {
opacity: 1; opacity: 1;
display: inline-block; display: inline-block;
} }
.movies.thumbs_list .data .actions { .movies.thumbs_list .data .actions {
bottom: 2px; bottom: 12px;
right: 10px; right: 10px;
top: auto; top: auto;
} }
.movies .movie:hover .action { opacity: 0.6; } .movies .movie:hover .action { opacity: 0.6; }
.movies .movie:hover .action:hover { opacity: 1; } .movies .movie:hover .action:hover { opacity: 1; }
.movies .data .action { .movies .data .action {
background-repeat: no-repeat;
background-position: center;
display: inline-block; display: inline-block;
width: 26px; height: 22px;
height: 26px; min-width: 33px;
padding: 3px; padding: 0 5px;
line-height: 26px;
text-align: center;
font-size: 13px;
color: #FFF;
margin-left: 1px;
} }
.movies .data .action.trailer { color: #FFF; }
.movies .data .action.download { color: #b9dec0; }
.movies .data .action.edit { color: #c6b589; }
.movies .data .action.refresh { color: #cbeecc; }
.movies .data .action.delete { color: #e9b0b0; }
.movies .data .action.directory { color: #ffed92; }
.movies .data .action.readd { color: #c2fac5; }
.movies.mass_edit_list .movie .data .actions { .movies.mass_edit_list .movie .data .actions {
display: none; display: none;
} }
.movies.list_list .movie:not(.details_view):hover .actions, .movies.list_list .movie:not(.details_view):hover .actions,
.movies.mass_edit_list .movie:hover .actions { .movies.mass_edit_list .movie:hover .actions,
.touch_enabled .movies.list_list .movie:not(.details_view) .actions {
margin: 0; margin: 0;
background: #4e5969; background: #4e5969;
top: 2px; top: 2px;
@@ -473,12 +520,10 @@
text-align: center; text-align: center;
font-size: 20px; font-size: 20px;
position: absolute; position: absolute;
padding: 70px 0 0; padding: 80px 0 0;
left: 120px; left: 120px;
right: 0; right: 0;
} }
.movies .delete_container .cancel {
}
.movies .delete_container .or { .movies .delete_container .or {
padding: 10px; padding: 10px;
} }
@@ -498,9 +543,9 @@
} }
.movies .options .form { .movies .options .form {
margin: 70px 20px 0; margin: 80px 0 0;
float: left;
font-size: 20px; font-size: 20px;
text-align: center;
} }
.movies .options .form select { .movies .options .form select {
@@ -510,16 +555,21 @@
.movies .options .table { .movies .options .table {
height: 180px; height: 180px;
overflow: auto; overflow: auto;
line-height: 2em;
} }
.movies .options .table .item { .movies .options .table .item {
border-bottom: 1px solid rgba(255,255,255,0.1); border-bottom: 1px solid rgba(255,255,255,0.1);
} }
.movies .options .table .item.ignored span { .movies .options .table .item.ignored span,
.movies .options .table .item.failed span {
text-decoration: line-through; text-decoration: line-through;
color: rgba(255,255,255,0.4); color: rgba(255,255,255,0.4);
} }
.movies .options .table .item.ignored .delete { .movies .options .table .item.ignored .delete:before,
background-image: url('../images/icon.undo.png'); .movies .options .table .item.failed .delete:before {
display: inline-block;
content: "\e04b";
transform: scale(-1, 1);
} }
.movies .options .table .item:last-child { border: 0; } .movies .options .table .item:last-child { border: 0; }
@@ -565,10 +615,13 @@
width: 30px !important; width: 30px !important;
height: 20px; height: 20px;
opacity: 0.8; opacity: 0.8;
line-height: 25px;
} }
.movies .options .table a:hover { .movies .options .table a:hover { opacity: 1; }
opacity: 1; .movies .options .table a.download { color: #a7fbaf; }
} .movies .options .table a.delete { color: #fda3a3; }
.movies .options .table .ignored a.delete,
.movies .options .table .failed a.delete { color: #b5fda3; }
.movies .options .table .head > * { .movies .options .table .head > * {
font-weight: bold; font-weight: bold;
@@ -578,7 +631,7 @@
height: auto; height: auto;
} }
.movies .movie .trailer_container { .trailer_container {
width: 100%; width: 100%;
background: #000; background: #000;
text-align: center; text-align: center;
@@ -588,11 +641,17 @@
position: absolute; position: absolute;
z-index: 10; z-index: 10;
} }
.movies .movie .trailer_container.hide { @media only screen and (device-width: 768px) {
.trailer_container iframe {
margin-top: 25px;
}
}
.trailer_container.hide {
height: 0 !important; height: 0 !important;
} }
.movies .movie .hide_trailer { .hide_trailer {
position: absolute; position: absolute;
top: 0; top: 0;
left: 50%; left: 50%;
@@ -601,11 +660,10 @@
text-align: center; text-align: center;
padding: 3px 10px; padding: 3px 10px;
background: #4e5969; background: #4e5969;
border-radius: 0 0 2px 2px;
transition: all .2s cubic-bezier(0.9,0,0.1,1) .2s; transition: all .2s cubic-bezier(0.9,0,0.1,1) .2s;
z-index: 11; z-index: 11;
} }
.movies .movie .hide_trailer.hide { .hide_trailer.hide {
top: -30px; top: -30px;
} }
@@ -637,17 +695,18 @@
.movies .movie .trynext { .movies .movie .trynext {
display: inline; display: inline;
position: absolute; position: absolute;
right: 135px; right: 180px;
z-index: 2; z-index: 2;
opacity: 0; opacity: 0;
background: #4e5969; background: #4e5969;
min-width: 300px;
text-align: right; text-align: right;
height: 100%; height: 100%;
padding: 3px 0;
top: 0; top: 0;
} }
.touch_enabled .movies .movie .trynext {
display: none;
}
@media all and (max-width: 480px) { @media all and (max-width: 480px) {
.movies .movie .trynext { .movies .movie .trynext {
display: none; display: none;
@@ -655,31 +714,42 @@
} }
.movies.mass_edit_list .trynext { display: none; } .movies.mass_edit_list .trynext { display: none; }
.wanted .movies .movie .trynext { .wanted .movies .movie .trynext {
padding-right: 50px; padding-right: 30px;
} }
.movies .movie:hover .trynext { .movies .movie:hover .trynext,
.touch_enabled .movies.details_list .movie .trynext {
opacity: 1; opacity: 1;
} }
.movies.details_list .movie .trynext { .movies.details_list .movie .trynext {
background: #47515f; background: #47515f;
padding: 0; padding: 0;
right: 0; right: 0;
bottom: 35px; height: 25px;
height: auto;
} }
.movies .movie .trynext a { .movies .movie .trynext a {
background-position: 5px center; background-position: 5px center;
padding: 0 5px 0 25px; padding: 0 5px 0 25px;
margin-right: 10px; margin-right: 10px;
color: #FFF; color: #FFF;
border-radius: 2px; height: 100%;
line-height: 27px;
font-family: OpenSans, "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif;
} }
.movies .movie .trynext a:last-child { .movies .movie .trynext a:before {
margin: 0; margin: 2px 0 0 -20px;
position: absolute;
font-family: 'Elusive-Icons';
} }
.movies .movie .trynext a:hover { .movies.details_list .movie .trynext a {
line-height: 23px;
}
.movies .movie .trynext a:last-child {
margin: 0;
}
.movies .movie .trynext a:hover,
.touch_enabled .movies .movie .trynext a {
background-color: #369545; background-color: #369545;
} }
@@ -694,14 +764,7 @@
} }
.movies .alph_nav { .movies .alph_nav {
transition: box-shadow .4s linear; height: 44px;
position: relative;
z-index: 4;
top: 0px;
right: 0;
margin: 0 auto;
width: 100%;
padding: 10px 0;
} }
@media all and (max-width: 480px) { @media all and (max-width: 480px) {
@@ -710,12 +773,9 @@
} }
} }
.movies .alph_nav > div { .movies .alph_nav .menus {
position: relative; display: inline-block;
max-width: 980px; float: right;
margin: 0 auto;
padding: 0;
min-height: 24px;
} }
.movies .alph_nav .numbers, .movies .alph_nav .numbers,
@@ -724,118 +784,152 @@
list-style: none; list-style: none;
padding: 0 0 1px; padding: 0 0 1px;
margin: 0; margin: 0;
float: left;
user-select: none; user-select: none;
} }
.movies .alph_nav .counter { .movies .alph_nav .counter {
display: inline-block;
text-align: right; text-align: right;
position: absolute; padding: 0 10px;
right: 270px; height: 100%;
background: #4e5969; line-height: 43px;
padding: 4px 10px; border-right: 1px solid rgba(255,255,255,.07);
} }
.movies .alph_nav .numbers li, .movies .alph_nav .numbers li,
.movies .alph_nav .actions li { .movies .alph_nav .actions li {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
height: 24px; height: 100%;
line-height: 23px; line-height: 30px;
text-align: center; text-align: center;
cursor: pointer;
color: rgba(255,255,255,0.2);
border: 1px solid transparent; border: 1px solid transparent;
transition: all 0.1s ease-in-out; transition: all 0.1s ease-in-out;
} }
@media all and (max-width: 900px) {
.movies .alph_nav .numbers {
display: none;
}
}
.movies .alph_nav .numbers li { .movies .alph_nav .numbers li {
width: auto; width: 30px;
padding: 0 4px; height: 30px;
opacity: 0.3;
} }
.movies .alph_nav .numbers li.letter_all {
width: 60px;
}
.movies .alph_nav li.available { .movies .alph_nav li.available {
color: #FFF; font-weight: bold;
font-weight: bolder; cursor: pointer;
opacity: 1;
} }
.movies .alph_nav li.active.available, .movies .alph_nav li.active.available,
.movies .alph_nav li.available:hover { .movies .alph_nav li.available:hover {
background: rgba(255,255,255,.1); background: rgba(0,0,0,.1);
} }
.movies .alph_nav .search { .movies .alph_nav .search input {
padding: 6px 5px; padding: 6px 5px;
margin: 0 0 0 20px; width: 100%;
position: absolute; height: 44px;
right: 30px; display: inline-block;
width: 154px; border: 0;
height: 25px; background: none;
transition: all 0.6s cubic-bezier(0.9,0,0.1,1); color: #444;
font-size: 14px;
padding: 10px;
padding: 0 10px 0 30px;
border-bottom: 1px solid rgba(0,0,0,.08);
} }
.movies .alph_nav .search input:focus {
background: rgba(0,0,0,.08);
}
.movies .alph_nav .search input::-webkit-input-placeholder {
color: #444;
opacity: .6;
}
.movies .alph_nav .search:before {
font-family: 'Elusive-Icons';
content: "\e03e";
position: absolute;
height: 20px;
line-height: 45px;
font-size: 12px;
margin: 0 0 0 10px;
opacity: .6;
color: #444;
}
.movies .alph_nav .actions { .movies .alph_nav .actions {
margin: 0 6px 0 0;
-moz-user-select: none; -moz-user-select: none;
position: absolute; width: 44px;
right: 183px; height: 44px;
display: inline-block;
vertical-align: top;
z-index: 200;
position: relative;
border: 1px solid rgba(255,255,255,.07);
border-width: 0 1px;
} }
.movies .alph_nav .actions:hover {
box-shadow: 0 100px 20px -10px rgba(0,0,0,0.55);
}
.movies .alph_nav .actions li { .movies .alph_nav .actions li {
border-radius: 1px; width: 100%;
width: auto; height: 45px;
line-height: 40px;
position: relative;
z-index: 20;
display: none;
cursor: pointer;
} }
.movies .alph_nav .actions li.active { .movies .alph_nav .actions:hover li:not(.active) {
background: none; display: block;
border: 1px solid transparent; background: #FFF;
box-shadow: none; color: #444;
} }
.movies .alph_nav .actions li span { .movies .alph_nav .actions li:hover:not(.active) {
display: block; background: #ccc;
background: url('../images/sprite.png') no-repeat; }
width: 25px; .movies .alph_nav .actions li.active {
height: 100%; display: block;
}
.movies .alph_nav .actions li.mass_edit span {
background-position: 3px 3px;
} }
.movies .alph_nav .actions li.list span { .movies .alph_nav .actions li.mass_edit:before {
background-position: 3px -95px; content: "\e070";
} }
.movies .alph_nav .actions li.details span { .movies .alph_nav .actions li.list:before {
background-position: 3px -74px; content: "\e0d8";
} }
.movies .alph_nav .actions li:first-child { .movies .alph_nav .actions li.details:before {
border-radius: 3px 0 0 3px; content: "\e022";
} }
.movies .alph_nav .actions li:last-child {
border-radius: 0 3px 3px 0;
}
.movies .alph_nav .mass_edit_form { .movies .alph_nav .mass_edit_form {
clear: both; clear: both;
text-align: center; text-align: center;
display: none; display: none;
overflow: hidden;
float: left;
height: 44px;
line-height: 44px;
} }
.movies.mass_edit_list .mass_edit_form { .movies.mass_edit_list .mass_edit_form {
display: block; display: inline-block;
} }
.movies.mass_edit_list .mass_edit_form .select { .movies.mass_edit_list .mass_edit_form .select {
float: left;
margin: 5px 0 0 5px;
font-size: 14px; font-size: 14px;
display: inline-block;
} }
.movies.mass_edit_list .mass_edit_form .select span { .movies.mass_edit_list .mass_edit_form .select .check {
display: inline-block;
vertical-align: middle; vertical-align: middle;
margin: -4px 0 0 5px;
}
.movies.mass_edit_list .mass_edit_form .select span {
opacity: 0.7; opacity: 0.7;
} }
.movies.mass_edit_list .mass_edit_form .select .count { .movies.mass_edit_list .mass_edit_form .select .count {
@@ -844,8 +938,7 @@
} }
.movies .alph_nav .mass_edit_form .quality { .movies .alph_nav .mass_edit_form .quality {
float: left; display: inline-block;
padding: 8px 0 0;
margin: 0 0 0 16px; margin: 0 0 0 16px;
} }
.movies .alph_nav .mass_edit_form .quality select { .movies .alph_nav .mass_edit_form .quality select {
@@ -858,8 +951,8 @@
.movies .alph_nav .mass_edit_form .refresh, .movies .alph_nav .mass_edit_form .refresh,
.movies .alph_nav .mass_edit_form .delete { .movies .alph_nav .mass_edit_form .delete {
float: left; display: inline-block;
padding: 8px 0 0 8px; margin-left: 8px;
} }
.movies .alph_nav .mass_edit_form .refresh span, .movies .alph_nav .mass_edit_form .refresh span,
@@ -867,28 +960,48 @@
margin: 0 10px 0 0; margin: 0 10px 0 0;
} }
.movies .alph_nav .more_menu {
right: 0;
position: absolute;
}
.movies .alph_nav .more_menu > a { .movies .alph_nav .more_menu > a {
background-color: #4e5969; background: none;
background-position: center -158px;
} }
.movies .alph_nav .more_menu.extra > a:before {
content: '...';
font-size: 1.7em;
line-height: 23px;
text-align: center;
display: block;
}
.movies .alph_nav .more_menu.filter {
}
.movies .alph_nav .more_menu.filter > a:before {
content: "\e0e8";
font-family: 'Elusive-Icons';
line-height: 33px;
display: block;
text-align: center;
}
.movies .alph_nav .more_menu.filter .wrapper {
right: 88px;
width: 300px;
}
.movies .empty_wanted { .movies .empty_wanted {
background-image: url('../images/emptylist.png'); background-image: url('../images/emptylist.png');
background-position: 80% 0;
height: 750px; height: 750px;
width: 800px; width: 100%;
max-width: 900px;
padding-top: 260px; padding-top: 260px;
margin-top: -50px;
} }
.movies .empty_manage { .movies .empty_manage {
text-align: center; text-align: center;
font-size: 25px; font-size: 25px;
line-height: 150%; line-height: 150%;
padding: 40px 0;
} }
.movies .empty_manage .after_manage { .movies .empty_manage .after_manage {
@@ -897,7 +1010,6 @@
} }
.movies .progress { .movies .progress {
border-radius: 2px;
padding: 10px; padding: 10px;
margin: 5px 0; margin: 5px 0;
text-align: left; text-align: left;
@@ -912,7 +1024,6 @@
width: 49%; width: 49%;
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
margin: 2px 0.5%; margin: 2px 0.5%;
border-radius: 3px;
} }
.movies .progress > div .folder { .movies .progress > div .folder {
@@ -11,9 +11,10 @@ var Movie = new Class({
self.view = options.view || 'details'; self.view = options.view || 'details';
self.list = list; self.list = list;
self.el = new Element('div.movie.inlay'); self.el = new Element('div.movie');
self.profile = Quality.getProfile(data.profile_id) || {}; self.profile = Quality.getProfile(data.profile_id) || {};
self.category = CategoryList.getCategory(data.category_id) || {};
self.parent(self, options); self.parent(self, options);
self.addEvents(); self.addEvents();
@@ -23,18 +24,19 @@ var Movie = new Class({
var self = this; var self = this;
App.addEvent('movie.update.'+self.data.id, function(notification){ App.addEvent('movie.update.'+self.data.id, function(notification){
self.busy(false) self.busy(false);
self.removeView();
self.update.delay(2000, self, notification); self.update.delay(2000, self, notification);
}); });
['movie.busy', 'searcher.started'].each(function(listener){ ['movie.busy', 'movie.searcher.started'].each(function(listener){
App.addEvent(listener+'.'+self.data.id, function(notification){ App.addEvent(listener+'.'+self.data.id, function(notification){
if(notification.data) if(notification.data)
self.busy(true) self.busy(true)
}); });
}) })
App.addEvent('searcher.ended.'+self.data.id, function(notification){ App.addEvent('movie.searcher.ended.'+self.data.id, function(notification){
if(notification.data) if(notification.data)
self.busy(false) self.busy(false)
}); });
@@ -51,12 +53,12 @@ var Movie = new Class({
// Remove events // Remove events
App.removeEvents('movie.update.'+self.data.id); App.removeEvents('movie.update.'+self.data.id);
['movie.busy', 'searcher.started'].each(function(listener){ ['movie.busy', 'movie.searcher.started'].each(function(listener){
App.removeEvents(listener+'.'+self.data.id); App.removeEvents(listener+'.'+self.data.id);
}) })
}, },
busy: function(set_busy){ busy: function(set_busy, timeout){
var self = this; var self = this;
if(!set_busy){ if(!set_busy){
@@ -70,9 +72,9 @@ var Movie = new Class({
self.spinner.el.destroy(); self.spinner.el.destroy();
self.spinner = null; self.spinner = null;
self.mask = null; self.mask = null;
}, 400); }, timeout || 400);
} }
}, 1000) }, timeout || 1000)
} }
else if(!self.spinner) { else if(!self.spinner) {
self.createMask(); self.createMask();
@@ -107,8 +109,10 @@ var Movie = new Class({
self.data = notification.data; self.data = notification.data;
self.el.empty(); self.el.empty();
self.removeView();
self.profile = Quality.getProfile(self.data.profile_id) || {}; self.profile = Quality.getProfile(self.data.profile_id) || {};
self.category = CategoryList.getCategory(self.data.category_id) || {};
self.create(); self.create();
self.busy(false); self.busy(false);
@@ -139,9 +143,6 @@ var Movie = new Class({
'text': self.data.library.year || 'n/a' 'text': self.data.library.year || 'n/a'
}) })
), ),
self.rating = new Element('div.rating.icon', {
'text': self.data.library.rating
}),
self.description = new Element('div.description', { self.description = new Element('div.description', {
'text': self.data.library.plot 'text': self.data.library.plot
}), }),
@@ -149,8 +150,8 @@ var Movie = new Class({
'events': { 'events': {
'click': function(e){ 'click': function(e){
var releases = self.el.getElement('.actions .releases'); var releases = self.el.getElement('.actions .releases');
if(releases) if(releases.isVisible())
releases.fireEvent('click', [e]) releases.fireEvent('click', [e])
} }
} }
}) })
@@ -178,20 +179,21 @@ var Movie = new Class({
}); });
// Add releases // Add releases
self.data.releases.each(function(release){ if(self.data.releases)
self.data.releases.each(function(release){
var q = self.quality.getElement('.q_id'+ release.quality_id), var q = self.quality.getElement('.q_id'+ release.quality_id),
status = Status.get(release.status_id); status = Status.get(release.status_id);
if(!q && (status.identifier == 'snatched' || status.identifier == 'done')) if(!q && (status.identifier == 'snatched' || status.identifier == 'done'))
var q = self.addQuality(release.quality_id) var q = self.addQuality(release.quality_id)
if (status && q && !q.hasClass(status.identifier)){ if (status && q && !q.hasClass(status.identifier)){
q.addClass(status.identifier); q.addClass(status.identifier);
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status.label) q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status.label)
} }
}); });
Object.each(self.options.actions, function(action, key){ Object.each(self.options.actions, function(action, key){
self.action[key.toLowerCase()] = action = new self.options.actions[key](self) self.action[key.toLowerCase()] = action = new self.options.actions[key](self)
@@ -199,9 +201,6 @@ var Movie = new Class({
self.actions.adopt(action) self.actions.adopt(action)
}); });
if(!self.data.library.rating)
self.rating.hide();
}, },
addQuality: function(quality_id){ addQuality: function(quality_id){
@@ -244,10 +243,10 @@ var Movie = new Class({
if(direction == 'in'){ if(direction == 'in'){
self.temp_view = self.view; self.temp_view = self.view;
self.changeView('details') self.changeView('details');
self.el.addEvent('outerClick', function(){ self.el.addEvent('outerClick', function(){
self.removeView() self.removeView();
self.slide('out') self.slide('out')
}) })
el.show(); el.show();
@@ -257,7 +256,8 @@ var Movie = new Class({
self.el.removeEvents('outerClick') self.el.removeEvents('outerClick')
setTimeout(function(){ setTimeout(function(){
self.el.getElements('> :not(.data):not(.poster):not(.movie_container)').hide(); if(self.el)
self.el.getElements('> :not(.data):not(.poster):not(.movie_container)').hide();
}, 600); }, 600);
self.data_container.removeClass('hide_right'); self.data_container.removeClass('hide_right');
@@ -267,9 +267,10 @@ var Movie = new Class({
changeView: function(new_view){ changeView: function(new_view){
var self = this; var self = this;
self.el if(self.el)
.removeClass(self.view+'_view') self.el
.addClass(new_view+'_view') .removeClass(self.view+'_view')
.addClass(new_view+'_view')
self.view = new_view; self.view = new_view;
}, },
@@ -44,9 +44,8 @@
.search_form .input input { .search_form .input input {
border-radius: 0; border-radius: 0;
display: block; display: block;
width: 100%;
border: 0; border: 0;
background: rgba(255,255,255,.08); background: none;
color: #FFF; color: #FFF;
font-size: 25px; font-size: 25px;
height: 100%; height: 100%;
@@ -60,6 +59,11 @@
.search_form.shown .input input { .search_form.shown .input input {
opacity: 1; opacity: 1;
} }
.search_form input::-ms-clear {
width : 0;
height: 0;
}
@media all and (max-width: 480px) { @media all and (max-width: 480px) {
.search_form .input input { .search_form .input input {
@@ -106,7 +110,7 @@
background: #5c697b; background: #5c697b;
margin: 4px 0 0; margin: 4px 0 0;
width: 470px; width: 470px;
min-height: 140px; min-height: 50px;
box-shadow: 0 20px 20px -10px rgba(0,0,0,0.55); box-shadow: 0 20px 20px -10px rgba(0,0,0,0.55);
display: none; display: none;
} }
@@ -160,13 +164,15 @@
display: inline-block; display: inline-block;
margin-right: 10px; margin-right: 10px;
} }
.movie_result .options select[name=title] { width: 180px; } .movie_result .options select[name=title] { width: 170px; }
.movie_result .options select[name=profile] { width: 90px; } .movie_result .options select[name=profile] { width: 90px; }
.movie_result .options select[name=category] { width: 80px; }
@media all and (max-width: 480px) { @media all and (max-width: 480px) {
.movie_result .options select[name=title] { width: 90px; } .movie_result .options select[name=title] { width: 90px; }
.movie_result .options select[name=profile] { width: 60px; } .movie_result .options select[name=profile] { width: 50px; }
.movie_result .options select[name=category] { width: 50px; }
} }
@@ -194,7 +200,7 @@
transition: all .4s cubic-bezier(0.9,0,0.1,1); transition: all .4s cubic-bezier(0.9,0,0.1,1);
} }
.movie_result .data.open { .movie_result .data.open {
left: 100%; left: 100% !important;
} }
.movie_result:last-child .data { border-bottom: 0; } .movie_result:last-child .data { border-bottom: 0; }
@@ -218,26 +224,51 @@
position: absolute; position: absolute;
top: 20%; top: 20%;
left: 15px; left: 15px;
right: 60px; right: 7px;
vertical-align: middle; vertical-align: middle;
} }
.movie_result .info h2 { .movie_result .info h2 {
margin: 0;
font-weight: normal; font-weight: normal;
font-size: 20px; font-size: 20px;
padding: 0;
}
.search_form .info h2 {
position: absolute;
width: 100%;
}
.movie_result .info h2 .title {
display: block; display: block;
margin: 0; margin: 0;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
width: 100%;
}
.movie_result .info h2 span {
padding: 0 5px;
position: absolute;
right: -60px;
} }
.search_form .info h2 .title {
position: absolute;
width: 88%;
}
.movie_result .info h2 .year {
padding: 0 5px;
text-align: center;
position: absolute;
width: 12%;
right: 0;
}
@media all and (max-width: 480px) {
.search_form .info h2 .year {
font-size: 12px;
margin-top: 7px;
}
}
.search_form .mask, .search_form .mask,
.movie_result .mask { .movie_result .mask {
@@ -98,6 +98,9 @@ Block.Search = new Class({
self.el[self.q() ? 'addClass' : 'removeClass']('filled') self.el[self.q() ? 'addClass' : 'removeClass']('filled')
if(self.q() != self.last_q){ if(self.q() != self.last_q){
if(self.api_request && self.api_request.isRunning())
self.api_request.cancel();
if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer) if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer)
self.autocomplete_timer = self.autocomplete.delay(300, self) self.autocomplete_timer = self.autocomplete.delay(300, self)
} }
@@ -116,12 +119,9 @@ Block.Search = new Class({
}, },
list: function(){ list: function(){
var self = this; var self = this,
q = self.q(),
if(self.api_request && self.api_request.running) return cache = self.cache[q];
var q = self.q();
var cache = self.cache[q];
self.hideResults(false); self.hideResults(false);
@@ -164,9 +164,6 @@ Block.Search = new Class({
}); });
if(q != self.q())
self.list()
// Calculate result heights // Calculate result heights
var w = window.getSize(), var w = window.getSize(),
rc = self.result_container.getCoordinates(); rc = self.result_container.getCoordinates();
@@ -188,8 +185,11 @@ Block.Search = new Class({
Block.Search.Item = new Class({ Block.Search.Item = new Class({
Implements: [Options, Events],
initialize: function(info, options){ initialize: function(info, options){
var self = this; var self = this;
self.setOptions(options);
self.info = info; self.info = info;
self.alternative_titles = []; self.alternative_titles = [];
@@ -211,18 +211,15 @@ Block.Search.Item = new Class({
}) : null, }) : null,
self.options_el = new Element('div.options.inlay'), self.options_el = new Element('div.options.inlay'),
self.data_container = new Element('div.data', { self.data_container = new Element('div.data', {
'tween': {
duration: 400,
transition: 'quint:in:out'
},
'events': { 'events': {
'click': self.showOptions.bind(self) 'click': self.showOptions.bind(self)
} }
}).adopt( }).adopt(
new Element('div.info').adopt( self.info_container = new Element('div.info').adopt(
self.title = new Element('h2', { new Element('h2').adopt(
'text': info.titles[0] self.title = new Element('span.title', {
}).adopt( 'text': info.titles && info.titles.length > 0 ? info.titles[0] : 'Unknown'
}),
self.year = info.year ? new Element('span.year', { self.year = info.year ? new Element('span.year', {
'text': info.year 'text': info.year
}) : null }) : null
@@ -231,12 +228,12 @@ Block.Search.Item = new Class({
) )
) )
if(info.titles)
info.titles.each(function(title){ info.titles.each(function(title){
self.alternativeTitle({ self.alternativeTitle({
'title': title 'title': title
}); });
}) })
}, },
alternativeTitle: function(alternative){ alternativeTitle: function(alternative){
@@ -245,6 +242,20 @@ Block.Search.Item = new Class({
self.alternative_titles.include(alternative); self.alternative_titles.include(alternative);
}, },
getTitle: function(){
var self = this;
try {
return self.info.original_title ? self.info.original_title : self.info.titles[0];
}
catch(e){
return 'Unknown';
}
},
get: function(key){
return this.info[key]
},
showOptions: function(){ showOptions: function(){
var self = this; var self = this;
@@ -264,7 +275,9 @@ Block.Search.Item = new Class({
add: function(e){ add: function(e){
var self = this; var self = this;
(e).preventDefault();
if(e)
(e).preventDefault();
self.loadingMask(); self.loadingMask();
@@ -272,7 +285,8 @@ Block.Search.Item = new Class({
'data': { 'data': {
'identifier': self.info.imdb, 'identifier': self.info.imdb,
'title': self.title_select.get('value'), 'title': self.title_select.get('value'),
'profile_id': self.profile_select.get('value') 'profile_id': self.profile_select.get('value'),
'category_id': self.category_select.get('value')
}, },
'onComplete': function(json){ 'onComplete': function(json){
self.options_el.empty(); self.options_el.empty();
@@ -282,6 +296,8 @@ Block.Search.Item = new Class({
}) })
); );
self.mask.fade('out'); self.mask.fade('out');
self.fireEvent('added');
}, },
'onFailure': function(){ 'onFailure': function(){
self.options_el.empty(); self.options_el.empty();
@@ -310,10 +326,10 @@ Block.Search.Item = new Class({
self.options_el.grab( self.options_el.grab(
new Element('div', { new Element('div', {
'class': self.info.in_wanted && self.info.in_wanted.profile || in_library ? 'in_library_wanted' : '' 'class': self.info.in_wanted && self.info.in_wanted.profile_id || in_library ? 'in_library_wanted' : ''
}).adopt( }).adopt(
self.info.in_wanted && self.info.in_wanted.profile ? new Element('span.in_wanted', { self.info.in_wanted && self.info.in_wanted.profile_id ? new Element('span.in_wanted', {
'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label 'text': 'Already in wanted list: ' + Quality.getProfile(self.info.in_wanted.profile_id).get('label')
}) : (in_library ? new Element('span.in_library', { }) : (in_library ? new Element('span.in_library', {
'text': 'Already in library: ' + in_library.join(', ') 'text': 'Already in library: ' + in_library.join(', ')
}) : null), }) : null),
@@ -323,7 +339,12 @@ Block.Search.Item = new Class({
self.profile_select = new Element('select', { self.profile_select = new Element('select', {
'name': 'profile' 'name': 'profile'
}), }),
new Element('a.button', { self.category_select = new Element('select', {
'name': 'category'
}).grab(
new Element('option', {'value': -1, 'text': 'None'})
),
self.add_button = new Element('a.button', {
'text': 'Add', 'text': 'Add',
'events': { 'events': {
'click': self.add.bind(self) 'click': self.add.bind(self)
@@ -338,7 +359,28 @@ Block.Search.Item = new Class({
}).inject(self.title_select) }).inject(self.title_select)
}) })
Quality.getActiveProfiles().each(function(profile){
// Fill categories
var categories = CategoryList.getAll();
if(categories.length == 0)
self.category_select.hide();
else {
self.category_select.show();
categories.each(function(category){
new Element('option', {
'value': category.data.id,
'text': category.data.label
}).inject(self.category_select);
});
}
// Fill profiles
var profiles = Quality.getActiveProfiles();
if(profiles.length == 1)
self.profile_select.hide();
profiles.each(function(profile){
new Element('option', { new Element('option', {
'value': profile.id ? profile.id : profile.data.id, 'value': profile.id ? profile.id : profile.data.id,
'text': profile.label ? profile.label : profile.data.label 'text': profile.label ? profile.label : profile.data.label
@@ -346,6 +388,11 @@ Block.Search.Item = new Class({
}); });
self.options_el.addClass('set'); self.options_el.addClass('set');
if(categories.length == 0 && self.title_select.getElements('option').length == 1 && profiles.length == 1 &&
!(self.info.in_wanted && self.info.in_wanted.profile_id || in_library))
self.add();
} }
}, },
@@ -0,0 +1,6 @@
from .main import MovieLibraryPlugin
def start():
return MovieLibraryPlugin()
config = []
@@ -1,9 +1,8 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.helpers.encoding import toUnicode, simplifyString from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.media._base.library import LibraryBase
from couchpotato.core.settings.model import Library, LibraryTitle, File from couchpotato.core.settings.model import Library, LibraryTitle, File
from string import ascii_letters from string import ascii_letters
import time import time
@@ -11,17 +10,20 @@ import traceback
log = CPLog(__name__) log = CPLog(__name__)
class LibraryPlugin(Plugin):
class MovieLibraryPlugin(LibraryBase):
default_dict = {'titles': {}, 'files':{}} default_dict = {'titles': {}, 'files':{}}
def __init__(self): def __init__(self):
addEvent('library.add', self.add) addEvent('library.add.movie', self.add)
addEvent('library.update', self.update) addEvent('library.update.movie', self.update)
addEvent('library.update_release_date', self.updateReleaseDate) addEvent('library.update.movie.release_date', self.updateReleaseDate)
def add(self, attrs = None, update_after = True):
if not attrs: attrs = {}
def add(self, attrs = {}, update_after = True): primary_provider = attrs.get('primary_provider', 'imdb')
db = get_session() db = get_session()
@@ -33,7 +35,8 @@ class LibraryPlugin(Plugin):
identifier = attrs.get('identifier'), identifier = attrs.get('identifier'),
plot = toUnicode(attrs.get('plot')), plot = toUnicode(attrs.get('plot')),
tagline = toUnicode(attrs.get('tagline')), tagline = toUnicode(attrs.get('tagline')),
status_id = status.get('id') status_id = status.get('id'),
info = {}
) )
title = LibraryTitle( title = LibraryTitle(
@@ -49,37 +52,39 @@ class LibraryPlugin(Plugin):
# Update library info # Update library info
if update_after is not False: if update_after is not False:
handle = fireEventAsync if update_after is 'async' else fireEvent handle = fireEventAsync if update_after is 'async' else fireEvent
handle('library.update', identifier = l.identifier, default_title = toUnicode(attrs.get('title', ''))) handle('library.update.movie', identifier = l.identifier, default_title = toUnicode(attrs.get('title', '')))
library_dict = l.to_dict(self.default_dict) library_dict = l.to_dict(self.default_dict)
db.expire_all()
return library_dict return library_dict
def update(self, identifier, default_title = '', force = False): def update(self, identifier, default_title = '', force = False):
if self.shuttingDown():
return
db = get_session() db = get_session()
library = db.query(Library).filter_by(identifier = identifier).first() library = db.query(Library).filter_by(identifier = identifier).first()
done_status = fireEvent('status.get', 'done', single = True) done_status = fireEvent('status.get', 'done', single = True)
library_dict = None
if library: if library:
library_dict = library.to_dict(self.default_dict) library_dict = library.to_dict(self.default_dict)
do_update = True do_update = True
if library.status_id == done_status.get('id') and not force: info = fireEvent('movie.info', merge = True, identifier = identifier)
do_update = False
else:
info = fireEvent('movie.info', merge = True, identifier = identifier)
# Don't need those here # Don't need those here
try: del info['in_wanted'] try: del info['in_wanted']
except: pass except: pass
try: del info['in_library'] try: del info['in_library']
except: pass except: pass
if not info or len(info) == 0: if not info or len(info) == 0:
log.error('Could not update, no movie info to work with: %s', identifier) log.error('Could not update, no movie info to work with: %s', identifier)
return False return False
# Main info # Main info
if do_update: if do_update:
@@ -87,7 +92,7 @@ class LibraryPlugin(Plugin):
library.tagline = toUnicode(info.get('tagline', '')) library.tagline = toUnicode(info.get('tagline', ''))
library.year = info.get('year', 0) library.year = info.get('year', 0)
library.status_id = done_status.get('id') library.status_id = done_status.get('id')
library.info = info library.info.update(info)
db.commit() db.commit()
# Titles # Titles
@@ -132,6 +137,7 @@ class LibraryPlugin(Plugin):
library_dict = library.to_dict(self.default_dict) library_dict = library.to_dict(self.default_dict)
db.expire_all()
return library_dict return library_dict
def updateReleaseDate(self, identifier): def updateReleaseDate(self, identifier):
@@ -147,9 +153,10 @@ class LibraryPlugin(Plugin):
if dates and dates.get('expires', 0) < time.time() or not dates: if dates and dates.get('expires', 0) < time.time() or not dates:
dates = fireEvent('movie.release_date', identifier = identifier, merge = True) dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
library.info = mergeDicts(library.info, {'release_date': dates }) library.info.update({'release_date': dates })
db.commit() db.commit()
db.expire_all()
return dates return dates
@@ -0,0 +1,73 @@
from .main import MovieSearcher
import random
def start():
return MovieSearcher()
config = [{
'name': 'moviesearcher',
'order': 20,
'groups': [
{
'tab': 'searcher',
'name': 'movie_searcher',
'label': 'Movie search',
'description': 'Search options for movies',
'advanced': True,
'options': [
{
'name': 'always_search',
'default': False,
'migrate_from': 'searcher',
'type': 'bool',
'label': 'Always search',
'description': 'Search for movies even before there is a ETA. Enabling this will probably get you a lot of fakes.',
},
{
'name': 'run_on_launch',
'migrate_from': 'searcher',
'label': 'Run on launch',
'advanced': True,
'default': 0,
'type': 'bool',
'description': 'Force run the searcher after (re)start.',
},
{
'name': 'search_on_add',
'label': 'Search after add',
'advanced': True,
'default': 1,
'type': 'bool',
'description': 'Disable this to only search for movies on cron.',
},
{
'name': 'cron_day',
'migrate_from': 'searcher',
'label': 'Day',
'advanced': True,
'default': '*',
'type': 'string',
'description': '<strong>*</strong>: Every day, <strong>*/2</strong>: Every 2 days, <strong>1</strong>: Every first of the month. See <a href="http://packages.python.org/APScheduler/cronschedule.html">APScheduler</a> for details.',
},
{
'name': 'cron_hour',
'migrate_from': 'searcher',
'label': 'Hour',
'advanced': True,
'default': random.randint(0, 23),
'type': 'string',
'description': '<strong>*</strong>: Every hour, <strong>*/8</strong>: Every 8 hours, <strong>3</strong>: At 3, midnight.',
},
{
'name': 'cron_minute',
'migrate_from': 'searcher',
'label': 'Minute',
'advanced': True,
'default': random.randint(0, 59),
'type': 'string',
'description': "Just keep it random, so the providers don't get DDOSed by every CP user on a 'full' hour."
},
],
},
],
}]
@@ -0,0 +1,438 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import simplifyString, toUnicode, ss
from couchpotato.core.helpers.variable import md5, getTitle, splitString, \
possibleTitles, getImdb
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.searcher.base import SearcherBase
from couchpotato.core.media.movie import MovieTypeBase
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
from couchpotato.environment import Env
from datetime import date
from sqlalchemy.exc import InterfaceError
import random
import re
import time
import traceback
log = CPLog(__name__)
class MovieSearcher(SearcherBase, MovieTypeBase):
in_progress = False
def __init__(self):
super(MovieSearcher, self).__init__()
addEvent('movie.searcher.all', self.searchAll)
addEvent('movie.searcher.all_view', self.searchAllView)
addEvent('movie.searcher.single', self.single)
addEvent('movie.searcher.correct_movie', self.correctMovie)
addEvent('movie.searcher.try_next_release', self.tryNextRelease)
addEvent('movie.searcher.could_be_released', self.couldBeReleased)
addApiView('movie.searcher.try_next', self.tryNextReleaseView, docs = {
'desc': 'Marks the snatched results as ignored and try the next best release',
'params': {
'id': {'desc': 'The id of the movie'},
},
})
addApiView('movie.searcher.full_search', self.searchAllView, docs = {
'desc': 'Starts a full search for all wanted movies',
})
addApiView('movie.searcher.progress', self.getProgress, docs = {
'desc': 'Get the progress of current full search',
'return': {'type': 'object', 'example': """{
'progress': False || object, total & to_go,
}"""},
})
if self.conf('run_on_launch'):
addEvent('app.load', self.searchAll)
def searchAllView(self, **kwargs):
fireEventAsync('movie.searcher.all')
return {
'success': not self.in_progress
}
def searchAll(self):
if self.in_progress:
log.info('Search already in progress')
fireEvent('notify.frontend', type = 'movie.searcher.already_started', data = True, message = 'Full search already in progress')
return
self.in_progress = True
fireEvent('notify.frontend', type = 'movie.searcher.started', data = True, message = 'Full search started')
db = get_session()
movies = db.query(Movie).filter(
Movie.status.has(identifier = 'active')
).all()
random.shuffle(movies)
self.in_progress = {
'total': len(movies),
'to_go': len(movies),
}
try:
search_protocols = fireEvent('searcher.protocols', single = True)
for movie in movies:
movie_dict = movie.to_dict({
'category': {},
'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files':{}},
'files': {},
})
try:
self.single(movie_dict, search_protocols)
except IndexError:
log.error('Forcing library update for %s, if you see this often, please report: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
fireEvent('library.update.movie', movie_dict['library']['identifier'], force = True)
except:
log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
self.in_progress['to_go'] -= 1
# Break if CP wants to shut down
if self.shuttingDown():
break
except SearchSetupError:
pass
self.in_progress = False
def single(self, movie, search_protocols = None, manual = False):
# Find out search type
try:
if not search_protocols:
search_protocols = fireEvent('searcher.protocols', single = True)
except SearchSetupError:
return
done_status = fireEvent('status.get', 'done', single = True)
if not movie['profile'] or (movie['status_id'] == done_status.get('id') and not manual):
log.debug('Movie doesn\'t have a profile or already done, assuming in manage tab.')
return
db = get_session()
pre_releases = fireEvent('quality.pre_releases', single = True)
release_dates = fireEvent('library.update.movie.release_date', identifier = movie['library']['identifier'], merge = True)
available_status, ignored_status, failed_status = fireEvent('status.get', ['available', 'ignored', 'failed'], single = True)
found_releases = []
too_early_to_search = []
default_title = getTitle(movie['library'])
if not default_title:
log.error('No proper info found for movie, removing it from library to cause it from having more issues.')
fireEvent('movie.delete', movie['id'], single = True)
return
fireEvent('notify.frontend', type = 'movie.searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title)
ret = False
for quality_type in movie['profile']['types']:
if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['library']['year']):
too_early_to_search.append(quality_type['quality']['identifier'])
continue
has_better_quality = 0
# See if better quality is available
for release in movie['releases']:
if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id'), failed_status.get('id')]:
has_better_quality += 1
# Don't search for quality lower then already available.
if has_better_quality is 0:
log.info('Search for %s in %s', (default_title, quality_type['quality']['label']))
quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True)
results = []
for search_protocol in search_protocols:
protocol_results = fireEvent('provider.search.%s.movie' % search_protocol, movie, quality, merge = True)
if protocol_results:
results += protocol_results
sorted_results = sorted(results, key = lambda k: k['score'], reverse = True)
if len(sorted_results) == 0:
log.debug('Nothing found for %s in %s', (default_title, quality_type['quality']['label']))
download_preference = self.conf('preferred_method', section = 'searcher')
if download_preference != 'both':
sorted_results = sorted(sorted_results, key = lambda k: k['protocol'][:3], reverse = (download_preference == 'torrent'))
# Check if movie isn't deleted while searching
if not db.query(Movie).filter_by(id = movie.get('id')).first():
break
# Add them to this movie releases list
for nzb in sorted_results:
nzb_identifier = md5(nzb['url'])
found_releases.append(nzb_identifier)
rls = db.query(Release).filter_by(identifier = nzb_identifier).first()
if not rls:
rls = Release(
identifier = nzb_identifier,
movie_id = movie.get('id'),
quality_id = quality_type.get('quality_id'),
status_id = available_status.get('id')
)
db.add(rls)
else:
[db.delete(old_info) for old_info in rls.info]
rls.last_edit = int(time.time())
db.commit()
for info in nzb:
try:
if not isinstance(nzb[info], (str, unicode, int, long, float)):
continue
rls_info = ReleaseInfo(
identifier = info,
value = toUnicode(nzb[info])
)
rls.info.append(rls_info)
except InterfaceError:
log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc()))
db.commit()
nzb['status_id'] = rls.status_id
for nzb in sorted_results:
if not quality_type.get('finish', False) and quality_type.get('wait_for', 0) > 0 and nzb.get('age') <= quality_type.get('wait_for', 0):
log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name']))
continue
if nzb['status_id'] in [ignored_status.get('id'), failed_status.get('id')]:
log.info('Ignored: %s', nzb['name'])
continue
if nzb['score'] <= 0:
log.info('Ignored, score to low: %s', nzb['name'])
continue
downloaded = fireEvent('searcher.download', data = nzb, movie = movie, manual = manual, single = True)
if downloaded is True:
ret = True
break
elif downloaded != 'try_next':
break
# Remove releases that aren't found anymore
for release in movie.get('releases', []):
if release.get('status_id') == available_status.get('id') and release.get('identifier') not in found_releases:
fireEvent('release.delete', release.get('id'), single = True)
else:
log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title))
fireEvent('movie.restatus', movie['id'])
break
# Break if CP wants to shut down
if self.shuttingDown() or ret:
break
if len(too_early_to_search) > 0:
log.info2('Too early to search for %s, %s', (too_early_to_search, default_title))
fireEvent('notify.frontend', type = 'movie.searcher.ended.%s' % movie['id'], data = True)
return ret
def correctMovie(self, nzb = None, movie = None, quality = None, **kwargs):
imdb_results = kwargs.get('imdb_results', False)
retention = Env.setting('retention', section = 'nzb')
if nzb.get('seeders') is None and 0 < retention < nzb.get('age', 0):
log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name']))
return False
movie_name = getTitle(movie['library'])
movie_words = re.split('\W+', simplifyString(movie_name))
nzb_name = simplifyString(nzb['name'])
nzb_words = re.split('\W+', nzb_name)
# Make sure it has required words
required_words = splitString(self.conf('required_words', section = 'searcher').lower())
try: required_words = list(set(required_words + splitString(movie['category']['required'].lower())))
except: pass
req_match = 0
for req_set in required_words:
req = splitString(req_set, '&')
req_match += len(list(set(nzb_words) & set(req))) == len(req)
if len(required_words) > 0 and req_match == 0:
log.info2('Wrong: Required word missing: %s', nzb['name'])
return False
# Ignore releases
ignored_words = splitString(self.conf('ignored_words', section = 'searcher').lower())
try: ignored_words = list(set(ignored_words + splitString(movie['category']['ignored'].lower())))
except: pass
ignored_match = 0
for ignored_set in ignored_words:
ignored = splitString(ignored_set, '&')
ignored_match += len(list(set(nzb_words) & set(ignored))) == len(ignored)
if len(ignored_words) > 0 and ignored_match:
log.info2("Wrong: '%s' contains 'ignored words'", (nzb['name']))
return False
# Ignore porn stuff
pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic', 'cock', 'dick']
pron_words = list(set(nzb_words) & set(pron_tags) - set(movie_words))
if pron_words:
log.info('Wrong: %s, probably pr0n', (nzb['name']))
return False
preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True)
# Contains lower quality string
if fireEvent('searcher.contains_other_quality', nzb, movie_year = movie['library']['year'], preferred_quality = preferred_quality, single = True):
log.info2('Wrong: %s, looking for %s', (nzb['name'], quality['label']))
return False
# File to small
if nzb['size'] and preferred_quality['size_min'] > nzb['size']:
log.info2('Wrong: "%s" is too small to be %s. %sMB instead of the minimal of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_min']))
return False
# File to large
if nzb['size'] and preferred_quality.get('size_max') < nzb['size']:
log.info2('Wrong: "%s" is too large to be %s. %sMB instead of the maximum of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_max']))
return False
# Provider specific functions
get_more = nzb.get('get_more_info')
if get_more:
get_more(nzb)
extra_check = nzb.get('extra_check')
if extra_check and not extra_check(nzb):
return False
if imdb_results:
return True
# Check if nzb contains imdb link
if getImdb(nzb.get('description', '')) == movie['library']['identifier']:
return True
for raw_title in movie['library']['titles']:
for movie_title in possibleTitles(raw_title['title']):
movie_words = re.split('\W+', simplifyString(movie_title))
if fireEvent('searcher.correct_name', nzb['name'], movie_title, single = True):
# if no IMDB link, at least check year range 1
if len(movie_words) > 2 and fireEvent('searcher.correct_year', nzb['name'], movie['library']['year'], 1, single = True):
return True
# if no IMDB link, at least check year
if len(movie_words) <= 2 and fireEvent('searcher.correct_year', nzb['name'], movie['library']['year'], 0, single = True):
return True
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'", (nzb['name'], movie_name, movie['library']['year']))
return False
def couldBeReleased(self, is_pre_release, dates, year = None):
now = int(time.time())
now_year = date.today().year
if (year is None or year < now_year - 1) and (not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0)):
return True
else:
# For movies before 1972
if not dates or dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0:
return True
if is_pre_release:
# Prerelease 1 week before theaters
if dates.get('theater') - 604800 < now:
return True
else:
# 12 weeks after theater release
if dates.get('theater') > 0 and dates.get('theater') + 7257600 < now:
return True
if dates.get('dvd') > 0:
# 4 weeks before dvd release
if dates.get('dvd') - 2419200 < now:
return True
# Dvd should be released
if dates.get('dvd') < now:
return True
return False
def tryNextReleaseView(self, id = None, **kwargs):
trynext = self.tryNextRelease(id, manual = True)
return {
'success': trynext
}
def tryNextRelease(self, movie_id, manual = False):
snatched_status, done_status, ignored_status = fireEvent('status.get', ['snatched', 'done', 'ignored'], single = True)
try:
db = get_session()
rels = db.query(Release) \
.filter_by(movie_id = movie_id) \
.filter(Release.status_id.in_([snatched_status.get('id'), done_status.get('id')])) \
.all()
for rel in rels:
rel.status_id = ignored_status.get('id')
db.commit()
movie_dict = fireEvent('movie.get', movie_id, single = True)
log.info('Trying next release for: %s', getTitle(movie_dict['library']))
fireEvent('movie.searcher.single', movie_dict, manual = manual)
return True
except:
log.error('Failed searching for next release: %s', traceback.format_exc())
return False
class SearchSetupError(Exception):
pass
@@ -0,0 +1,17 @@
from migrate.changeset.schema import create_column
from sqlalchemy.schema import MetaData, Column, Table, Index
from sqlalchemy.types import Integer
meta = MetaData()
def upgrade(migrate_engine):
meta.bind = migrate_engine
category_column = Column('category_id', Integer)
movie = Table('movie', meta, category_column)
create_column(category_column, movie)
Index('ix_movie_category_id', movie.c.category_id).create()
def downgrade(migrate_engine):
pass
+10 -6
View File
@@ -1,6 +1,5 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import Provider from couchpotato.core.providers.base import Provider
from couchpotato.environment import Env from couchpotato.environment import Env
@@ -33,7 +32,9 @@ class Notification(Provider):
addEvent(listener, self.createNotifyHandler(listener)) addEvent(listener, self.createNotifyHandler(listener))
def createNotifyHandler(self, listener): def createNotifyHandler(self, listener):
def notify(message = None, group = {}, data = None): def notify(message = None, group = None, data = None):
if not group: group = {}
if not self.conf('on_snatch', default = True) and listener == 'movie.snatched': if not self.conf('on_snatch', default = True) and listener == 'movie.snatched':
return return
return self._notify(message = message, data = data if data else group, listener = listener) return self._notify(message = message, data = data if data else group, listener = listener)
@@ -46,11 +47,12 @@ class Notification(Provider):
def _notify(self, *args, **kwargs): def _notify(self, *args, **kwargs):
if self.isEnabled(): if self.isEnabled():
return self.notify(*args, **kwargs) return self.notify(*args, **kwargs)
return False
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = None, listener = None):
pass if not data: data = {}
def test(self): def test(self, **kwargs):
test_type = self.testNotifyName() test_type = self.testNotifyName()
@@ -62,7 +64,9 @@ class Notification(Provider):
listener = 'test' listener = 'test'
) )
return jsonified({'success': success}) return {
'success': success
}
def testNotifyName(self): def testNotifyName(self):
return 'notify.%s.test' % self.getName().lower() return 'notify.%s.test' % self.getName().lower()
@@ -10,7 +10,8 @@ class Boxcar(Notification):
url = 'https://boxcar.io/devices/providers/7MNNXY3UIzVBwvzkKwkC/notifications' url = 'https://boxcar.io/devices/providers/7MNNXY3UIzVBwvzkKwkC/notifications'
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = None, listener = None):
if not data: data = {}
try: try:
message = message.strip() message = message.strip()
+62 -39
View File
@@ -2,15 +2,16 @@ from couchpotato import get_session
from couchpotato.api import addApiView, addNonBlockApiView from couchpotato.api import addApiView, addNonBlockApiView
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import tryInt, splitString from couchpotato.core.helpers.variable import tryInt, splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from couchpotato.core.settings.model import Notification as Notif from couchpotato.core.settings.model import Notification as Notif
from couchpotato.environment import Env from couchpotato.environment import Env
from operator import itemgetter
from sqlalchemy.sql.expression import or_ from sqlalchemy.sql.expression import or_
import threading import threading
import time import time
import traceback
import uuid import uuid
log = CPLog(__name__) log = CPLog(__name__)
@@ -18,9 +19,7 @@ log = CPLog(__name__)
class CoreNotifier(Notification): class CoreNotifier(Notification):
m_lock = threading.Lock() m_lock = None
messages = []
listeners = []
def __init__(self): def __init__(self):
super(CoreNotifier, self).__init__() super(CoreNotifier, self).__init__()
@@ -51,10 +50,15 @@ class CoreNotifier(Notification):
addApiView('notification.listener', self.listener) addApiView('notification.listener', self.listener)
fireEvent('schedule.interval', 'core.check_messages', self.checkMessages, hours = 12, single = True) fireEvent('schedule.interval', 'core.check_messages', self.checkMessages, hours = 12, single = True)
fireEvent('schedule.interval', 'core.clean_messages', self.cleanMessages, seconds = 15, single = True)
addEvent('app.load', self.clean) addEvent('app.load', self.clean)
addEvent('app.load', self.checkMessages) addEvent('app.load', self.checkMessages)
self.messages = []
self.listeners = []
self.m_lock = threading.Lock()
def clean(self): def clean(self):
db = get_session() db = get_session()
@@ -62,11 +66,9 @@ class CoreNotifier(Notification):
db.commit() db.commit()
def markAsRead(self): def markAsRead(self, ids = None, **kwargs):
ids = None ids = splitString(ids) if ids else None
if getParam('ids'):
ids = splitString(getParam('ids'))
db = get_session() db = get_session()
@@ -79,14 +81,13 @@ class CoreNotifier(Notification):
db.commit() db.commit()
return jsonified({ return {
'success': True 'success': True
}) }
def listView(self): def listView(self, limit_offset = None, **kwargs):
db = get_session() db = get_session()
limit_offset = getParam('limit_offset', None)
q = db.query(Notif) q = db.query(Notif)
@@ -105,18 +106,18 @@ class CoreNotifier(Notification):
ndict['type'] = 'notification' ndict['type'] = 'notification'
notifications.append(ndict) notifications.append(ndict)
return jsonified({ return {
'success': True, 'success': True,
'empty': len(notifications) == 0, 'empty': len(notifications) == 0,
'notifications': notifications 'notifications': notifications
}) }
def checkMessages(self): def checkMessages(self):
prop_name = 'messages.last_check' prop_name = 'messages.last_check'
last_check = tryInt(Env.prop(prop_name, default = 0)) last_check = tryInt(Env.prop(prop_name, default = 0))
messages = fireEvent('cp.messages', last_check = last_check, single = True) messages = fireEvent('cp.messages', last_check = last_check, single = True) or []
for message in messages: for message in messages:
if message.get('time') > last_check: if message.get('time') > last_check:
@@ -127,7 +128,8 @@ class CoreNotifier(Notification):
Env.prop(prop_name, value = last_check) Env.prop(prop_name, value = last_check)
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = None, listener = None):
if not data: data = {}
db = get_session() db = get_session()
@@ -148,7 +150,10 @@ class CoreNotifier(Notification):
return True return True
def frontend(self, type = 'notification', data = {}, message = None): def frontend(self, type = 'notification', data = None, message = None):
if not data: data = {}
log.debug('Notifying frontend')
self.m_lock.acquire() self.m_lock.acquire()
notification = { notification = {
@@ -168,10 +173,12 @@ class CoreNotifier(Notification):
'result': [notification], 'result': [notification],
}) })
except: except:
break log.debug('Failed sending to listener: %s', traceback.format_exc())
self.listeners = []
self.m_lock.release() self.m_lock.release()
self.cleanMessages()
log.debug('Done notifying frontend')
def addListener(self, callback, last_id = None): def addListener(self, callback, last_id = None):
@@ -183,59 +190,75 @@ class CoreNotifier(Notification):
'result': messages, 'result': messages,
}) })
self.m_lock.acquire()
self.listeners.append((callback, last_id)) self.listeners.append((callback, last_id))
self.m_lock.release()
def removeListener(self, callback): def removeListener(self, callback):
self.m_lock.acquire()
new_listeners = []
for list_tuple in self.listeners: for list_tuple in self.listeners:
try: try:
listener, last_id = list_tuple listener, last_id = list_tuple
if listener == callback: if listener != callback:
self.listeners.remove(list_tuple) new_listeners.append(list_tuple)
except: except:
pass log.debug('Failed removing listener: %s', traceback.format_exc())
def cleanMessages(self):
self.m_lock.acquire()
for message in self.messages:
if message['time'] < (time.time() - 15):
self.messages.remove(message)
self.listeners = new_listeners
self.m_lock.release() self.m_lock.release()
def cleanMessages(self):
if len(self.messages) == 0:
return
log.debug('Cleaning messages')
self.m_lock.acquire()
time_ago = (time.time() - 15)
self.messages[:] = [m for m in self.messages if (m['time'] > time_ago)]
self.m_lock.release()
log.debug('Done cleaning messages')
def getMessages(self, last_id): def getMessages(self, last_id):
log.debug('Getting messages with id: %s', last_id)
self.m_lock.acquire() self.m_lock.acquire()
recent = [] recent = []
index = 0 try:
for i in xrange(len(self.messages)): index = map(itemgetter('message_id'), self.messages).index(last_id)
index = len(self.messages) - i - 1 recent = self.messages[index + 1:]
if self.messages[index]["message_id"] == last_id: break except:
recent = self.messages[index:] pass
self.m_lock.release() self.m_lock.release()
log.debug('Returning for %s %s messages', (last_id, len(recent)))
return recent or [] return recent
def listener(self): def listener(self, init = False, **kwargs):
messages = [] messages = []
# Get unread # Get unread
if getParam('init'): if init:
db = get_session() db = get_session()
notifications = db.query(Notif) \ notifications = db.query(Notif) \
.filter(or_(Notif.read == False, Notif.added > (time.time() - 259200))) \ .filter(or_(Notif.read == False, Notif.added > (time.time() - 259200))) \
.all() .all()
for n in notifications: for n in notifications:
ndict = n.to_dict() ndict = n.to_dict()
ndict['type'] = 'notification' ndict['type'] = 'notification'
messages.append(ndict) messages.append(ndict)
return jsonified({ return {
'success': True, 'success': True,
'result': messages, 'result': messages,
}) }
@@ -31,8 +31,8 @@ var NotificationBase = new Class({
}); });
window.addEvent('load', function(){ window.addEvent('load', function(){
self.startInterval.delay($(window).getSize().x <= 480 ? 2000 : 300, self) self.startInterval.delay($(window).getSize().x <= 480 ? 2000 : 100, self);
}); })
}, },
@@ -157,7 +157,7 @@ var NotificationBase = new Class({
} }
// Restart poll // Restart poll
self.startPoll() self.startPoll.delay(1500, self);
}, },
showMessage: function(message, sticky, data){ showMessage: function(message, sticky, data){
+2 -1
View File
@@ -11,7 +11,8 @@ log = CPLog(__name__)
class Email(Notification): class Email(Notification):
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = None, listener = None):
if not data: data = {}
# Extract all the settings from settings # Extract all the settings from settings
from_address = self.conf('from') from_address = self.conf('from')
+2 -1
View File
@@ -43,7 +43,8 @@ class Growl(Notification):
else: else:
log.error('Failed register of growl: %s', traceback.format_exc()) log.error('Failed register of growl: %s', traceback.format_exc())
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = None, listener = None):
if not data: data = {}
self.register() self.register()
+20 -20
View File
@@ -1,7 +1,6 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.request import getParams, jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
import re import re
@@ -22,21 +21,17 @@ class NMJ(Notification):
addApiView(self.testNotifyName(), self.test) addApiView(self.testNotifyName(), self.test)
addApiView('notify.nmj.auto_config', self.autoConfig) addApiView('notify.nmj.auto_config', self.autoConfig)
def autoConfig(self): def autoConfig(self, host = 'localhost', **kwargs):
params = getParams()
host = params.get('host', 'localhost')
database = ''
mount = '' mount = ''
try: try:
terminal = telnetlib.Telnet(host) terminal = telnetlib.Telnet(host)
except Exception: except Exception:
log.error('Warning: unable to get a telnet session to %s', (host)) log.error('Warning: unable to get a telnet session to %s', host)
return self.failed() return self.failed()
log.debug('Connected to %s via telnet', (host)) log.debug('Connected to %s via telnet', host)
terminal.read_until('sh-3.00# ') terminal.read_until('sh-3.00# ')
terminal.write('cat /tmp/source\n') terminal.write('cat /tmp/source\n')
terminal.write('cat /tmp/netshare\n') terminal.write('cat /tmp/netshare\n')
@@ -50,7 +45,7 @@ class NMJ(Notification):
device = match.group(2) device = match.group(2)
log.info('Found NMJ database %s on device %s', (database, device)) log.info('Found NMJ database %s on device %s', (database, device))
else: else:
log.error('Could not get current NMJ database on %s, NMJ is probably not running!', (host)) log.error('Could not get current NMJ database on %s, NMJ is probably not running!', host)
return self.failed() return self.failed()
if device.startswith('NETWORK_SHARE/'): if device.startswith('NETWORK_SHARE/'):
@@ -58,28 +53,29 @@ class NMJ(Notification):
if match: if match:
mount = match.group().replace('127.0.0.1', host) mount = match.group().replace('127.0.0.1', host)
log.info('Found mounting url on the Popcorn Hour in configuration: %s', (mount)) log.info('Found mounting url on the Popcorn Hour in configuration: %s', mount)
else: else:
log.error('Detected a network share on the Popcorn Hour, but could not get the mounting url') log.error('Detected a network share on the Popcorn Hour, but could not get the mounting url')
return self.failed() return self.failed()
return jsonified({ return {
'success': True, 'success': True,
'database': database, 'database': database,
'mount': mount, 'mount': mount,
}) }
def addToLibrary(self, message = None, group = {}): def addToLibrary(self, message = None, group = None):
if self.isDisabled(): return if self.isDisabled(): return
if not group: group = {}
host = self.conf('host') host = self.conf('host')
mount = self.conf('mount') mount = self.conf('mount')
database = self.conf('database') database = self.conf('database')
if mount: if mount:
log.debug('Try to mount network drive via url: %s', (mount)) log.debug('Try to mount network drive via url: %s', mount)
try: try:
data = self.urlopen(mount) self.urlopen(mount)
except: except:
return False return False
@@ -102,20 +98,24 @@ class NMJ(Notification):
et = etree.fromstring(response) et = etree.fromstring(response)
result = et.findtext('returnValue') result = et.findtext('returnValue')
except SyntaxError, e: except SyntaxError, e:
log.error('Unable to parse XML returned from the Popcorn Hour: %s', (e)) log.error('Unable to parse XML returned from the Popcorn Hour: %s', e)
return False return False
if int(result) > 0: if int(result) > 0:
log.error('Popcorn Hour returned an errorcode: %s', (result)) log.error('Popcorn Hour returned an errorcode: %s', result)
return False return False
else: else:
log.info('NMJ started background scan') log.info('NMJ started background scan')
return True return True
def failed(self): def failed(self):
return jsonified({'success': False}) return {
'success': False
}
def test(self): def test(self, **kwargs):
return jsonified({'success': self.addToLibrary()}) return {
'success': self.addToLibrary()
}
@@ -1,8 +1,8 @@
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from flask.helpers import json
import base64 import base64
import json
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -12,7 +12,8 @@ class Notifo(Notification):
url = 'https://api.notifo.com/v1/send_notification' url = 'https://api.notifo.com/v1/send_notification'
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = None, listener = None):
if not data: data = {}
try: try:
params = { params = {
@@ -8,19 +8,17 @@ log = CPLog(__name__)
class NotifyMyAndroid(Notification): class NotifyMyAndroid(Notification):
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = None, listener = None):
if not data: data = {}
nma = pynma.PyNMA() nma = pynma.PyNMA()
keys = splitString(self.conf('api_key')) keys = splitString(self.conf('api_key'))
nma.addkey(keys) nma.addkey(keys)
nma.developerkey(self.conf('dev_key')) nma.developerkey(self.conf('dev_key'))
# hacky fix for the event type
# as it seems to be part of the message now
self.event = message.split(' ')[0]
response = nma.push( response = nma.push(
application = self.default_title, application = self.default_title,
event = self.event, event = message.split(' ')[0],
description = message, description = message,
priority = self.conf('priority'), priority = self.conf('priority'),
batch_mode = len(keys) > 1 batch_mode = len(keys) > 1
@@ -22,6 +22,13 @@ config = [{
'description': 'Default should be on localhost', 'description': 'Default should be on localhost',
'advanced': True, 'advanced': True,
}, },
{
'name': 'on_snatch',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Also send message when movie is snatched.',
},
], ],
} }
], ],
+30 -12
View File
@@ -1,10 +1,10 @@
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.request import jsonified from couchpotato.core.helpers.variable import cleanHost, splitString
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from urllib2 import URLError from urllib2 import URLError
from urlparse import urlparse
from xml.dom import minidom from xml.dom import minidom
import traceback import traceback
@@ -17,16 +17,17 @@ class Plex(Notification):
super(Plex, self).__init__() super(Plex, self).__init__()
addEvent('renamer.after', self.addToLibrary) addEvent('renamer.after', self.addToLibrary)
def addToLibrary(self, message = None, group = {}): def addToLibrary(self, message = None, group = None):
if self.isDisabled(): return if self.isDisabled(): return
if not group: group = {}
log.info('Sending notification to Plex') log.info('Sending notification to Plex')
hosts = [cleanHost(x.strip() + ':32400') for x in self.conf('host').split(",")] hosts = self.getHosts(port = 32400)
for host in hosts: for host in hosts:
source_type = ['movie'] source_type = ['movie']
base_url = '%slibrary/sections' % host base_url = '%s/library/sections' % host
refresh_url = '%s/%%s/refresh' % base_url refresh_url = '%s/%%s/refresh' % base_url
try: try:
@@ -37,7 +38,7 @@ class Plex(Notification):
for s in sections: for s in sections:
if s.getAttribute('type') in source_type: if s.getAttribute('type') in source_type:
url = refresh_url % s.getAttribute('key') url = refresh_url % s.getAttribute('key')
x = self.urlopen(url) self.urlopen(url)
except: except:
log.error('Plex library update failed for %s, Media Server not running: %s', (host, traceback.format_exc(1))) log.error('Plex library update failed for %s, Media Server not running: %s', (host, traceback.format_exc(1)))
@@ -45,9 +46,10 @@ class Plex(Notification):
return True return True
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = None, listener = None):
if not data: data = {}
hosts = [x.strip() + ':3000' for x in self.conf('host').split(",")] hosts = self.getHosts(port = 3000)
successful = 0 successful = 0
for host in hosts: for host in hosts:
if self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host): if self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host):
@@ -57,8 +59,7 @@ class Plex(Notification):
def send(self, command, host): def send(self, command, host):
url = 'http://%s/xbmcCmds/xbmcHttp/?%s' % (host, tryUrlencode(command)) url = '%s/xbmcCmds/xbmcHttp/?%s' % (host, tryUrlencode(command))
headers = {} headers = {}
try: try:
@@ -73,7 +74,7 @@ class Plex(Notification):
log.info('Plex notification to %s successful.', host) log.info('Plex notification to %s successful.', host)
return True return True
def test(self): def test(self, **kwargs):
test_type = self.testNotifyName() test_type = self.testNotifyName()
@@ -86,4 +87,21 @@ class Plex(Notification):
) )
success2 = self.addToLibrary() success2 = self.addToLibrary()
return jsonified({'success': success or success2}) return {
'success': success or success2
}
def getHosts(self, port = None):
raw_hosts = splitString(self.conf('host'))
hosts = []
for h in raw_hosts:
h = cleanHost(h)
p = urlparse(h)
h = h.rstrip('/')
if port and not p.port:
h += ':%s' % port
hosts.append(h)
return hosts
+2 -1
View File
@@ -12,7 +12,8 @@ class Prowl(Notification):
'api': 'https://api.prowlapp.com/publicapi/add' 'api': 'https://api.prowlapp.com/publicapi/add'
} }
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = None, listener = None):
if not data: data = {}
data = { data = {
'apikey': self.conf('api_key'), 'apikey': self.conf('api_key'),
@@ -11,7 +11,8 @@ class Pushalot(Notification):
'api': 'https://pushalot.com/api/sendmessage' 'api': 'https://pushalot.com/api/sendmessage'
} }
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = None, listener = None):
if not data: data = {}
data = { data = {
'AuthorizationToken': self.conf('auth_token'), 'AuthorizationToken': self.conf('auth_token'),
@@ -11,7 +11,8 @@ class Pushover(Notification):
app_token = 'YkxHMYDZp285L265L3IwH3LmzkTaCy' app_token = 'YkxHMYDZp285L265L3IwH3LmzkTaCy'
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = None, listener = None):
if not data: data = {}
http_handler = HTTPSConnection("api.pushover.net:443") http_handler = HTTPSConnection("api.pushover.net:443")
@@ -23,7 +24,7 @@ class Pushover(Notification):
} }
if data and data.get('library'): if data and data.get('library'):
api_data.extend({ api_data.update({
'url': toUnicode('http://www.imdb.com/title/%s/' % data['library']['identifier']), 'url': toUnicode('http://www.imdb.com/title/%s/' % data['library']['identifier']),
'url_title': toUnicode('%s on IMDb' % getTitle(data['library'])), 'url_title': toUnicode('%s on IMDb' % getTitle(data['library'])),
}) })
@@ -1,5 +1,4 @@
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
import os import os
@@ -16,8 +15,9 @@ class Synoindex(Notification):
super(Synoindex, self).__init__() super(Synoindex, self).__init__()
addEvent('renamer.after', self.addToLibrary) addEvent('renamer.after', self.addToLibrary)
def addToLibrary(self, message = None, group = {}): def addToLibrary(self, message = None, group = None):
if self.isDisabled(): return if self.isDisabled(): return
if not group: group = {}
command = [self.index_path, '-A', group.get('destination_dir')] command = [self.index_path, '-A', group.get('destination_dir')]
log.info('Executing synoindex command: %s ', command) log.info('Executing synoindex command: %s ', command)
@@ -28,9 +28,10 @@ class Synoindex(Notification):
return True return True
except OSError, e: except OSError, e:
log.error('Unable to run synoindex: %s', e) log.error('Unable to run synoindex: %s', e)
return False
return True return False
def test(self): def test(self, **kwargs):
return jsonified({'success': os.path.isfile(self.index_path)}) return {
'success': os.path.isfile(self.index_path)
}
@@ -11,7 +11,8 @@ class Toasty(Notification):
'api': 'http://api.supertoasty.com/notify/%s?%s' 'api': 'http://api.supertoasty.com/notify/%s?%s'
} }
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = None, listener = None):
if not data: data = {}
data = { data = {
'title': self.default_title, 'title': self.default_title,
+2 -1
View File
@@ -13,7 +13,8 @@ class Trakt(Notification):
listen_to = ['movie.downloaded'] listen_to = ['movie.downloaded']
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = None, listener = None):
if not data: data = {}
post_data = { post_data = {
'username': self.conf('automation_username'), 'username': self.conf('automation_username'),
+16 -19
View File
@@ -1,12 +1,11 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import cleanHost from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from flask.helpers import url_for from couchpotato.environment import Env
from pytwitter import Api, parse_qsl from pytwitter import Api
from werkzeug.utils import redirect from urlparse import parse_qsl
import oauth2 import oauth2
log = CPLog(__name__) log = CPLog(__name__)
@@ -31,7 +30,8 @@ class Twitter(Notification):
addApiView('notify.%s.auth_url' % self.getName().lower(), self.getAuthorizationUrl) addApiView('notify.%s.auth_url' % self.getName().lower(), self.getAuthorizationUrl)
addApiView('notify.%s.credentials' % self.getName().lower(), self.getCredentials) addApiView('notify.%s.credentials' % self.getName().lower(), self.getCredentials)
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = None, listener = None):
if not data: data = {}
api = Api(self.consumer_key, self.consumer_secret, self.conf('access_token_key'), self.conf('access_token_secret')) api = Api(self.consumer_key, self.consumer_secret, self.conf('access_token_key'), self.conf('access_token_secret'))
@@ -52,7 +52,7 @@ class Twitter(Notification):
try: try:
if direct_message: if direct_message:
for user in direct_message_users.split(): for user in direct_message_users.split():
api.PostDirectMessage(user, '[%s] %s' % (self.default_title, message)) api.PostDirectMessage('[%s] %s' % (self.default_title, message), screen_name = user)
else: else:
update_message = '[%s] %s' % (self.default_title, message) update_message = '[%s] %s' % (self.default_title, message)
if len(update_message) > 140: if len(update_message) > 140:
@@ -70,10 +70,9 @@ class Twitter(Notification):
return True return True
def getAuthorizationUrl(self): def getAuthorizationUrl(self, host = None, **kwargs):
referer = getParam('host') callback_url = cleanHost(host) + '%snotify.%s.credentials/' % (Env.get('api_base').lstrip('/'), self.getName().lower())
callback_url = cleanHost(referer) + '%snotify.%s.credentials/' % (url_for('api.index').lstrip('/'), self.getName().lower())
oauth_consumer = oauth2.Consumer(self.consumer_key, self.consumer_secret) oauth_consumer = oauth2.Consumer(self.consumer_key, self.consumer_secret)
oauth_client = oauth2.Client(oauth_consumer) oauth_client = oauth2.Client(oauth_consumer)
@@ -82,31 +81,29 @@ class Twitter(Notification):
if resp['status'] != '200': if resp['status'] != '200':
log.error('Invalid response from Twitter requesting temp token: %s', resp['status']) log.error('Invalid response from Twitter requesting temp token: %s', resp['status'])
return jsonified({ return {
'success': False, 'success': False,
}) }
else: else:
self.request_token = dict(parse_qsl(content)) self.request_token = dict(parse_qsl(content))
auth_url = self.urls['authorize'] + ("?oauth_token=%s" % self.request_token['oauth_token']) auth_url = self.urls['authorize'] + ("?oauth_token=%s" % self.request_token['oauth_token'])
log.info('Redirecting to "%s"', auth_url) log.info('Redirecting to "%s"', auth_url)
return jsonified({ return {
'success': True, 'success': True,
'url': auth_url, 'url': auth_url,
}) }
def getCredentials(self): def getCredentials(self, oauth_verifier, **kwargs):
key = getParam('oauth_verifier')
token = oauth2.Token(self.request_token['oauth_token'], self.request_token['oauth_token_secret']) token = oauth2.Token(self.request_token['oauth_token'], self.request_token['oauth_token_secret'])
token.set_verifier(key) token.set_verifier(oauth_verifier)
oauth_consumer = oauth2.Consumer(key = self.consumer_key, secret = self.consumer_secret) oauth_consumer = oauth2.Consumer(key = self.consumer_key, secret = self.consumer_secret)
oauth_client = oauth2.Client(oauth_consumer, token) oauth_client = oauth2.Client(oauth_consumer, token)
resp, content = oauth_client.request(self.urls['access'], method = 'POST', body = 'oauth_verifier=%s' % key) resp, content = oauth_client.request(self.urls['access'], method = 'POST', body = 'oauth_verifier=%s' % oauth_verifier)
access_token = dict(parse_qsl(content)) access_token = dict(parse_qsl(content))
if resp['status'] != '200': if resp['status'] != '200':
@@ -121,4 +118,4 @@ class Twitter(Notification):
self.request_token = None self.request_token = None
return redirect(url_for('web.index') + 'settings/notifications/') return 'redirect', Env.get('web_base') + 'settings/notifications/'
@@ -31,6 +31,28 @@ config = [{
'default': '', 'default': '',
'type': 'password', 'type': 'password',
}, },
{
'name': 'only_first',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Only update the first host when movie snatched, useful for synced XBMC',
},
{
'name': 'remote_dir_scan',
'label': 'Remote Folder Scan',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Only scan new movie folder at remote XBMC servers. Works if movie location is the same.',
},
{
'name': 'on_snatch',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Also send message when movie is snatched.',
},
], ],
} }
], ],
+53 -21
View File
@@ -1,8 +1,10 @@
from couchpotato.core.helpers.variable import splitString from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from flask.helpers import json from urllib2 import URLError
import base64 import base64
import json
import socket
import traceback import traceback
import urllib import urllib
@@ -11,39 +13,56 @@ log = CPLog(__name__)
class XBMC(Notification): class XBMC(Notification):
listen_to = ['renamer.after'] listen_to = ['renamer.after', 'movie.snatched']
use_json_notifications = {} use_json_notifications = {}
http_time_between_calls = 0
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = None, listener = None):
if not data: data = {}
hosts = splitString(self.conf('host')) hosts = splitString(self.conf('host'))
successful = 0 successful = 0
max_successful = 0
for host in hosts: for host in hosts:
if self.use_json_notifications.get(host) is None: if self.use_json_notifications.get(host) is None:
self.getXBMCJSONversion(host, message = message) self.getXBMCJSONversion(host, message = message)
if self.use_json_notifications.get(host): if self.use_json_notifications.get(host):
response = self.request(host, [ calls = [
('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}), ('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}),
('VideoLibrary.Scan', {}), ]
])
if data and data.get('destination_dir') and (not self.conf('only_first') or hosts.index(host) == 0):
param = {}
if self.conf('remote_dir_scan') or socket.getfqdn('localhost') == socket.getfqdn(host.split(':')[0]):
param = {'directory': data['destination_dir']}
calls.append(('VideoLibrary.Scan', param))
max_successful += len(calls)
response = self.request(host, calls)
else: else:
response = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message}) response = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message})
response += self.request(host, [('VideoLibrary.Scan', {})])
if data and data.get('destination_dir') and (not self.conf('only_first') or hosts.index(host) == 0):
response += self.request(host, [('VideoLibrary.Scan', {})])
max_successful += 1
max_successful += 1
try: try:
for result in response: for result in response:
if (result.get('result') and result['result'] == 'OK'): if result.get('result') and result['result'] == 'OK':
successful += 1 successful += 1
elif (result.get('error')): elif result.get('error'):
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code'])) log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
except: except:
log.error('Failed parsing results: %s', traceback.format_exc()) log.error('Failed parsing results: %s', traceback.format_exc())
return successful == len(hosts) * 2 return successful == max_successful
def getXBMCJSONversion(self, host, message = ''): def getXBMCJSONversion(self, host, message = ''):
@@ -52,9 +71,9 @@ class XBMC(Notification):
# XBMC JSON-RPC version request # XBMC JSON-RPC version request
response = self.request(host, [ response = self.request(host, [
('JSONRPC.Version', {}) ('JSONRPC.Version', {})
]) ])
for result in response: for result in response:
if (result.get('result') and type(result['result']['version']).__name__ == 'int'): if result.get('result') and type(result['result']['version']).__name__ == 'int':
# only v2 and v4 return an int object # only v2 and v4 return an int object
# v6 (as of XBMC v12(Frodo)) is required to send notifications # v6 (as of XBMC v12(Frodo)) is required to send notifications
xbmc_rpc_version = str(result['result']['version']) xbmc_rpc_version = str(result['result']['version'])
@@ -67,15 +86,15 @@ class XBMC(Notification):
# send the text message # send the text message
resp = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message}) resp = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message})
for result in resp: for result in resp:
if (result.get('result') and result['result'] == 'OK'): if result.get('result') and result['result'] == 'OK':
log.debug('Message delivered successfully!') log.debug('Message delivered successfully!')
success = True success = True
break break
elif (result.get('error')): elif result.get('error'):
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code'])) log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
break break
elif (result.get('result') and type(result['result']['version']).__name__ == 'dict'): elif result.get('result') and type(result['result']['version']).__name__ == 'dict':
# XBMC JSON-RPC v6 returns an array object containing # XBMC JSON-RPC v6 returns an array object containing
# major, minor and patch number # major, minor and patch number
xbmc_rpc_version = str(result['result']['version']['major']) xbmc_rpc_version = str(result['result']['version']['major'])
@@ -90,16 +109,16 @@ class XBMC(Notification):
# send the text message # send the text message
resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message, 'image': self.getNotificationImage('small')})]) resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message, 'image': self.getNotificationImage('small')})])
for result in resp: for result in resp:
if (result.get('result') and result['result'] == 'OK'): if result.get('result') and result['result'] == 'OK':
log.debug('Message delivered successfully!') log.debug('Message delivered successfully!')
success = True success = True
break break
elif (result.get('error')): elif result.get('error'):
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code'])) log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
break break
# error getting version info (we do have contact with XBMC though) # error getting version info (we do have contact with XBMC though)
elif (result.get('error')): elif result.get('error'):
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code'])) log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
log.debug('Use JSON notifications: %s ', self.use_json_notifications) log.debug('Use JSON notifications: %s ', self.use_json_notifications)
@@ -137,7 +156,7 @@ class XBMC(Notification):
# <li>Error:<message> # <li>Error:<message>
# </html> # </html>
# #
response = self.urlopen(server, headers = headers) response = self.urlopen(server, headers = headers, timeout = 3, show_error = False)
if 'OK' in response: if 'OK' in response:
log.debug('Returned from non-JSON-type request %s: %s', (host, response)) log.debug('Returned from non-JSON-type request %s: %s', (host, response))
@@ -148,6 +167,13 @@ class XBMC(Notification):
# manually fake expected response array # manually fake expected response array
return [{'result': 'Error'}] return [{'result': 'Error'}]
except URLError, e:
if isinstance(e.reason, socket.timeout):
log.info('Couldn\'t send request to XBMC, assuming it\'s turned off')
return [{'result': 'Error'}]
else:
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc())
return [{'result': 'Error'}]
except: except:
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc()) log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc())
return [{'result': 'Error'}] return [{'result': 'Error'}]
@@ -176,11 +202,17 @@ class XBMC(Notification):
try: try:
log.debug('Sending request to %s: %s', (host, data)) log.debug('Sending request to %s: %s', (host, data))
rdata = self.urlopen(server, headers = headers, params = data, multipart = True) response = self.getJsonData(server, headers = headers, params = data, timeout = 3, show_error = False)
response = json.loads(rdata)
log.debug('Returned from request %s: %s', (host, response)) log.debug('Returned from request %s: %s', (host, response))
return response return response
except URLError, e:
if isinstance(e.reason, socket.timeout):
log.info('Couldn\'t send request to XBMC, assuming it\'s turned off')
return []
else:
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
return []
except: except:
log.error('Failed sending request to XBMC: %s', traceback.format_exc()) log.error('Failed sending request to XBMC: %s', traceback.format_exc())
return [] return []
+11 -1
View File
@@ -26,6 +26,10 @@ class Automation(Plugin):
movie_ids = [] movie_ids = []
for imdb_id in movies: for imdb_id in movies:
if self.shuttingDown():
break
prop_name = 'automation.added.%s' % imdb_id prop_name = 'automation.added.%s' % imdb_id
added = Env.prop(prop_name, default = False) added = Env.prop(prop_name, default = False)
if not added: if not added:
@@ -35,5 +39,11 @@ class Automation(Plugin):
Env.prop(prop_name, True) Env.prop(prop_name, True)
for movie_id in movie_ids: for movie_id in movie_ids:
if self.shuttingDown():
break
movie_dict = fireEvent('movie.get', movie_id, single = True) movie_dict = fireEvent('movie.get', movie_id, single = True)
fireEvent('searcher.single', movie_dict) fireEvent('movie.searcher.single', movie_dict)
return True
+71 -36
View File
@@ -1,16 +1,18 @@
from StringIO import StringIO from StringIO import StringIO
from couchpotato import addView
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString, \
from couchpotato.core.helpers.variable import getExt, md5 toUnicode
from couchpotato.core.helpers.variable import getExt, md5, isLocalIP
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.environment import Env from couchpotato.environment import Env
from flask.templating import render_template_string
from multipartpost import MultipartPostHandler from multipartpost import MultipartPostHandler
from tornado import template
from tornado.web import StaticFileHandler
from urlparse import urlparse from urlparse import urlparse
import cookielib import cookielib
import glob import glob
import gzip import gzip
import inspect
import math import math
import os.path import os.path
import re import re
@@ -23,55 +25,76 @@ log = CPLog(__name__)
class Plugin(object): class Plugin(object):
_class_name = None
plugin_path = None
enabled_option = 'enabled' enabled_option = 'enabled'
auto_register_static = True auto_register_static = True
_needs_shutdown = False _needs_shutdown = False
_running = None
user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:24.0) Gecko/20130519 Firefox/24.0'
http_last_use = {} http_last_use = {}
http_time_between_calls = 0 http_time_between_calls = 0
http_failed_request = {} http_failed_request = {}
http_failed_disabled = {} http_failed_disabled = {}
def __new__(typ, *args, **kwargs):
new_plugin = super(Plugin, typ).__new__(typ)
new_plugin.registerPlugin()
return new_plugin
def registerPlugin(self): def registerPlugin(self):
addEvent('app.do_shutdown', self.doShutdown) addEvent('app.do_shutdown', self.doShutdown)
addEvent('plugin.running', self.isRunning) addEvent('plugin.running', self.isRunning)
self._running = []
def conf(self, attr, value = None, default = None): if self.auto_register_static:
return Env.setting(attr, self.getName().lower(), value = value, default = default) self.registerStatic(inspect.getfile(self.__class__))
def conf(self, attr, value = None, default = None, section = None):
class_name = self.getName().lower().split(':')
return Env.setting(attr, section = section if section else class_name[0].lower(), value = value, default = default)
def getName(self): def getName(self):
return self.__class__.__name__ return self._class_name or self.__class__.__name__
def renderTemplate(self, parent_file, template, **params): def setName(self, name):
self._class_name = name
template = open(os.path.join(os.path.dirname(parent_file), template), 'r').read() def renderTemplate(self, parent_file, templ, **params):
return render_template_string(template, **params)
t = template.Template(open(os.path.join(os.path.dirname(parent_file), templ), 'r').read())
return t.generate(**params)
def registerStatic(self, plugin_file, add_to_head = True): def registerStatic(self, plugin_file, add_to_head = True):
# Register plugin path # Register plugin path
self.plugin_path = os.path.dirname(plugin_file) self.plugin_path = os.path.dirname(plugin_file)
static_folder = toUnicode(os.path.join(self.plugin_path, 'static'))
if not os.path.isdir(static_folder):
return
# Get plugin_name from PluginName # Get plugin_name from PluginName
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', self.__class__.__name__) s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', self.__class__.__name__)
class_name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() class_name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
path = 'api/%s/static/%s/' % (Env.setting('api_key'), class_name) # View path
addView(path + '<path:filename>', self.showStatic, static = True) path = 'static/plugin/%s/' % (class_name)
# Add handler to Tornado
Env.get('app').add_handlers(".*$", [(Env.get('web_base') + path + '(.*)', StaticFileHandler, {'path': static_folder})])
# Register for HTML <HEAD>
if add_to_head: if add_to_head:
for f in glob.glob(os.path.join(self.plugin_path, 'static', '*')): for f in glob.glob(os.path.join(self.plugin_path, 'static', '*')):
ext = getExt(f) ext = getExt(f)
if ext in ['js', 'css']: if ext in ['js', 'css']:
fireEvent('register_%s' % ('script' if ext in 'js' else 'style'), path + os.path.basename(f), f) fireEvent('register_%s' % ('script' if ext in 'js' else 'style'), path + os.path.basename(f), f)
def showStatic(self, filename):
d = os.path.join(self.plugin_path, 'static')
from flask.helpers import send_from_directory
return send_from_directory(d, filename)
def createFile(self, path, content, binary = False): def createFile(self, path, content, binary = False):
path = ss(path) path = ss(path)
@@ -104,19 +127,22 @@ class Plugin(object):
if not params: params = {} if not params: params = {}
# Fill in some headers # Fill in some headers
headers['Referer'] = headers.get('Referer', urlparse(url).hostname) parsed_url = urlparse(url)
headers['Host'] = headers.get('Host', urlparse(url).hostname) host = '%s%s' % (parsed_url.hostname, (':' + str(parsed_url.port) if parsed_url.port else ''))
headers['User-Agent'] = headers.get('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:10.0.2) Gecko/20100101 Firefox/10.0.2')
headers['Accept-encoding'] = headers.get('Accept-encoding', 'gzip')
host = urlparse(url).hostname headers['Referer'] = headers.get('Referer', '%s://%s' % (parsed_url.scheme, host))
headers['Host'] = headers.get('Host', host)
headers['User-Agent'] = headers.get('User-Agent', self.user_agent)
headers['Accept-encoding'] = headers.get('Accept-encoding', 'gzip')
headers['Connection'] = headers.get('Connection', 'keep-alive')
headers['Cache-Control'] = headers.get('Cache-Control', 'max-age=0')
# Don't try for failed requests # Don't try for failed requests
if self.http_failed_disabled.get(host, 0) > 0: if self.http_failed_disabled.get(host, 0) > 0:
if self.http_failed_disabled[host] > (time.time() - 900): if self.http_failed_disabled[host] > (time.time() - 900):
log.info2('Disabled calls to %s for 15 minutes because so many failed requests.', host) log.info2('Disabled calls to %s for 15 minutes because so many failed requests.', host)
if not show_error: if not show_error:
raise raise Exception('Disabled calls to %s for 15 minutes because so many failed requests')
else: else:
return '' return ''
else: else:
@@ -126,6 +152,10 @@ class Plugin(object):
self.wait(host) self.wait(host)
try: try:
# Make sure opener has the correct headers
if opener:
opener.add_headers = headers
if multipart: if multipart:
log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data')) log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
request = urllib2.Request(url, params, headers) request = urllib2.Request(url, params, headers)
@@ -138,8 +168,13 @@ class Plugin(object):
response = opener.open(request, timeout = timeout) response = opener.open(request, timeout = timeout)
else: else:
log.info('Opening url: %s, params: %s', (url, [x for x in params.iterkeys()])) log.info('Opening url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
data = tryUrlencode(params) if len(params) > 0 else None
if isinstance(params, (str, unicode)) and len(params) > 0:
data = params
else:
data = tryUrlencode(params) if len(params) > 0 else None
request = urllib2.Request(url, data, headers) request = urllib2.Request(url, data, headers)
if opener: if opener:
@@ -152,8 +187,10 @@ class Plugin(object):
buf = StringIO(response.read()) buf = StringIO(response.read())
f = gzip.GzipFile(fileobj = buf) f = gzip.GzipFile(fileobj = buf)
data = f.read() data = f.read()
f.close()
else: else:
data = response.read() data = response.read()
response.close()
self.http_failed_request[host] = 0 self.http_failed_request[host] = 0
except IOError: except IOError:
@@ -168,7 +205,7 @@ class Plugin(object):
self.http_failed_request[host] += 1 self.http_failed_request[host] += 1
# Disable temporarily # Disable temporarily
if self.http_failed_request[host] > 5: if self.http_failed_request[host] > 5 and not isLocalIP(host):
self.http_failed_disabled[host] = time.time() self.http_failed_disabled[host] = time.time()
except: except:
@@ -209,9 +246,6 @@ class Plugin(object):
def isRunning(self, value = None, boolean = True): def isRunning(self, value = None, boolean = True):
if not hasattr(self, '_running'):
self._running = []
if value is None: if value is None:
return self._running return self._running
@@ -225,8 +259,8 @@ class Plugin(object):
def getCache(self, cache_key, url = None, **kwargs): def getCache(self, cache_key, url = None, **kwargs):
cache_key = md5(ss(cache_key)) cache_key_md5 = md5(cache_key)
cache = Env.get('cache').get(cache_key) cache = Env.get('cache').get(cache_key_md5)
if cache: if cache:
if not Env.get('dev'): log.debug('Getting cache %s', cache_key) if not Env.get('dev'): log.debug('Getting cache %s', cache_key)
return cache return cache
@@ -250,8 +284,9 @@ class Plugin(object):
return '' return ''
def setCache(self, cache_key, value, timeout = 300): def setCache(self, cache_key, value, timeout = 300):
cache_key_md5 = md5(cache_key)
log.debug('Setting cache %s', cache_key) log.debug('Setting cache %s', cache_key)
Env.get('cache').set(cache_key, value, timeout) Env.get('cache').set(cache_key_md5, value, timeout)
return value return value
def createNzbName(self, data, movie): def createNzbName(self, data, movie):
@@ -260,9 +295,9 @@ class Plugin(object):
def createFileName(self, data, filedata, movie): def createFileName(self, data, filedata, movie):
name = os.path.join(self.createNzbName(data, movie)) name = os.path.join(self.createNzbName(data, movie))
if data.get('type') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata: if data.get('protocol') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata:
return '%s.%s' % (name, 'rar') return '%s.%s' % (name, 'rar')
return '%s.%s' % (name, data.get('type')) return '%s.%s' % (name, data.get('protocol'))
def cpTag(self, movie): def cpTag(self, movie):
if Env.setting('enabled', 'renamer'): if Env.setting('enabled', 'renamer'):
@@ -274,4 +309,4 @@ class Plugin(object):
return not self.isEnabled() return not self.isEnabled()
def isEnabled(self): def isEnabled(self):
return self.conf(self.enabled_option) or self.conf(self.enabled_option) == None return self.conf(self.enabled_option) or self.conf(self.enabled_option) is None
+5 -7
View File
@@ -1,5 +1,4 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.helpers.request import getParam, jsonified
from couchpotato.core.helpers.variable import getUserDir from couchpotato.core.helpers.variable import getUserDir
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
import ctypes import ctypes
@@ -13,7 +12,7 @@ if os.name == 'nt':
except: except:
# todo:: subclass ImportError for missing dependencies, vs. broken plugins? # todo:: subclass ImportError for missing dependencies, vs. broken plugins?
raise ImportError("Missing the win32file module, which is a part of the prerequisite \ raise ImportError("Missing the win32file module, which is a part of the prerequisite \
pywin32 package. You can get it from http://sourceforge.net/projects/pywin32/files/pywin32/"); pywin32 package. You can get it from http://sourceforge.net/projects/pywin32/files/pywin32/")
else: else:
import win32file #@UnresolvedImport import win32file #@UnresolvedImport
@@ -63,16 +62,15 @@ class FileBrowser(Plugin):
return driveletters return driveletters
def view(self): def view(self, path = '/', show_hidden = True, **kwargs):
path = getParam('path', '/')
home = getUserDir() home = getUserDir()
if not path: if not path:
path = home path = home
try: try:
dirs = self.getDirectories(path = path, show_hidden = getParam('show_hidden', True)) dirs = self.getDirectories(path = path, show_hidden = show_hidden)
except: except:
dirs = [] dirs = []
@@ -82,14 +80,14 @@ class FileBrowser(Plugin):
elif parent != '/' and parent[-2:] != ':\\': elif parent != '/' and parent[-2:] != ':\\':
parent += os.path.sep parent += os.path.sep
return jsonified({ return {
'is_root': path == '/', 'is_root': path == '/',
'empty': len(dirs) == 0, 'empty': len(dirs) == 0,
'parent': parent, 'parent': parent,
'home': home + os.path.sep, 'home': home + os.path.sep,
'platform': os.name, 'platform': os.name,
'dirs': dirs, 'dirs': dirs,
}) }
def is_hidden(self, filepath): def is_hidden(self, filepath):
@@ -0,0 +1,6 @@
from .main import CategoryPlugin
def start():
return CategoryPlugin()
config = []
+121
View File
@@ -0,0 +1,121 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie, Category
log = CPLog(__name__)
class CategoryPlugin(Plugin):
def __init__(self):
addEvent('category.all', self.all)
addApiView('category.save', self.save)
addApiView('category.save_order', self.saveOrder)
addApiView('category.delete', self.delete)
addApiView('category.list', self.allView, docs = {
'desc': 'List all available categories',
'return': {'type': 'object', 'example': """{
'success': True,
'list': array, categories
}"""}
})
def allView(self, **kwargs):
return {
'success': True,
'list': self.all()
}
def all(self):
db = get_session()
categories = db.query(Category).all()
temp = []
for category in categories:
temp.append(category.to_dict())
db.expire_all()
return temp
def save(self, **kwargs):
db = get_session()
c = db.query(Category).filter_by(id = kwargs.get('id')).first()
if not c:
c = Category()
db.add(c)
c.order = kwargs.get('order', c.order if c.order else 0)
c.label = toUnicode(kwargs.get('label', ''))
c.ignored = toUnicode(kwargs.get('ignored', ''))
c.preferred = toUnicode(kwargs.get('preferred', ''))
c.required = toUnicode(kwargs.get('required', ''))
c.destination = toUnicode(kwargs.get('destination', ''))
db.commit()
category_dict = c.to_dict()
return {
'success': True,
'category': category_dict
}
def saveOrder(self, **kwargs):
db = get_session()
order = 0
for category_id in kwargs.get('ids', []):
c = db.query(Category).filter_by(id = category_id).first()
c.order = order
order += 1
db.commit()
return {
'success': True
}
def delete(self, id = None, **kwargs):
db = get_session()
success = False
message = ''
try:
c = db.query(Category).filter_by(id = id).first()
db.delete(c)
db.commit()
# Force defaults on all empty category movies
self.removeFromMovie(id)
success = True
except Exception, e:
message = log.error('Failed deleting category: %s', e)
db.expire_all()
return {
'success': success,
'message': message
}
def removeFromMovie(self, category_id):
db = get_session()
movies = db.query(Movie).filter(Movie.category_id == category_id).all()
if len(movies) > 0:
for movie in movies:
movie.category_id = None
db.commit()
@@ -0,0 +1,82 @@
.add_new_category {
padding: 20px;
display: block;
text-align: center;
font-size: 20px;
border-bottom: 1px solid rgba(255,255,255,0.2);
}
.category {
border-bottom: 1px solid rgba(255,255,255,0.2);
position: relative;
}
.category > .delete {
position: absolute;
padding: 16px;
right: 0;
cursor: pointer;
opacity: 0.6;
color: #fd5353;
}
.category > .delete:hover {
opacity: 1;
}
.category .ctrlHolder:hover {
background: none;
}
.category .formHint {
width: 250px !important;
margin: 0 !important;
opacity: 0.1;
}
.category:hover .formHint {
opacity: 1;
}
#category_ordering {
}
#category_ordering ul {
float: left;
margin: 0;
width: 275px;
padding: 0;
}
#category_ordering li {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
border-bottom: 1px solid rgba(255,255,255,0.2);
padding: 0 5px;
}
#category_ordering li:last-child { border: 0; }
#category_ordering li .check {
margin: 2px 10px 0 0;
vertical-align: top;
}
#category_ordering li > span {
display: inline-block;
height: 20px;
vertical-align: top;
line-height: 20px;
}
#category_ordering li .handle {
background: url('../../static/profile_plugin/handle.png') center;
width: 20px;
float: right;
}
#category_ordering .formHint {
clear: none;
float: right;
width: 250px;
margin: 0;
}
@@ -0,0 +1,332 @@
var CategoryListBase = new Class({
initialize: function(){
var self = this;
App.addEvent('load', self.addSettings.bind(self));
},
setup: function(categories){
var self = this;
self.categories = []
Array.each(categories, self.createCategory.bind(self));
},
addSettings: function(){
var self = this;
self.settings = App.getPage('Settings')
self.settings.addEvent('create', function(){
var tab = self.settings.createSubTab('category', {
'label': 'Categories',
'name': 'category',
'subtab_label': 'Category & filtering'
}, self.settings.tabs.searcher ,'searcher');
self.tab = tab.tab;
self.content = tab.content;
self.createList();
self.createOrdering();
})
// Add categories in renamer
self.settings.addEvent('create', function(){
var renamer_group = self.settings.tabs.renamer.groups.renamer;
self.categories.each(function(category){
var input = new Option.Directory('section_name', 'option.name', category.get('destination'), {
'name': category.get('label')
});
input.inject(renamer_group.getElement('.renamer_to'));
input.fireEvent('injected');
input.save = function(){
category.data.destination = input.getValue();
category.save();
};
});
})
},
createList: function(){
var self = this;
var count = self.categories.length;
self.settings.createGroup({
'label': 'Categories',
'description': 'Create categories, each one extending global filters. (Needs refresh \'' +(App.isMac() ? 'CMD+R' : 'F5')+ '\' after editing)'
}).inject(self.content).adopt(
self.category_container = new Element('div.container'),
new Element('a.add_new_category', {
'text': count > 0 ? 'Create another category' : 'Click here to create a category.',
'events': {
'click': function(){
var category = self.createCategory();
$(category).inject(self.category_container)
}
}
})
);
// Add categories, that aren't part of the core (for editing)
Array.each(self.categories, function(category){
$(category).inject(self.category_container)
});
},
getCategory: function(id){
return this.categories.filter(function(category){
return category.data.id == id
}).pick()
},
getAll: function(){
return this.categories;
},
createCategory: function(data){
var self = this;
var data = data || {'id': randomString()}
var category = new Category(data)
self.categories.include(category)
return category;
},
createOrdering: function(){
var self = this;
var category_list;
var group = self.settings.createGroup({
'label': 'Category ordering'
}).adopt(
new Element('.ctrlHolder#category_ordering').adopt(
new Element('label[text=Order]'),
category_list = new Element('ul'),
new Element('p.formHint', {
'html': 'Change the order the categories are in the dropdown list.<br />First one will be default.'
})
)
).inject(self.content)
Array.each(self.categories, function(category){
new Element('li', {'data-id': category.data.id}).adopt(
new Element('span.category_label', {
'text': category.data.label
}),
new Element('span.handle')
).inject(category_list);
});
// Sortable
self.category_sortable = new Sortables(category_list, {
'revert': true,
'handle': '',
'opacity': 0.5,
'onComplete': self.saveOrdering.bind(self)
});
},
saveOrdering: function(){
var self = this;
var ids = [];
self.category_sortable.list.getElements('li').each(function(el, nr){
ids.include(el.get('data-id'));
});
Api.request('category.save_order', {
'data': {
'ids': ids
}
});
}
})
window.CategoryList = new CategoryListBase();
var Category = new Class({
data: {},
initialize: function(data){
var self = this;
self.data = data;
self.create();
self.el.addEvents({
'change:relay(select)': self.save.bind(self, 0),
'keyup:relay(input[type=text])': self.save.bind(self, [300])
});
},
create: function(){
var self = this;
var data = self.data;
self.el = new Element('div.category').adopt(
self.delete_button = new Element('span.delete.icon2', {
'events': {
'click': self.del.bind(self)
}
}),
new Element('.category_label.ctrlHolder').adopt(
new Element('label', {'text':'Name'}),
new Element('input.inlay', {
'type':'text',
'value': data.label,
'placeholder': 'Example: Kids, Horror or His'
}),
new Element('p.formHint', {'text': 'See global filters for explanation.'})
),
new Element('.category_preferred.ctrlHolder').adopt(
new Element('label', {'text':'Preferred'}),
new Element('input.inlay', {
'type':'text',
'value': data.preferred,
'placeholder': 'Blu-ray, DTS'
})
),
new Element('.category_required.ctrlHolder').adopt(
new Element('label', {'text':'Required'}),
new Element('input.inlay', {
'type':'text',
'value': data.required,
'placeholder': 'Example: DTS, AC3 & English'
})
),
new Element('.category_ignored.ctrlHolder').adopt(
new Element('label', {'text':'Ignored'}),
new Element('input.inlay', {
'type':'text',
'value': data.ignored,
'placeholder': 'Example: dubbed, swesub, french'
})
)
);
self.makeSortable()
},
save: function(delay){
var self = this;
if(self.save_timer) clearTimeout(self.save_timer);
self.save_timer = (function(){
var data = self.getData();
Api.request('category.save', {
'data': self.getData(),
'useSpinner': true,
'spinnerOptions': {
'target': self.el
},
'onComplete': function(json){
if(json.success){
self.data = json.category;
}
}
});
}).delay(delay || 0, self)
},
getData: function(){
var self = this;
var data = {
'id' : self.data.id,
'label' : self.el.getElement('.category_label input').get('value'),
'required' : self.el.getElement('.category_required input').get('value'),
'preferred' : self.el.getElement('.category_preferred input').get('value'),
'ignored' : self.el.getElement('.category_ignored input').get('value'),
'destination': self.data.destination
}
return data
},
del: function(){
var self = this;
if(self.data.label == undefined){
self.el.destroy();
return;
}
var label = self.el.getElement('.category_label input').get('value');
var qObj = new Question('Are you sure you want to delete <strong>"'+label+'"</strong>?', '', [{
'text': 'Delete "'+label+'"',
'class': 'delete',
'events': {
'click': function(e){
(e).preventDefault();
Api.request('category.delete', {
'data': {
'id': self.data.id
},
'useSpinner': true,
'spinnerOptions': {
'target': self.el
},
'onComplete': function(json){
if(json.success) {
qObj.close();
self.el.destroy();
} else {
alert(json.message);
}
}
});
}
}
}, {
'text': 'Cancel',
'cancel': true
}]);
},
makeSortable: function(){
var self = this;
self.sortable = new Sortables(self.category_container, {
'revert': true,
'handle': '.handle',
'opacity': 0.5,
'onComplete': self.save.bind(self, 300)
});
},
get: function(attr){
return this.data[attr]
},
toElement: function(){
return this.el
}
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

+69 -75
View File
@@ -1,13 +1,14 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.request import jsonified, getParams
from couchpotato.core.helpers.variable import splitString, tryInt from couchpotato.core.helpers.variable import splitString, tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie from couchpotato.core.settings.model import Movie, Library, LibraryTitle, \
Release
from sqlalchemy.orm import joinedload_all from sqlalchemy.orm import joinedload_all
import random from sqlalchemy.sql.expression import asc, or_
import random as rndm
import time import time
log = CPLog(__name__) log = CPLog(__name__)
@@ -16,41 +17,10 @@ log = CPLog(__name__)
class Dashboard(Plugin): class Dashboard(Plugin):
def __init__(self): def __init__(self):
addApiView('dashboard.suggestions', self.suggestView)
addApiView('dashboard.soon', self.getSoonView) addApiView('dashboard.soon', self.getSoonView)
def newSuggestions(self): def getSoonView(self, limit_offset = None, random = False, late = False, **kwargs):
movies = fireEvent('movie.list', status = ['active', 'done'], limit_offset = (20, 0), single = True)
movie_identifiers = [m['library']['identifier'] for m in movies[1]]
ignored_movies = fireEvent('movie.list', status = ['ignored', 'deleted'], limit_offset = (100, 0), single = True)
ignored_identifiers = [m['library']['identifier'] for m in ignored_movies[1]]
suggestions = fireEvent('movie.suggest', movies = movie_identifiers, ignore = ignored_identifiers, single = True)
suggest_status = fireEvent('status.get', 'suggest', single = True)
for suggestion in suggestions:
fireEvent('movie.add', params = {'identifier': suggestion}, force_readd = False, search_after = False, status_id = suggest_status.get('id'))
def suggestView(self):
db = get_session()
movies = db.query(Movie).limit(20).all()
identifiers = [m.library.identifier for m in movies]
suggestions = fireEvent('movie.suggest', movies = identifiers, single = True)
return jsonified({
'result': True,
'suggestions': suggestions
})
def getSoonView(self):
params = getParams()
db = get_session() db = get_session()
now = time.time() now = time.time()
@@ -72,63 +42,87 @@ class Dashboard(Plugin):
profile_pre[profile.get('id')] = contains profile_pre[profile.get('id')] = contains
# Get all active movies
active_status = fireEvent('status.get', 'active', single = True)
subq = db.query(Movie).filter(Movie.status_id == active_status.get('id')).subquery()
q = db.query(Movie).join((subq, subq.c.id == Movie.id)) \
.options(joinedload_all('releases')) \
.options(joinedload_all('profile.types')) \
.options(joinedload_all('library.titles')) \
.options(joinedload_all('library.files')) \
.options(joinedload_all('status')) \
.options(joinedload_all('files'))
# Add limit # Add limit
limit_offset = params.get('limit_offset')
limit = 12 limit = 12
if limit_offset: if limit_offset:
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
limit = tryInt(splt[0]) limit = tryInt(splt[0])
all_movies = q.all() # Get all active movies
active_status, ignored_status = fireEvent('status.get', ['active', 'ignored'], single = True)
q = db.query(Movie) \
.join(Library) \
.outerjoin(Movie.releases) \
.filter(Movie.status_id == active_status.get('id')) \
.with_entities(Movie.id, Movie.profile_id, Library.info, Library.year) \
.group_by(Movie.id) \
.filter(or_(Release.id == None, Release.status_id == ignored_status.get('id')))
if params.get('random', False): if not random:
random.shuffle(all_movies) q = q.join(LibraryTitle) \
.filter(LibraryTitle.default == True) \
.order_by(asc(LibraryTitle.simple_title))
active = q.all()
movies = [] movies = []
for movie in all_movies:
pp = profile_pre.get(movie.profile.id)
eta = movie.library.info.get('release_date', {}) or {}
coming_soon = False
# Theater quality if len(active) > 0:
if pp.get('theater') and fireEvent('searcher.could_be_released', True, eta, single = True):
coming_soon = True
if pp.get('dvd') and fireEvent('searcher.could_be_released', False, eta, single = True):
coming_soon = True
# Do the shuffle
if random:
rndm.shuffle(active)
if coming_soon: movie_ids = []
temp = movie.to_dict({ for movie in active:
'profile': {'types': {}}, movie_id, profile_id, info, year = movie
'releases': {'files':{}, 'info': {}},
'library': {'titles': {}, 'files':{}},
'files': {},
})
# Don't list older movies pp = profile_pre.get(profile_id)
if ((not params.get('late') and (not eta.get('dvd') or (eta.get('dvd') and eta.get('dvd') > (now - 2419200)))) or \ if not pp: continue
(params.get('late') and eta.get('dvd') and eta.get('dvd') < (now - 2419200))):
movies.append(temp)
if len(movies) >= limit: eta = info.get('release_date', {}) or {}
break coming_soon = False
return jsonified({ # Theater quality
if pp.get('theater') and fireEvent('movie.searcher.could_be_released', True, eta, year, single = True):
coming_soon = True
elif pp.get('dvd') and fireEvent('movie.searcher.could_be_released', False, eta, year, single = True):
coming_soon = True
if coming_soon:
# Don't list older movies
if ((not late and (not eta.get('dvd') and not eta.get('theater') or eta.get('dvd') and eta.get('dvd') > (now - 2419200))) or
(late and (eta.get('dvd', 0) > 0 or eta.get('theater')) and eta.get('dvd') < (now - 2419200))):
movie_ids.append(movie_id)
if len(movie_ids) >= limit:
break
if len(movie_ids) > 0:
# Get all movie information
movies_raw = db.query(Movie) \
.options(joinedload_all('library.titles')) \
.options(joinedload_all('library.files')) \
.options(joinedload_all('files')) \
.filter(Movie.id.in_(movie_ids)) \
.all()
# Create dict by movie id
movie_dict = {}
for movie in movies_raw:
movie_dict[movie.id] = movie
for movie_id in movie_ids:
movies.append(movie_dict[movie_id].to_dict({
'library': {'titles': {}, 'files':{}},
'files': {},
}))
return {
'success': True, 'success': True,
'empty': len(movies) == 0, 'empty': len(movies) == 0,
'movies': movies, 'movies': movies,
}) }
getLateView = getSoonView getLateView = getSoonView
+17 -22
View File
@@ -2,15 +2,13 @@ from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.helpers.variable import md5, getExt from couchpotato.core.helpers.variable import md5, getExt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.plugins.scanner.main import Scanner from couchpotato.core.plugins.scanner.main import Scanner
from couchpotato.core.settings.model import FileType, File from couchpotato.core.settings.model import FileType, File
from couchpotato.environment import Env from couchpotato.environment import Env
from flask.helpers import send_file from tornado.web import StaticFileHandler
from werkzeug.exceptions import NotFound
import os.path import os.path
import time import time
import traceback import traceback
@@ -25,7 +23,7 @@ class FileManager(Plugin):
addEvent('file.download', self.download) addEvent('file.download', self.download)
addEvent('file.types', self.getTypes) addEvent('file.types', self.getTypes)
addApiView('file.cache/<path:filename>', self.showCacheFile, static = True, docs = { addApiView('file.cache/(.*)', self.showCacheFile, static = True, docs = {
'desc': 'Return a file from the cp_data/cache directory', 'desc': 'Return a file from the cp_data/cache directory',
'params': { 'params': {
'filename': {'desc': 'path/filename of the wanted file'} 'filename': {'desc': 'path/filename of the wanted file'}
@@ -73,25 +71,20 @@ class FileManager(Plugin):
db = get_session() db = get_session()
for root, dirs, walk_files in os.walk(Env.get('cache_dir')): for root, dirs, walk_files in os.walk(Env.get('cache_dir')):
for filename in walk_files: for filename in walk_files:
if root == python_cache or 'minified' in filename or 'version' in filename: continue if os.path.splitext(filename)[1] in ['.png', '.jpg', '.jpeg']:
file_path = os.path.join(root, filename) file_path = os.path.join(root, filename)
f = db.query(File).filter(File.path == toUnicode(file_path)).first() f = db.query(File).filter(File.path == toUnicode(file_path)).first()
if not f: if not f:
os.remove(file_path) os.remove(file_path)
except: except:
log.error('Failed removing unused file: %s', traceback.format_exc()) log.error('Failed removing unused file: %s', traceback.format_exc())
def showCacheFile(self, filename = ''): def showCacheFile(self, route, **kwargs):
Env.get('app').add_handlers(".*$", [('%s%s' % (Env.get('api_base'), route), StaticFileHandler, {'path': Env.get('cache_dir')})])
file_path = os.path.join(Env.get('cache_dir'), os.path.basename(filename))
if not os.path.isfile(file_path): def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = None):
log.error('File "%s" not found', file_path) if not urlopen_kwargs: urlopen_kwargs = {}
raise NotFound()
return send_file(file_path, conditional = True)
def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = {}):
if not dest: # to Cache if not dest: # to Cache
dest = os.path.join(Env.get('cache_dir'), '%s.%s' % (md5(url), getExt(url))) dest = os.path.join(Env.get('cache_dir'), '%s.%s' % (md5(url), getExt(url)))
@@ -108,7 +101,9 @@ class FileManager(Plugin):
self.createFile(dest, filedata, binary = True) self.createFile(dest, filedata, binary = True)
return dest return dest
def add(self, path = '', part = 1, type_tuple = (), available = 1, properties = {}): def add(self, path = '', part = 1, type_tuple = (), available = 1, properties = None):
if not properties: properties = {}
type_id = self.getType(type_tuple).get('id') type_id = self.getType(type_tuple).get('id')
db = get_session() db = get_session()
@@ -158,8 +153,8 @@ class FileManager(Plugin):
return types return types
def getTypesView(self): def getTypesView(self, **kwargs):
return jsonified({ return {
'types': self.getTypes() 'types': self.getTypes()
}) }
+7 -4
View File
@@ -1,11 +1,11 @@
var File = new Class({ var File = new Class({
initialize: function(file){ initialize: function(type, file){
var self = this; var self = this;
if(!file){ if(!file){
self.empty = true; self.empty = true;
self.el = new Element('div'); self.el = new Element('div.empty_file.'+type);
return return
} }
@@ -22,7 +22,10 @@ var File = new Class({
var file_name = self.data.path.replace(/^.*[\\\/]/, ''); var file_name = self.data.path.replace(/^.*[\\\/]/, '');
self.el = new Element('div', { self.el = new Element('div', {
'class': 'type_image ' + self.type.identifier 'class': 'type_image ' + self.type.identifier,
'styles': {
'background-image': 'url('+Api.createUrl('file.cache') + file_name+')'
}
}).adopt( }).adopt(
new Element('img', { new Element('img', {
'src': Api.createUrl('file.cache') + file_name 'src': Api.createUrl('file.cache') + file_name
@@ -45,7 +48,7 @@ var FileSelect = new Class({
}); });
if(single) if(single)
return new File(results.pop()); return new File(type, results.pop());
return results; return results;
@@ -1,6 +0,0 @@
from .main import LibraryPlugin
def start():
return LibraryPlugin()
config = []
+19 -25
View File
@@ -1,6 +1,5 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParam, getParams
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
@@ -47,9 +46,9 @@ class Logging(Plugin):
} }
}) })
def get(self): def get(self, nr = 0, **kwargs):
nr = int(getParam('nr', 0)) nr = tryInt(nr)
current_path = None current_path = None
total = 1 total = 1
@@ -71,16 +70,15 @@ class Logging(Plugin):
f = open(current_path, 'r') f = open(current_path, 'r')
log = f.read() log = f.read()
return jsonified({ return {
'success': True, 'success': True,
'log': toUnicode(log), 'log': toUnicode(log),
'total': total, 'total': total,
}) }
def partial(self): def partial(self, type = 'all', lines = 30, **kwargs):
log_type = getParam('type', 'all') total_lines = tryInt(lines)
total_lines = tryInt(getParam('lines', 30))
log_lines = [] log_lines = []
@@ -92,7 +90,6 @@ class Logging(Plugin):
if not os.path.isfile(path): if not os.path.isfile(path):
break break
reversed_lines = []
f = open(path, 'r') f = open(path, 'r')
reversed_lines = toUnicode(f.read()).split('[0m\n') reversed_lines = toUnicode(f.read()).split('[0m\n')
reversed_lines.reverse() reversed_lines.reverse()
@@ -100,7 +97,7 @@ class Logging(Plugin):
brk = False brk = False
for line in reversed_lines: for line in reversed_lines:
if log_type == 'all' or '%s ' % log_type.upper() in line: if type == 'all' or '%s ' % type.upper() in line:
log_lines.append(line) log_lines.append(line)
if len(log_lines) >= total_lines: if len(log_lines) >= total_lines:
@@ -111,18 +108,18 @@ class Logging(Plugin):
break break
log_lines.reverse() log_lines.reverse()
return jsonified({ return {
'success': True, 'success': True,
'log': '[0m\n'.join(log_lines), 'log': '[0m\n'.join(log_lines),
}) }
def clear(self): def clear(self, **kwargs):
for x in range(0, 50): for x in range(0, 50):
path = '%s%s' % (Env.get('log_path'), '.%s' % x if x > 0 else '') path = '%s%s' % (Env.get('log_path'), '.%s' % x if x > 0 else '')
if not os.path.isfile(path): if not os.path.isfile(path):
break continue
try: try:
@@ -135,24 +132,21 @@ class Logging(Plugin):
except: except:
log.error('Couldn\'t delete file "%s": %s', (path, traceback.format_exc())) log.error('Couldn\'t delete file "%s": %s', (path, traceback.format_exc()))
return jsonified({ return {
'success': True 'success': True
}) }
def log(self): def log(self, type = 'error', **kwargs):
params = getParams()
try: try:
log_message = 'API log: %s' % params log_message = 'API log: %s' % kwargs
try: try:
getattr(log, params.get('type', 'error'))(log_message) getattr(log, type)(log_message)
except: except:
log.error(log_message) log.error(log_message)
except: except:
log.error('Couldn\'t log via API: %s', params) log.error('Couldn\'t log via API: %s', kwargs)
return {
return jsonified({
'success': True 'success': True
}) }
@@ -6,19 +6,6 @@ Page.Log = new Class({
title: 'Show recent logs.', title: 'Show recent logs.',
has_tab: false, has_tab: false,
initialize: function(options){
var self = this;
self.parent(options)
App.getBlock('more').addLink(new Element('a', {
'href': App.createUrl(self.name),
'text': self.name.capitalize(),
'title': self.title
}))
},
indexAction: function(){ indexAction: function(){
var self = this; var self = this;
@@ -28,6 +28,14 @@ config = [{
'description': 'Remove movie from db if it can\'t be found after re-scan.', 'description': 'Remove movie from db if it can\'t be found after re-scan.',
'default': True, 'default': True,
}, },
{
'label': 'Scan at startup',
'name': 'startup_scan',
'type': 'bool',
'default': True,
'advanced': True,
'description': 'Do a quick scan on startup. On slow systems better disable this.',
},
], ],
}, },
], ],
+24 -19
View File
@@ -1,7 +1,6 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
from couchpotato.core.helpers.encoding import ss from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import splitString, getTitle from couchpotato.core.helpers.variable import splitString, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
@@ -27,7 +26,8 @@ class Manage(Plugin):
addEvent('manage.diskspace', self.getDiskSpace) addEvent('manage.diskspace', self.getDiskSpace)
# Add files after renaming # Add files after renaming
def after_rename(message = None, group = {}): def after_rename(message = None, group = None):
if not group: group = {}
return self.scanFilesToLibrary(folder = group['destination_dir'], files = group['renamed_files']) return self.scanFilesToLibrary(folder = group['destination_dir'], files = group['renamed_files'])
addEvent('renamer.after', after_rename, priority = 110) addEvent('renamer.after', after_rename, priority = 110)
@@ -45,25 +45,24 @@ class Manage(Plugin):
}"""}, }"""},
}) })
if not Env.get('dev'): if not Env.get('dev') and self.conf('startup_scan'):
def updateLibrary(): addEvent('app.load', self.updateLibraryQuick)
self.updateLibrary(full = False)
addEvent('app.load', updateLibrary)
def getProgress(self): def getProgress(self, **kwargs):
return jsonified({ return {
'progress': self.in_progress 'progress': self.in_progress
}) }
def updateLibraryView(self): def updateLibraryView(self, full = 1, **kwargs):
full = getParam('full', default = 1)
fireEventAsync('manage.update', full = True if full == '1' else False) fireEventAsync('manage.update', full = True if full == '1' else False)
return jsonified({ return {
'success': True 'success': True
}) }
def updateLibraryQuick(self):
return self.updateLibrary(full = False)
def updateLibrary(self, full = True): def updateLibrary(self, full = True):
last_update = float(Env.prop('manage.last_update', default = 0)) last_update = float(Env.prop('manage.last_update', default = 0))
@@ -119,7 +118,9 @@ class Manage(Plugin):
fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all') fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all')
else: else:
for release in done_movie.get('releases', []): releases = fireEvent('release.for_movie', id = done_movie.get('id'), single = True)
for release in releases:
if len(release.get('files', [])) == 0: if len(release.get('files', [])) == 0:
fireEvent('release.delete', release['id']) fireEvent('release.delete', release['id'])
else: else:
@@ -130,9 +131,9 @@ class Manage(Plugin):
break break
# Check if there are duplicate releases (different quality) use the last one, delete the rest # Check if there are duplicate releases (different quality) use the last one, delete the rest
if len(done_movie.get('releases', [])) > 1: if len(releases) > 1:
used_files = {} used_files = {}
for release in done_movie.get('releases', []): for release in releases:
for release_file in release.get('files', []): for release_file in release.get('files', []):
already_used = used_files.get(release_file['path']) already_used = used_files.get(release_file['path'])
@@ -171,6 +172,7 @@ class Manage(Plugin):
self.in_progress = False self.in_progress = False
def createAddToLibrary(self, folder, added_identifiers = []): def createAddToLibrary(self, folder, added_identifiers = []):
def addToLibrary(group, total_found, to_go): def addToLibrary(group, total_found, to_go):
if self.in_progress[folder]['total'] is None: if self.in_progress[folder]['total'] is None:
self.in_progress[folder] = { self.in_progress[folder] = {
@@ -184,9 +186,9 @@ class Manage(Plugin):
# Add it to release and update the info # Add it to release and update the info
fireEvent('release.add', group = group) fireEvent('release.add', group = group)
fireEventAsync('library.update', identifier = identifier, on_complete = self.createAfterUpdate(folder, identifier)) fireEventAsync('library.update.movie', identifier = identifier, on_complete = self.createAfterUpdate(folder, identifier))
else: else:
self.in_progress[folder]['to_go'] = self.in_progress[folder]['to_go'] - 1 self.in_progress[folder]['to_go'] -= 1
return addToLibrary return addToLibrary
@@ -194,7 +196,10 @@ class Manage(Plugin):
# Notify frontend # Notify frontend
def afterUpdate(): def afterUpdate():
self.in_progress[folder]['to_go'] = self.in_progress[folder]['to_go'] - 1 if not self.in_progress or self.shuttingDown():
return
self.in_progress[folder]['to_go'] -= 1
total = self.in_progress[folder]['total'] total = self.in_progress[folder]['total']
movie_dict = fireEvent('movie.get', identifier, single = True) movie_dict = fireEvent('movie.get', identifier, single = True)
@@ -1,6 +0,0 @@
from .main import MoviePlugin
def start():
return MoviePlugin()
config = []
+31 -29
View File
@@ -2,10 +2,10 @@ from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParams, getParam
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Profile, ProfileType, Movie from couchpotato.core.settings.model import Profile, ProfileType, Movie
from sqlalchemy.orm import joinedload_all
log = CPLog(__name__) log = CPLog(__name__)
@@ -46,48 +46,49 @@ class ProfilePlugin(Plugin):
movie.profile_id = default_profile.get('id') movie.profile_id = default_profile.get('id')
db.commit() db.commit()
def allView(self): def allView(self, **kwargs):
return jsonified({ return {
'success': True, 'success': True,
'list': self.all() 'list': self.all()
}) }
def all(self): def all(self):
db = get_session() db = get_session()
profiles = db.query(Profile).all() profiles = db.query(Profile) \
.options(joinedload_all('types')) \
.all()
temp = [] temp = []
for profile in profiles: for profile in profiles:
temp.append(profile.to_dict(self.to_dict)) temp.append(profile.to_dict(self.to_dict))
db.expire_all()
return temp return temp
def save(self): def save(self, **kwargs):
params = getParams()
db = get_session() db = get_session()
p = db.query(Profile).filter_by(id = params.get('id')).first() p = db.query(Profile).filter_by(id = kwargs.get('id')).first()
if not p: if not p:
p = Profile() p = Profile()
db.add(p) db.add(p)
p.label = toUnicode(params.get('label')) p.label = toUnicode(kwargs.get('label'))
p.order = params.get('order', p.order if p.order else 0) p.order = kwargs.get('order', p.order if p.order else 0)
p.core = params.get('core', False) p.core = kwargs.get('core', False)
#delete old types #delete old types
[db.delete(t) for t in p.types] [db.delete(t) for t in p.types]
order = 0 order = 0
for type in params.get('types', []): for type in kwargs.get('types', []):
t = ProfileType( t = ProfileType(
order = order, order = order,
finish = type.get('finish') if order > 0 else 1, finish = type.get('finish') if order > 0 else 1,
wait_for = params.get('wait_for'), wait_for = kwargs.get('wait_for'),
quality_id = type.get('quality_id') quality_id = type.get('quality_id')
) )
p.types.append(t) p.types.append(t)
@@ -98,41 +99,41 @@ class ProfilePlugin(Plugin):
profile_dict = p.to_dict(self.to_dict) profile_dict = p.to_dict(self.to_dict)
return jsonified({ return {
'success': True, 'success': True,
'profile': profile_dict 'profile': profile_dict
}) }
def default(self): def default(self):
db = get_session() db = get_session()
default = db.query(Profile).first() default = db.query(Profile) \
.options(joinedload_all('types')) \
.first()
default_dict = default.to_dict(self.to_dict) default_dict = default.to_dict(self.to_dict)
db.expire_all()
return default_dict return default_dict
def saveOrder(self): def saveOrder(self, **kwargs):
params = getParams()
db = get_session() db = get_session()
order = 0 order = 0
for profile in params.get('ids', []): for profile in kwargs.get('ids', []):
p = db.query(Profile).filter_by(id = profile).first() p = db.query(Profile).filter_by(id = profile).first()
p.hide = params.get('hidden')[order] p.hide = kwargs.get('hidden')[order]
p.order = order p.order = order
order += 1 order += 1
db.commit() db.commit()
return jsonified({ return {
'success': True 'success': True
}) }
def delete(self): def delete(self, id = None, **kwargs):
id = getParam('id')
db = get_session() db = get_session()
@@ -151,14 +152,15 @@ class ProfilePlugin(Plugin):
except Exception, e: except Exception, e:
message = log.error('Failed deleting Profile: %s', e) message = log.error('Failed deleting Profile: %s', e)
return jsonified({ db.expire_all()
return {
'success': success, 'success': success,
'message': message 'message': message
}) }
def fill(self): def fill(self):
db = get_session(); db = get_session()
profiles = [{ profiles = [{
'label': 'Best', 'label': 'Best',
@@ -13,11 +13,11 @@
.profile > .delete { .profile > .delete {
position: absolute; position: absolute;
padding: 25px 20px; padding: 16px;
background-position: center;
right: 0; right: 0;
cursor: pointer; cursor: pointer;
opacity: 0.6; opacity: 0.6;
color: #fd5353;
} }
.profile > .delete:hover { .profile > .delete:hover {
opacity: 1; opacity: 1;
@@ -29,6 +29,7 @@
.profile .qualities { .profile .qualities {
min-height: 80px; min-height: 80px;
padding-top: 0;
} }
.profile .formHint { .profile .formHint {
@@ -97,11 +98,13 @@
} }
.profile .types .type .delete { .profile .types .type .delete {
background-position: left center;
height: 20px; height: 20px;
width: 20px; width: 20px;
line-height: 20px;
visibility: hidden; visibility: hidden;
cursor: pointer; cursor: pointer;
font-size: 13px;
color: #fd5353;
} }
.profile .types .type:hover:not(.is_empty) .delete { .profile .types .type:hover:not(.is_empty) .delete {
@@ -24,7 +24,7 @@ var Profile = new Class({
var data = self.data; var data = self.data;
self.el = new Element('div.profile').adopt( self.el = new Element('div.profile').adopt(
self.delete_button = new Element('span.delete.icon', { self.delete_button = new Element('span.delete.icon2', {
'events': { 'events': {
'click': self.del.bind(self) 'click': self.del.bind(self)
} }
@@ -256,7 +256,7 @@ Profile.Type = new Class({
} }
}) })
), ),
new Element('span.delete.icon', { new Element('span.delete.icon2', {
'events': { 'events': {
'click': self.del.bind(self) 'click': self.del.bind(self)
} }
+67 -55
View File
@@ -2,13 +2,11 @@ from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParams
from couchpotato.core.helpers.variable import mergeDicts, md5, getExt from couchpotato.core.helpers.variable import mergeDicts, md5, getExt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Quality, Profile, ProfileType from couchpotato.core.settings.model import Quality, Profile, ProfileType
from sqlalchemy.sql.expression import or_ from sqlalchemy.sql.expression import or_
import os.path
import re import re
import time import time
@@ -19,13 +17,13 @@ class QualityPlugin(Plugin):
qualities = [ qualities = [
{'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]}, {'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]},
{'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']}, {'identifier': '1080p', 'hd': True, 'size': (4000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
{'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']}, {'identifier': '720p', 'hd': True, 'size': (3000, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi']}, {'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi'], 'tags': ['hdtv', 'hdrip', 'webdl', ('web', 'dl')]},
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']}, {'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']},
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]}, {'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': [], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg'], 'tags': ['webrip', ('web', 'rip')]},
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']} {'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}
@@ -52,12 +50,12 @@ class QualityPlugin(Plugin):
def preReleases(self): def preReleases(self):
return self.pre_releases return self.pre_releases
def allView(self): def allView(self, **kwargs):
return jsonified({ return {
'success': True, 'success': True,
'list': self.all() 'list': self.all()
}) }
def all(self): def all(self):
@@ -89,24 +87,22 @@ class QualityPlugin(Plugin):
if identifier == q.get('identifier'): if identifier == q.get('identifier'):
return q return q
def saveSize(self): def saveSize(self, **kwargs):
params = getParams()
db = get_session() db = get_session()
quality = db.query(Quality).filter_by(identifier = params.get('identifier')).first() quality = db.query(Quality).filter_by(identifier = kwargs.get('identifier')).first()
if quality: if quality:
setattr(quality, params.get('value_type'), params.get('value')) setattr(quality, kwargs.get('value_type'), kwargs.get('value'))
db.commit() db.commit()
return jsonified({ return {
'success': True 'success': True
}) }
def fill(self): def fill(self):
db = get_session(); db = get_session()
order = 0 order = 0
for q in self.qualities: for q in self.qualities:
@@ -156,61 +152,77 @@ class QualityPlugin(Plugin):
return True return True
def guess(self, files, extra = {}): def guess(self, files, extra = None):
if not extra: extra = {}
# Create hash for cache # Create hash for cache
hash = md5(str([f.replace('.' + getExt(f), '') for f in files])) cache_key = md5(str([f.replace('.' + getExt(f), '') for f in files]))
cached = self.getCache(hash) cached = self.getCache(cache_key)
if cached and extra is {}: return cached if cached and len(extra) == 0: return cached
qualities = self.all()
for cur_file in files: for cur_file in files:
size = (os.path.getsize(cur_file) / 1024 / 1024) if os.path.isfile(cur_file) else 0
words = re.split('\W+', cur_file.lower()) words = re.split('\W+', cur_file.lower())
for quality in self.all(): found = {}
for quality in qualities:
contains = self.containsTag(quality, words, cur_file)
if contains:
found[quality['identifier']] = True
# Check tags for quality in qualities:
# Check identifier
if quality['identifier'] in words: if quality['identifier'] in words:
log.debug('Found via identifier "%s" in %s', (quality['identifier'], cur_file)) if len(found) == 0 or len(found) == 1 and found.get(quality['identifier']):
return self.setCache(hash, quality) log.debug('Found via identifier "%s" in %s', (quality['identifier'], cur_file))
return self.setCache(cache_key, quality)
if list(set(quality.get('alternative', [])) & set(words)): # Check alt and tags
log.debug('Found %s via alt %s in %s', (quality['identifier'], quality.get('alternative'), cur_file)) contains = self.containsTag(quality, words, cur_file)
return self.setCache(hash, quality) if contains:
return self.setCache(cache_key, quality)
for tag in quality.get('tags', []):
if isinstance(tag, tuple) and '.'.join(tag) in '.'.join(words):
log.debug('Found %s via tag %s in %s', (quality['identifier'], quality.get('tags'), cur_file))
return self.setCache(hash, quality)
if list(set(quality.get('tags', [])) & set(words)):
log.debug('Found %s via tag %s in %s', (quality['identifier'], quality.get('tags'), cur_file))
return self.setCache(hash, quality)
# Try again with loose testing # Try again with loose testing
quality = self.guessLoose(hash, extra = extra) quality = self.guessLoose(cache_key, files = files, extra = extra)
if quality: if quality:
return self.setCache(hash, quality) return self.setCache(cache_key, quality)
log.debug('Could not identify quality for: %s', files) log.debug('Could not identify quality for: %s', files)
return None return None
def guessLoose(self, hash, extra): def containsTag(self, quality, words, cur_file = ''):
for quality in self.all(): # Check alt and tags
for tag_type in ['alternative', 'tags']:
for alt in quality.get(tag_type, []):
if isinstance(alt, tuple) and '.'.join(alt) in '.'.join(words):
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
return True
# Check width resolution, range 20 if list(set(quality.get(tag_type, [])) & set(words)):
if (quality.get('width', 720) - 20) <= extra.get('resolution_width', 0) <= (quality.get('width', 720) + 20): log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width', 720), extra.get('resolution_width', 0))) return True
return self.setCache(hash, quality)
# Check height resolution, range 20 return
if (quality.get('height', 480) - 20) <= extra.get('resolution_height', 0) <= (quality.get('height', 480) + 20):
log.debug('Found %s via resolution_height: %s == %s', (quality['identifier'], quality.get('height', 480), extra.get('resolution_height', 0)))
return self.setCache(hash, quality)
if 480 <= extra.get('resolution_width', 0) <= 720: def guessLoose(self, cache_key, files = None, extra = None):
log.debug('Found as dvdrip')
return self.setCache(hash, self.single('dvdrip')) if extra:
for quality in self.all():
# Check width resolution, range 20
if quality.get('width') and (quality.get('width') - 20) <= extra.get('resolution_width', 0) <= (quality.get('width') + 20):
log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width'), extra.get('resolution_width', 0)))
return self.setCache(cache_key, quality)
# Check height resolution, range 20
if quality.get('height') and (quality.get('height') - 20) <= extra.get('resolution_height', 0) <= (quality.get('height') + 20):
log.debug('Found %s via resolution_height: %s == %s', (quality['identifier'], quality.get('height'), extra.get('resolution_height', 0)))
return self.setCache(cache_key, quality)
if 480 <= extra.get('resolution_width', 0) <= 720:
log.debug('Found as dvdrip')
return self.setCache(cache_key, self.single('dvdrip'))
return None return None
@@ -41,7 +41,8 @@ var QualityBase = new Class({
self.settings.addEvent('create', function(){ self.settings.addEvent('create', function(){
var tab = self.settings.createSubTab('profile', { var tab = self.settings.createSubTab('profile', {
'label': 'Quality', 'label': 'Quality',
'name': 'profile' 'name': 'profile',
'subtab_label': 'Qualities'
}, self.settings.tabs.searcher ,'searcher'); }, self.settings.tabs.searcher ,'searcher');
self.tab = tab.tab; self.tab = tab.tab;
@@ -102,7 +103,8 @@ var QualityBase = new Class({
var profile_list; var profile_list;
var group = self.settings.createGroup({ var group = self.settings.createGroup({
'label': 'Profile Defaults' 'label': 'Profile Defaults',
'description': '(Needs refresh \'' +(App.isMac() ? 'CMD+R' : 'F5')+ '\' after editing)'
}).adopt( }).adopt(
new Element('.ctrlHolder#profile_ordering').adopt( new Element('.ctrlHolder#profile_ordering').adopt(
new Element('label[text=Order]'), new Element('label[text=Order]'),
+56 -23
View File
@@ -2,13 +2,14 @@ from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.encoding import ss from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.request import getParam, jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.plugins.scanner.main import Scanner from couchpotato.core.plugins.scanner.main import Scanner
from couchpotato.core.settings.model import File, Release as Relea, Movie from couchpotato.core.settings.model import File, Release as Relea, Movie
from sqlalchemy.orm import joinedload_all
from sqlalchemy.sql.expression import and_, or_ from sqlalchemy.sql.expression import and_, or_
import os import os
import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -36,7 +37,14 @@ class Release(Plugin):
'id': {'type': 'id', 'desc': 'ID of the release object in release-table'} 'id': {'type': 'id', 'desc': 'ID of the release object in release-table'}
} }
}) })
addApiView('release.for_movie', self.forMovieView, docs = {
'desc': 'Returns all releases for a movie. Ordered by score(desc)',
'params': {
'id': {'type': 'id', 'desc': 'ID of the movie'}
}
})
addEvent('release.for_movie', self.forMovie)
addEvent('release.delete', self.delete) addEvent('release.delete', self.delete)
addEvent('release.clean', self.clean) addEvent('release.clean', self.clean)
@@ -89,8 +97,8 @@ class Release(Plugin):
added_files = db.query(File).filter(or_(*[File.id == x for x in added_files])).all() added_files = db.query(File).filter(or_(*[File.id == x for x in added_files])).all()
rel.files.extend(added_files) rel.files.extend(added_files)
db.commit() db.commit()
except Exception, e: except:
log.debug('Failed to attach "%s" to release: %s', (cur_file, e)) log.debug('Failed to attach "%s" to release: %s', (added_files, traceback.format_exc()))
fireEvent('movie.restatus', movie.id) fireEvent('movie.restatus', movie.id)
@@ -108,13 +116,11 @@ class Release(Plugin):
# Check database and update/insert if necessary # Check database and update/insert if necessary
return fireEvent('file.add', path = filepath, part = fireEvent('scanner.partnumber', file, single = True), type_tuple = Scanner.file_types.get(type), properties = properties, single = True) return fireEvent('file.add', path = filepath, part = fireEvent('scanner.partnumber', file, single = True), type_tuple = Scanner.file_types.get(type), properties = properties, single = True)
def deleteView(self): def deleteView(self, id = None, **kwargs):
release_id = getParam('id') return {
'success': self.delete(id)
return jsonified({ }
'success': self.delete(release_id)
})
def delete(self, id): def delete(self, id):
@@ -146,25 +152,23 @@ class Release(Plugin):
return False return False
def ignore(self): def ignore(self, id = None, **kwargs):
db = get_session() db = get_session()
id = getParam('id')
rel = db.query(Relea).filter_by(id = id).first() rel = db.query(Relea).filter_by(id = id).first()
if rel: if rel:
ignored_status, available_status = fireEvent('status.get', ['ignored', 'available'], single = True) ignored_status, failed_status, available_status = fireEvent('status.get', ['ignored', 'failed', 'available'], single = True)
rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id') rel.status_id = available_status.get('id') if rel.status_id in [ignored_status.get('id'), failed_status.get('id')] else ignored_status.get('id')
db.commit() db.commit()
return jsonified({ return {
'success': True 'success': True
}) }
def download(self): def download(self, id = None, **kwargs):
db = get_session() db = get_session()
id = getParam('id')
snatched_status, done_status = fireEvent('status.get', ['snatched', 'done'], single = True) snatched_status, done_status = fireEvent('status.get', ['snatched', 'done'], single = True)
@@ -179,8 +183,12 @@ class Release(Plugin):
# Get matching provider # Get matching provider
provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True) provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True)
if item['type'] != 'torrent_magnet': if not item.get('protocol'):
item['download'] = provider.download item['protocol'] = item['type']
item['type'] = 'movie'
if item.get('protocol') != 'torrent_magnet':
item['download'] = provider.loginDownload if provider.urls.get('login') else provider.download
success = fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({ success = fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({
'profile': {'types': {'quality': {}}}, 'profile': {'types': {'quality': {}}},
@@ -199,12 +207,37 @@ class Release(Plugin):
fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Successfully snatched "%s"' % item['name']) fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Successfully snatched "%s"' % item['name'])
return jsonified({ return {
'success': success 'success': success
}) }
else: else:
log.error('Couldn\'t find release with id: %s', id) log.error('Couldn\'t find release with id: %s', id)
return jsonified({ return {
'success': False 'success': False
}) }
def forMovie(self, id = None):
db = get_session()
releases_raw = db.query(Relea) \
.options(joinedload_all('info')) \
.options(joinedload_all('files')) \
.filter(Relea.movie_id == id) \
.all()
releases = [r.to_dict({'info':{}, 'files':{}}) for r in releases_raw]
releases = sorted(releases, key = lambda k: k['info'].get('score', 0), reverse = True)
return releases
def forMovieView(self, id = None, **kwargs):
releases = self.forMovie(id)
return {
'releases': releases,
'success': True
}
+22 -5
View File
@@ -14,15 +14,20 @@ rename_options = {
'year': 'Year (2011)', 'year': 'Year (2011)',
'first': 'First letter (M)', 'first': 'First letter (M)',
'quality': 'Quality (720p)', 'quality': 'Quality (720p)',
'quality_type': '(HD) or (SD)',
'video': 'Video (x264)', 'video': 'Video (x264)',
'audio': 'Audio (DTS)', 'audio': 'Audio (DTS)',
'group': 'Releasegroup name', 'group': 'Releasegroup name',
'source': 'Source media (Bluray)', 'source': 'Source media (Bluray)',
'resolution_width': 'resolution width (1280)',
'resolution_height': 'resolution height (720)',
'audio_channels': 'audio channels (7.1)',
'original': 'Original filename', 'original': 'Original filename',
'original_folder': 'Original foldername', 'original_folder': 'Original foldername',
'imdb_id': 'IMDB id (tt0123456)', 'imdb_id': 'IMDB id (tt0123456)',
'cd': 'CD number (cd1)', 'cd': 'CD number (cd1)',
'cd_nr': 'Just the cd nr. (1)', 'cd_nr': 'Just the cd nr. (1)',
'mpaa': 'MPAA Rating',
}, },
} }
@@ -50,7 +55,7 @@ config = [{
{ {
'name': 'to', 'name': 'to',
'type': 'directory', 'type': 'directory',
'description': 'Folder where the movies should be moved to.', 'description': 'Default folder where the movies are moved to.',
}, },
{ {
'name': 'folder_name', 'name': 'folder_name',
@@ -68,6 +73,12 @@ config = [{
'type': 'choice', 'type': 'choice',
'options': rename_options 'options': rename_options
}, },
{
'name': 'unrar',
'type': 'bool',
'description': 'Extract rar files if found.',
'default': False,
},
{ {
'name': 'cleanup', 'name': 'cleanup',
'type': 'bool', 'type': 'bool',
@@ -109,16 +120,22 @@ config = [{
{ {
'advanced': True, 'advanced': True,
'name': 'separator', 'name': 'separator',
'label': 'Separator', 'label': 'File-Separator',
'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.',
},
{
'advanced': True,
'name': 'foldersep',
'label': 'Folder-Separator',
'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.', 'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.',
}, },
{ {
'name': 'file_action', 'name': 'file_action',
'label': 'Torrent File Action', 'label': 'Torrent File Action',
'default': 'move', 'default': 'link',
'type': 'dropdown', 'type': 'dropdown',
'values': [('Move', 'move'), ('Copy', 'copy'), ('Hard link', 'hardlink'), ('Sym link', 'symlink'), ('Move & Sym link', 'move_symlink')], 'values': [('Link', 'link'), ('Copy', 'copy'), ('Move', 'move')],
'description': 'Define which kind of file operation you want to use for torrents. Before you start using <a href="http://en.wikipedia.org/wiki/Hard_link">hard links</a> or <a href="http://en.wikipedia.org/wiki/Sym_link">sym links</a>, PLEASE read about their possible drawbacks.', 'description': '<strong>Link</strong> or <strong>Copy</strong> after downloading completed (and allow for seeding), or <strong>Move</strong> after seeding completed. Link first tries <a href="http://en.wikipedia.org/wiki/Hard_link">hard link</a>, then <a href="http://en.wikipedia.org/wiki/Sym_link">sym link</a> and falls back to Copy.',
'advanced': True, 'advanced': True,
}, },
{ {

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