Compare commits

..
235 Commits
Author SHA1 Message Date
Ruud 796aff4514 Remove login_opener 2014-01-11 13:58:15 +01:00
Ruud 2a2fe448e7 Merge branch 'refs/heads/develop' into tv
Conflicts:
	couchpotato/core/media/movie/_base/main.py
	couchpotato/core/providers/torrent/bitsoup/main.py
	couchpotato/core/providers/torrent/iptorrents/main.py
	couchpotato/core/providers/torrent/sceneaccess/main.py
	couchpotato/core/providers/torrent/torrentleech/main.py
	couchpotato/core/providers/torrent/torrentshack/main.py
2014-01-11 13:55:12 +01:00
Ruud Burger 23967d11dd Merge pull request #2681 from fuzeman/tv_searcher
[TV] Cleanup retrieval of media query and title
2014-01-11 04:02:04 -08:00
Dean Gardiner 7a3251f649 Added new version of 'library.title' to return the title of the media excluding year and identifiers. 2014-01-11 16:36:29 +13:00
Dean Gardiner 9ba8910281 Renamed 'library.title' to 'library.query' 2014-01-11 16:23:33 +13:00
Dean Gardiner e83a3cf263 Renamed movie library.title 'include_identifier' to 'include_year', show library.title defaults to 'condense' enabled now. 2014-01-11 15:55:20 +13:00
Dean Gardiner b3c2945d9b 'related_libraries' and 'root_library' references are now added to child libraries. 2014-01-11 15:11:32 +13:00
Dean Gardiner fc3cf08675 Moved 'searcher.get_search_title' to 'library.title', include_identifier is enabled by default now and title condensing can be enabled by the 'condense' parameter now. 2014-01-11 15:11:31 +13:00
Ruud Burger 8162cd31b7 Merge pull request #2652 from nikagl/patch-1
Library object has media instead of movies
2014-01-06 13:27:12 -08:00
nikagl 1ea6fdc9a7 Library object has media instead of movies
Make the renamer work again by scanning the media instead of non-existent movies attribute in the library object (fixing error: AttributeError: 'Library' object has no attribute 'movies')
2014-01-01 20:54:34 +01:00
Ruud Burger 1b0c9f40cc Merge pull request #2647 from nikagl/patch-1
Update main.py
2013-12-31 02:21:41 -08:00
nikagl c0111a467b Update main.py
Release table has media_id column, not movie_id
2013-12-31 11:02:32 +01:00
Ruud Burger 64175151f8 Merge pull request #2634 from dkboy/tv_bitsoup
Updated Bitsoup provider to include TV support
2013-12-29 15:35:50 -08:00
dkboy 586957e840 Updated Bitsoup provider to include TV support
Updated Bitsoup Provider to include TV support as well as Movies.
2013-12-28 21:30:49 +13:00
Joel Kåberg f2fc775963 Revert "Merge branches 'develop' and 'tv' of https://github.com/RuudBurger/CouchPotatoServer into tv"
This reverts commit b8bce948c8, reversing
changes made to 0a996857dd.
2013-12-20 02:29:15 +01:00
Joel Kåberg b8bce948c8 Merge branches 'develop' and 'tv' of https://github.com/RuudBurger/CouchPotatoServer into tv
Conflicts:
	couchpotato/core/media/movie/_base/main.py
	couchpotato/core/providers/torrent/yify/main.py
2013-12-20 02:15:08 +01:00
Joel Kåberg 0a996857dd Merge branches 'develop' and 'tv' of https://github.com/RuudBurger/CouchPotatoServer into tv
Conflicts:
	couchpotato/core/plugins/release/main.py
	couchpotato/core/plugins/renamer/main.py
2013-12-19 20:57:12 +01:00
Joel Kåberg 26509f614c use identifier instead 2013-12-15 11:12:47 +01:00
Joel Kåberg 3e28d5a936 use year as identifier for movies 2013-12-15 11:02:57 +01:00
Joel Kåberg 95ff427873 ignore series from omdbapi (for now?) 2013-12-15 10:05:51 +01:00
Joel Kåberg 8ed10037df Merge pull request #2602 from saxicek/tv_tsh
update TorrentShack for tv branch
2013-12-15 00:37:59 -08:00
sax 7a090dd4a2 update TorrentShack for tv branch 2013-12-15 00:08:18 +01:00
Joel Kåberg 49f34cb48d movie > media 2013-12-14 23:57:20 +01:00
Joel Kåberg 2a76de50dd Merge branches 'develop' and 'tv' of https://github.com/RuudBurger/CouchPotatoServer into tv
Conflicts:
	couchpotato/core/plugins/renamer/main.py
2013-12-14 23:43:29 +01:00
Joel Kåberg 8adf7fc600 Merge remote-tracking branch 'remotes/origin/develop' into tv 2013-12-14 21:24:07 +01:00
Joel Kåberg f4c053f56f fix season search for SCC provider 2013-12-14 21:22:46 +01:00
Joel Kåberg 5cb5a1677d Merge branch 'tv' of https://github.com/RuudBurger/CouchPotatoServer into tv 2013-12-14 20:35:03 +01:00
Joel Kåberg 9fb9f0ef5b Merge pull request #2599 from fuzeman/tv_searcher
[TV] Moved matcher to core/media, updated NZBIndex provider
2013-12-13 08:11:10 -08:00
Dean Gardiner 242d69a981 The nzbindex provider now uses the caper usenet parser to get release names from usenet subjects. 2013-12-13 16:31:26 +13:00
Dean Gardiner eb151a4c5d Updated Caper to v0.3.1 2013-12-13 15:24:40 +13:00
Dean Gardiner 2520b19798 Fixed bug in searcher where episode searches would be triggered if a season release has already been snatched at a better quality 2013-12-09 13:33:25 +13:00
Dean Gardiner 319c9e979a Split ShowMatcher into Episode and Season matchers, updated correctIdentifier method so there should be less false matches now. 2013-12-09 13:32:43 +13:00
Dean Gardiner 93aa5b1920 Updated Caper to v0.2.9 2013-12-09 11:23:57 +13:00
Dean Gardiner f648af66a6 Moved matcher plugin to core/media, moved some matcher related functions from ShowSearcher to ShowMatcher 2013-12-08 22:40:39 +13:00
Joel Kåberg 7c4185e1fa Merge branch 'develop' into tv 2013-12-07 23:40:42 +01:00
Joel Kåberg 0fb06a3fd3 Merge pull request #2577 from fuzeman/tv_searcher
[TV] Season pack matching, better show search triggering
2013-12-07 01:10:27 -08:00
Dean Gardiner 1e39d643a8 Searching for a show now triggers searching for all the seasons, Season searches that fail to find anything now trigger individual episode searches. 2013-12-07 21:23:10 +13:00
Dean Gardiner 69d58663ef Profile and categories on seasons and episodes are now set to the same value as the Show 2013-12-07 21:21:15 +13:00
Dean Gardiner e59b53fab2 Searching is now deferred until the entire show has been loaded into the database 2013-12-07 18:48:26 +13:00
Dean Gardiner a66f6f0166 Fixed reference to 'movie.restatus' (should be 'media.restatus'), minor formatting changes 2013-12-07 18:11:06 +13:00
Dean Gardiner 1344f03b16 Fixed matcher bug when matching resolution on ['480p', None] 2013-12-07 18:11:05 +13:00
Dean Gardiner a23c409939 Updated Caper to v0.2.6 2013-12-07 18:11:05 +13:00
Dean Gardiner a6b1cc833f Added more TV qualities for testing (arh.. bit of a mess) 2013-12-07 18:11:04 +13:00
Joel Kåberg d2c7e3ef56 update Nzbindex for tv branch 2013-12-06 12:55:02 +01:00
Joel Kåberg 6c87008d7b update Nzbclub for tv branch 2013-12-06 12:46:36 +01:00
Joel Kåberg 6b3af21e45 update Binsearch for tv branch 2013-12-06 12:38:54 +01:00
Joel Kåberg 5a5cc0005c Merge pull request #2574 from fuzeman/tv_searcher
[TV] Matching, serialization and UI notification fixes
2013-12-06 03:18:04 -08:00
Joel Kåberg d65117c0e3 update TorrentPotato for tv branch 2013-12-06 12:16:58 +01:00
Dean Gardiner d8884bb655 Changed '.searcher.single' call to use search_dict for media serialization 2013-12-06 23:58:47 +13:00
Dean Gardiner afe9aed2eb Fixed bug where media default_dict contained related and root library attributes. 2013-12-06 23:58:45 +13:00
Dean Gardiner 01e64e989e Updated Caper to v0.2.5 - fixes 'H 264' tag bug 2013-12-06 23:58:44 +13:00
Dean Gardiner 9496df9e9d Fixed a bug where matching show names with a year would fail 2013-12-06 23:58:43 +13:00
Joel Kåberg 8b4c67b977 update Yify for tv branch 2013-12-06 11:44:59 +01:00
Joel Kåberg f77a8f5573 update PassThePopcorn for tv branch 2013-12-06 11:40:58 +01:00
Joel Kåberg de8aefebb7 update Bit-HDTV for tv branch 2013-12-06 11:03:24 +01:00
Joel Kåberg 8f0d22a6f2 update TPB for tv branch 2013-12-06 10:46:08 +01:00
Joel Kåberg 721190028b not needed 2013-12-06 09:34:02 +01:00
Joel Kåberg 50e565142e typo 2013-12-06 09:27:02 +01:00
Joel Kåberg bead3e2b07 update PublicHD for tv branch 2013-12-06 09:26:20 +01:00
Joel Kåberg 71aa0cbb9a use buildUrl 2013-12-06 09:09:53 +01:00
Joel Kåberg 8de19cbd52 fixes 2013-12-06 08:19:13 +01:00
Joel Kåberg 8573832ff7 fixes 2013-12-06 08:05:34 +01:00
Joel Kåberg 7c1d3f8762 fixes 2013-12-06 08:05:15 +01:00
Joel Kåberg 9cd1adcdee fixes 2013-12-06 08:04:58 +01:00
Joel Kåberg f017ac9dca use searcher.get_search_title 2013-12-06 07:46:50 +01:00
Joel Kåberg 907704e45f fix self.getSearchTitle() 2013-12-06 07:44:16 +01:00
Joel Kåberg b17f937389 use include_identifier 2013-12-06 07:40:15 +01:00
Joel Kåberg f591c56dd4 Merge pull request #2570 from fuzeman/tv_searcher
[TV] WEB-DL matcher fix, updated 'searcher.get_search_title'
2013-12-05 22:14:24 -08:00
Dean Gardiner 2fd54901e7 Added optional parameter 'include_identifier' to the 'searcher.get_search_title' event handler. 2013-12-06 14:04:00 +13:00
Dean Gardiner 1bf6c5a82e Changed 'searcher.get_search_title' to accept a 'library' instead of the 'media' as a parameter. 2013-12-06 13:53:56 +13:00
Dean Gardiner 45484461b5 Adjusted Matcher.chainMatch to support 'WEB DL' tags 2013-12-06 13:50:01 +13:00
Dean Gardiner aa394f59ae Updated Caper to v0.2.4 2013-12-06 13:50:00 +13:00
Joel Kåberg 717111f5d2 cleanup Newznab provider 2013-12-05 21:11:45 +01:00
Joel Kåberg e3461dc35f updated TorrentDay for tv branch 2013-12-05 17:13:34 +01:00
Joel Kåberg 9b834f62a9 updated Torrentleech for tv branch 2013-12-05 16:55:34 +01:00
Joel Kåberg 935938474c SCC Provider: remove debug info 2013-12-05 16:23:07 +01:00
Joel Kåberg 6573196186 update SCC for tv branch 2013-12-05 16:21:58 +01:00
Joel Kåberg 9a07f2ed65 use searcher.get_search_title and library.identifier (not present in movie library module?) 2013-12-05 15:40:21 +01:00
Joel Kåberg 613ff3b729 updated newznab provider for tv branch. see inline comments 2013-12-05 14:09:35 +01:00
Ruud Burger def62fc865 Merge pull request #2568 from fuzeman/tv_searcher
[TV] Fixed bug with Library serialization when adding shows
2013-12-05 00:42:13 -08:00
Dean Gardiner 037c355836 Fixed bug with Library serialization when adding shows 2013-12-05 16:23:21 +13:00
Joel Kåberg 180b2bbffe Merge pull request #2549 from fuzeman/tv_searcher
[TV] Searcher cleanup and matcher updates
2013-12-03 23:16:11 -08:00
Dean Gardiner 143dcad4f3 Fixed incorrect reference to library 'season' and 'episode' attributes. 2013-12-04 19:50:48 +13:00
Dean Gardiner b0e352ab6d Updated Caper to v0.2.3 and Logr to v0.2.2 to greatly improve matching performance 2013-12-04 19:29:02 +13:00
Dean Gardiner 5ea7dc5920 Moved 'searcher.get_media_identifier' into season and episode libraries as 'library.identifier' 2013-12-04 17:15:08 +13:00
Dean Gardiner 3c675b5b8a searcher and matcher now uses the new related_libraries and root_library from media instead of using extra db queries 2013-12-02 23:27:26 +13:00
Dean Gardiner 11ea9b4e91 related_libraries are now only included on searches and added the root_library attribute 2013-12-02 23:26:31 +13:00
Dean Gardiner e8a2139ecf Related libraries are now merged into {<type>: [<library>,...]} type 2013-12-02 21:18:48 +13:00
Dean Gardiner dc57d7b6d1 Added related_libraries to Library model. 2013-12-01 20:25:33 +13:00
Dean Gardiner 0925f1312d Fixed refresh action - changed show searcher to bind to 'season' and 'episode' media types for '.searcher.single' as well. 2013-12-01 20:22:07 +13:00
Dean Gardiner efc02f66f5 Changed the IPTorrents show provider into a new season and episode provider, removed grouped cat_ids 2013-12-01 20:20:19 +13:00
Ruud 9ce8ffc14b movie_id > media_id 2013-11-30 16:52:08 +01:00
Ruud bab07a05e7 Merge branch 'refs/heads/develop' into tv 2013-11-30 16:48:52 +01:00
Ruud 1df9f7c83f Merge branch 'refs/heads/develop' into tv 2013-11-30 16:14:19 +01:00
Ruud efdf77ef6c Merge branch 'refs/heads/develop' into tv
Conflicts:
	couchpotato/core/media/movie/_base/main.py
	couchpotato/core/plugins/release/main.py
	couchpotato/core/plugins/renamer/main.py
2013-11-30 12:44:13 +01:00
Ruud Burger a989c93505 Merge pull request #2523 from fuzeman/tv_searcher
[TV] Merge fixes, removed get_media_searcher_id event
2013-11-25 07:38:08 -08:00
Dean Gardiner d122bd1b43 Removed 'searcher.download' (method was moved to the release plugin) 2013-11-25 19:47:05 +13:00
Dean Gardiner ab81824f4c Minor changes to matcher and added extra show searcher logging 2013-11-25 19:29:29 +13:00
Dean Gardiner 4eb73e3609 Renamed Release.movie references to Release.media 2013-11-25 19:28:43 +13:00
Dean Gardiner 6bcb279f0e Updated Caper library 2013-11-25 17:37:08 +13:00
Dean Gardiner f446c8ed33 Updated QueryCondenser library 2013-11-25 17:07:54 +13:00
Dean Gardiner 10a34f2b69 Removed the use of the 'searcher.get_media_searcher_id' event 2013-11-25 16:20:03 +13:00
Ruud cc3ebd79e8 Remove extensions from qualities 2013-11-24 23:17:18 +01:00
Ruud 3e035f84b1 Merge branch 'refs/heads/develop' into tv
Conflicts:
	couchpotato/core/helpers/variable.py
	couchpotato/core/media/_base/searcher/main.py
	couchpotato/core/media/movie/searcher/main.py
	couchpotato/core/plugins/quality/main.py
	couchpotato/core/plugins/release/main.py
	couchpotato/core/plugins/renamer/main.py
2013-11-24 23:13:25 +01:00
Joel Kåberg 611c159373 Merge pull request #2356 from fuzeman/tv_searcher
[TV][Searcher] Release Matching and Snatching
2013-10-15 22:16:35 -07:00
Joel Kåberg db65980ba4 Merge pull request #2354 from nrgaway/tv_xem
Tv xem
2013-10-15 21:51:41 -07:00
Dean Gardiner 180576f2b7 Minor change to ShowSearcher.correctMatch logging 2013-10-16 14:58:53 +13:00
Dean Gardiner 46d4d34da7 Minor cleanup to Searcher and Matcher 2013-10-16 14:58:52 +13:00
Dean Gardiner 3fa21560be Moved 'searcher.create_releases' from Searcher to Release. 2013-10-16 14:58:51 +13:00
Dean Gardiner b902186389 Cleaned up usage of helper functions 2013-10-16 14:58:50 +13:00
Dean Gardiner da87e68fad Implemented basic usage of QueryCondenser 2013-10-16 14:58:49 +13:00
Dean Gardiner f23412ea7e Added qcond (Query Condenser) v0.1.0 library - https://github.com/fuzeman/QueryCondenser 2013-10-16 14:58:48 +13:00
Dean Gardiner 07abf7c83d Updated Caper to version 0.2.2 2013-10-16 14:58:47 +13:00
Dean Gardiner 6259684487 Moved caper matching into a new 'matcher' plugin. 2013-10-16 14:58:47 +13:00
Dean Gardiner 0a0935d635 Fix to Provider getCatId when returning the cet_backup_id 2013-10-16 14:58:46 +13:00
Dean Gardiner fb5b17005f Cleaned up status.get calls in TV searcher 2013-10-16 14:58:45 +13:00
Dean Gardiner e3745b5d74 Updated Caper library 2013-10-16 14:58:44 +13:00
Dean Gardiner 8d24d96804 Implemented 'searcher.get_media_searcher_id' in the TV searcher. 2013-10-16 14:58:43 +13:00
Dean Gardiner 529b535d9f Added 'searcher.get_media_searcher_id' event, Cleaned up some 'status.get' calls, Renamed some references of 'nzb' to 'rel'. 2013-10-16 14:58:42 +13:00
Dean Gardiner 0793668e5c Chain result weight now returned from TV searcher correctRelease function. 2013-10-16 14:58:41 +13:00
Dean Gardiner 8d368ecf29 'searcher.correct_release' can now return a float indicating the weight/accuracy which is used to scale the score. Fix to IPT _buildUrl method. 2013-10-16 14:58:40 +13:00
Dean Gardiner 2d2b0c9048 IPT provider now searches in multiple categories. 2013-10-16 14:58:40 +13:00
Dean Gardiner fb0719d677 TV Searcher now supports xem scene mappings 2013-10-16 14:58:39 +13:00
Dean Gardiner 7ffa5dc7b6 Fixed IPT Show SD cat_ids 2013-10-16 14:58:38 +13:00
Dean Gardiner 32c289fd3d Renamed 'movie' -> 'media' in 'searcher.download' 2013-10-16 14:58:37 +13:00
Dean Gardiner ff63b8a1c5 Added TV release snatching/downloading 2013-10-16 14:58:36 +13:00
Dean Gardiner 60d8934444 Created 'searcher.try_download_result' event from section in MovieSearcher.single 2013-10-16 14:58:35 +13:00
Jason Mehring e0aba01866 more tvdb info provider guards 2013-10-15 17:54:06 -04:00
Jason Mehring 1ae498e3c8 Merge branch 'tv' of https://github.com/RuudBurger/CouchPotatoServer into tv_xem 2013-10-15 16:25:05 -04:00
Jason Mehring d1db099f71 grab tvdb fields more defensively 2013-10-15 16:24:49 -04:00
Ruud Burger f4ef64290d Merge pull request #2352 from fuzeman/tv
[TV] Fixed show searching (broken in search merge)
2013-10-15 12:20:29 -07:00
Dean Gardiner 026151d1a1 Fixed show searching (broken in search merge) 2013-10-15 15:28:35 +13:00
Ruud 70dada8ef6 Merge branch 'refs/heads/develop' into tv
Conflicts:
	couchpotato/core/media/_base/media/main.py
	couchpotato/core/media/_base/searcher/main.py
	couchpotato/core/media/movie/_base/main.py
	couchpotato/core/media/movie/searcher/main.py
2013-10-08 10:02:40 +02:00
Ruud 9ef752f8a3 Rename mediaplugin 2013-10-08 09:22:20 +02:00
Ruud d265a5bddd Remove refresh from movie media 2013-10-08 08:48:38 +02:00
Ruud b2b6e3eb33 Cleanup show media 2013-10-08 08:46:04 +02:00
Ruud 2b6c7a8f94 Move media refresh to media plugin 2013-10-08 08:45:45 +02:00
Ruud 6070209d33 Attach shows to searcher 2013-10-07 23:37:17 +02:00
Ruud fa78d18890 Merge searches 2013-10-07 23:23:46 +02:00
Ruud 40eaf2a96b Merge branch 'refs/heads/develop' into tv
Conflicts:
	couchpotato/core/media/movie/_base/static/search.js
2013-10-07 23:12:59 +02:00
Ruud 73dd0916c0 Merge branch 'tv' of github.com:RuudBurger/CouchPotatoServer into tv 2013-10-03 21:20:24 +02:00
Joel Kåberg 77d32fe16b Merge pull request #2292 from nrgaway/tv_xem
Tv xem
2013-10-03 08:17:26 -07:00
Jason Mehring 7def0944a6 Implemented map_absolute. model was changed to implement. map_names now stores in EpisodeLibrary 2013-10-03 04:30:05 -04:00
Ruud 8782cd77d5 Import cleanup 2013-10-03 08:21:37 +02:00
Ruud 1b59fd9af0 Merge branch 'refs/heads/develop' into tv
Conflicts:
	couchpotato/core/media/_base/searcher/main.py
	couchpotato/core/plugins/renamer/main.py
2013-10-03 08:17:20 +02:00
Joel Kåberg 9dca8a03be Merge pull request #2290 from nrgaway/tv_xem
Tv xem
2013-10-02 05:11:10 -07:00
Jason Mehring 132f4882e5 Merge branch 'tv' of https://github.com/RuudBurger/CouchPotatoServer into tv_xem 2013-10-02 01:29:55 -04:00
Jason Mehring 9e32a38288 fixed bug that was not storing xem maps in EpisodeLibrary.info 2013-10-02 01:29:21 -04:00
Joel Kåberg c1b13cd076 Merge pull request #2284 from fuzeman/tv_searcher
[TV][Searcher] TV searching, parsing and Release creation
2013-10-01 03:29:51 -07:00
Dean Gardiner 820588aa5f Created 'searcher.create_releases' event to replace some shared functionality, releases are now created for TV search results. 2013-09-30 23:08:32 +13:00
Dean Gardiner 8fbf050510 Created 'searcher.search' event to replace some shared functionality, Fixed an issue in Release.download when snatching movies. 2013-09-30 22:49:54 +13:00
Dean Gardiner dd5ae3c4ee Working TV correctRelease function with quality, identifier and show title checking. 2013-09-30 22:03:21 +13:00
Dean Gardiner ab51707607 Added Caper (0.2.0-master) and Logr (0.2.1) libraries 2013-09-30 18:40:27 +13:00
Dean Gardiner 8acdc56df1 Added the start of the ShowSearcher correctRelease function 2013-09-30 14:53:33 +13:00
Dean Gardiner d345a05b3c Switched IPTorrents provider to the MultiProvider layout, few fixes to provider base for MultiProvider 2013-09-30 14:53:32 +13:00
Dean Gardiner 5f427ec6ea Moved required/ignored word checking from 'correctMovie' into 'searcher.correct_words' event, Renamed 'movie.searcher.correct_movie' to 'searcher.correct_release' 2013-09-30 14:53:32 +13:00
Dean Gardiner a95c030885 Fix for discovering the cat_ids structure when the 'ids' are of str type. 2013-09-30 14:49:42 +13:00
Dean Gardiner bef6a74dfe Minor cleanup to getSearchTitle 2013-09-30 14:49:41 +13:00
Dean Gardiner 01da470c21 Few changes to getSearchTitle in case a title isn't found, Added check to ensure enough media was returned from _lookupMedia 2013-09-30 14:49:40 +13:00
Dean Gardiner 5fdf4d9085 Extended providers to support multiple media types
- 'cat_ids' now support media type groups
  - 'type' extended to allow a list of support media types
  - Added 'searcher.get_search_title' to return a title for media to be used in searches.
2013-09-30 14:49:40 +13:00
Dean Gardiner bc51e263e1 Switched back to a single search method 'show.searcher.single' 2013-09-30 14:47:51 +13:00
Dean Gardiner 4c527f0931 Added 'show.refresh' API method and the base for season and episode searching. 2013-09-30 14:47:50 +13:00
Ruud e9fc528a0f movie_id > media_id 2013-09-23 22:23:45 +02:00
Ruud c9ba3c804e Merge branch 'refs/heads/develop' into tv
Conflicts:
	couchpotato/core/plugins/dashboard/main.py
	couchpotato/core/plugins/renamer/main.py
	couchpotato/core/providers/torrent/sceneaccess/main.py
2013-09-23 22:14:06 +02:00
Joel Kåberg ee9fe347c7 Merge pull request #2155 from nrgaway/tv_xem
Tv xem
2013-09-16 12:39:15 -07:00
Jason Mehring 515aafe112 bug fixes for add show 2013-09-13 22:02:22 -04:00
Jason Mehring 314016e1fa (WIP) Started intergrating xem 2013-09-13 03:28:03 -04:00
Jason Mehring 906a54ef09 Finished creating xem info provider 2013-09-13 03:11:51 -04:00
Jason Mehring ec2facd056 Fix reference to Movie, its now Media 2013-09-13 03:10:22 -04:00
Jason Mehring ddbfef575f Merge branch 'tv' of https://github.com/RuudBurger/CouchPotatoServer into tv_tvdb 2013-09-12 01:30:57 -04:00
Jason Mehring 8dd7a4771c partially implemented xem info provider (wip) 2013-09-12 01:30:38 -04:00
Jason Mehring 49ba1f1acd Reworked code to allow better intergration of other info providers. Initial prep for xem mapping 2013-09-12 01:00:14 -04:00
Ruud c4d661535c Movie > Media 2013-09-11 23:25:51 +02:00
Ruud bd52ab7ab1 Movie > Media 2013-09-11 23:18:31 +02:00
Ruud cce0a8ec62 Merge branch 'refs/heads/develop' into tv
Conflicts:
	couchpotato/core/media/movie/_base/main.py
	couchpotato/core/media/movie/library/movie/main.py
	couchpotato/core/plugins/dashboard/main.py
	couchpotato/core/plugins/profile/main.py
	couchpotato/core/plugins/release/main.py
	couchpotato/core/plugins/suggestion/main.py
2013-09-11 23:13:28 +02:00
Joel Kåberg d02e62f89f Merge pull request #2139 from nrgaway/tv_tvdb
Tv tvdb
2013-09-11 07:52:42 -07:00
Jason Mehring e180addc3c added posters for seasons 2013-09-11 00:36:12 -04:00
Jason Mehring a37a4a8cd4 thetvdb, add alternate titles if they exist 2013-09-10 23:45:15 -04:00
Jason Mehring 8328c18728 set cache directory for thetvdb_api 2013-09-10 22:08:17 -04:00
Jason Mehring 7ae07d6c15 Opps, remove debug code for language 2013-09-10 21:58:47 -04:00
Jason Mehring 770bcf5bc6 Added ability to search thetvdb by language 2013-09-10 21:51:38 -04:00
Jason Mehring 7bd6a295d8 return False on fail. Everything caches now 2013-09-10 21:06:44 -04:00
Jason Mehring 4063761313 Changed model to accept Unicode value for airs_time and add last_updated field for episode. Now stores both as well as airs_daysofweek 2013-09-10 21:04:44 -04:00
Jason Mehring d62b346a74 Merge branch 'tv' of https://github.com/RuudBurger/CouchPotatoServer into tv_tvdb 2013-09-06 13:16:38 -04:00
Ruud 155732ab1a Rollback type remove 2013-09-02 00:13:05 +02:00
Ruud b3713b7ae5 Merge branch 'refs/heads/develop' into tv 2013-09-02 00:09:16 +02:00
Jason Mehring 19d026756c Merge branch 'tv' of https://github.com/RuudBurger/CouchPotatoServer into tv_tvdb 2013-08-24 21:11:53 -04:00
Ruud 3cddd29425 Merge branch 'refs/heads/develop' into tv 2013-08-25 01:15:40 +02:00
Jason Mehring 23bde0b866 Merge branch 'tv' of https://github.com/RuudBurger/CouchPotatoServer into tv_tvdb 2013-08-24 15:53:31 -04:00
Jason Mehring 6f895c1805 get apikey from config 2013-08-24 15:37:11 -04:00
Ruud 96089074ce Merge branch 'refs/heads/develop' into tv
Conflicts:
	couchpotato/core/loader.py
2013-08-24 20:22:55 +02:00
Ruud 2ed53df008 Import cleanup 2013-08-24 19:14:09 +02:00
Ruud 060859483a Delete show provider 2013-08-24 18:36:44 +02:00
Ruud eced476eaf Merge branch 'refs/heads/develop' into tv
Conflicts:
	couchpotato/core/loader.py
	couchpotato/core/providers/info/_modifier/__init__.py
	couchpotato/core/providers/info/_modifier/main.py
	couchpotato/core/providers/movie/_modifier/main.py
	couchpotato/core/providers/show/_modifier/main.py
2013-08-24 18:30:00 +02:00
Ruud 8d5b55a753 Make info modifier multiprovider 2013-08-24 15:30:17 +02:00
Ruud 7296dc54d0 Move thetvdb to info providers 2013-08-24 15:29:57 +02:00
Ruud e5e9cf7d5f Move info providers to proper folder 2013-08-24 15:20:00 +02:00
Ruud b106229a78 Merge branch 'refs/heads/develop' into tv 2013-08-24 15:07:21 +02:00
Ruud 73efd5549f Merge branch 'refs/heads/develop' into tv 2013-08-24 14:30:09 +02:00
Joel Kåberg 8139016636 Merge pull request #2061 from nrgaway/tv_loader
fix loader error messages for modules that are selected recursively but ...
2013-08-23 12:51:34 -07:00
Jason Mehring 59c0d0416e fix loader error messages for modules that are selected recursively but are not really modules 2013-08-23 15:32:47 -04:00
Joel Kåberg cd559ece04 Merge pull request #2058 from nrgaway/tv_refactored
Tv refactored
2013-08-23 07:47:29 -07:00
Joel Kåberg 120a4ad1ed Merge pull request #2057 from nrgaway/tv_development
Completed tvshow model
2013-08-23 07:47:14 -07:00
Jason Mehring 3363e164fd refactored Movie model to Media 2013-08-23 01:37:00 -04:00
Jason Mehring 6d6d5caeb6 Completed tvshow model 2013-08-23 00:53:34 -04:00
Joel Kåberg 21030e7cb4 Merge pull request #2052 from nrgaway/tv_database_2
Added Seasons
2013-08-22 12:40:20 -07:00
Jason Mehring 9b238ba712 Added Seasons. Show is the parent to Seasons and Episodes are the children if Season 2013-08-22 02:47:41 -04:00
Ruud b3d2d5349b Rename database for TV branch 2013-08-20 23:02:43 +02:00
Ruud Burger f9bad281de Merge pull request #2038 from nrgaway/tv_database
Tv database
2013-08-20 01:00:29 -07:00
Jason Mehring 72ce919989 Merge branch 'tv' of https://github.com/RuudBurger/CouchPotatoServer into tv_database 2013-08-20 02:18:03 -04:00
Jason Mehring ff782669f6 readded tvdb_api 2013-08-20 02:11:54 -04:00
Jason Mehring 36950993f1 removed tvdb_api since it was missing all files 2013-08-20 02:11:01 -04:00
Jason Mehring 7df93dc1b4 Moved library and refactored to its now location. Modified anything firing libray.add/update/_release date to now fire library.add.movie... 2013-08-20 01:54:47 -04:00
Ruud a45913eee7 Default to movie type 2013-08-18 13:20:53 +02:00
Ruud a25eac6c4e Make SceneAccess multiprovider 2013-08-18 11:47:07 +02:00
Ruud dd0fcf0bc1 Add multiprovider for provider grouping 2013-08-18 11:45:45 +02:00
Ruud 2267235eca Rename type to protocol 2013-08-18 11:44:00 +02:00
Jason Mehring 029cf9ecac New model implemented to work with both Movies and TV Shows as well any future types. Currenly episodes are mapped directly to shows; no seasons yet. Will get around to that soon. This version allows you to add any tv show and it will appear in wanted list, but no searches are written yet :) 2013-08-18 03:28:41 -04:00
Ruud f4217ecd3d Move registerPlugin to __new__ magic 2013-08-18 00:22:36 +02:00
Jason Mehring 31cd993506 EOD commit (WIP). So close to writing tv objects to database but too tired to finish. Currently storing a show as a movie using imdb metadata. Added another search button beside movie button 2013-08-17 04:15:51 -04:00
Jason Mehring fb579561de added a --noreloader option flag on startup to prevent CP from auto reloading when in development and debugging mode 2013-08-16 17:10:52 -04:00
Jason Mehring 37eb424827 Merge branch 'tv' of https://github.com/RuudBurger/CouchPotatoServer into tv_database 2013-08-16 15:41:37 -04:00
Ruud 4348451692 Merge branch 'refs/heads/develop' into tv
Conflicts:
	couchpotato/core/media/__init__.py
	couchpotato/core/media/_base/searcher/main.py
	couchpotato/core/media/movie/_base/main.py
	couchpotato/core/media/movie/searcher/__init__.py
	couchpotato/core/media/movie/searcher/main.py
2013-08-16 21:07:59 +02:00
Ruud e93e55a0f7 Searcher conf section 2013-08-16 10:22:43 +02:00
Jason Mehring bc11f90529 EOD commit (WIP). Commented out schema added yesterday in favour of a more global scheme. Added menu option in GUI to search for tv shows (placed on top of movie one for now). Partially implemented thetvdb provider. Search is working and returns a list of shows for GUI search along with posters. posters still need work. 2013-08-16 02:44:41 -04:00
Jason Mehring 8fcc246f25 Merge branch 'tv' of https://github.com/RuudBurger/CouchPotatoServer into tv_database 2013-08-15 20:48:57 -04:00
Ruud 4d7c38d6db Cleanup console log 2013-08-15 23:51:42 +02:00
Ruud c8d79cde21 Add base for TV media type 2013-08-15 23:47:07 +02:00
Ruud f4d792079b Give moviesearcher a unique name 2013-08-15 23:46:55 +02:00
Ruud 78ab419cd8 Move movie plugin to media folder 2013-08-15 23:38:14 +02:00
Ruud 3e93983f6e Move movie to new media type folder 2013-08-15 22:22:45 +02:00
Jason Mehring 6a4822cc26 merged upstream changes 2013-08-15 15:08:44 -04:00
Ruud 92b08bb5d5 Merge branch 'refs/heads/develop' into tv 2013-08-15 20:30:48 +02:00
Jason Mehring e270e09969 EOD commit (WIP). Added partial Show, Episode schema. 2013-08-15 01:15:48 -04:00
Ruud 40cd5218db Change branch to "tv" 2013-08-14 23:24:21 +02:00
356 changed files with 12266 additions and 4686 deletions
+4 -6
View File
@@ -1,5 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python
from __future__ import print_function
from logging import handlers from logging import handlers
from os.path import dirname from os.path import dirname
import logging import logging
@@ -133,15 +132,14 @@ if __name__ == '__main__':
pass pass
except SystemExit: except SystemExit:
raise raise
except socket.error as e: except socket.error as (nr, msg):
# log when socket receives SIGINT, but continue. # log when socket receives SIGINT, but continue.
# previous code would have skipped over other types of IO errors too. # previous code would have skipped over other types of IO errors too.
nr, msg = e
if nr != 4: if nr != 4:
try: try:
l.log.critical(traceback.format_exc()) l.log.critical(traceback.format_exc())
except: except:
print(traceback.format_exc()) print traceback.format_exc()
raise raise
except: except:
try: try:
@@ -150,7 +148,7 @@ if __name__ == '__main__':
if l: if l:
l.log.critical(traceback.format_exc()) l.log.critical(traceback.format_exc())
else: else:
print(traceback.format_exc()) print traceback.format_exc()
except: except:
print(traceback.format_exc()) print traceback.format_exc()
raise raise
-231
View File
@@ -1,231 +0,0 @@
from esky.util import appdir_from_executable #@UnresolvedImport
from threading import Thread
from version import VERSION
from wx.lib.softwareupdate import SoftwareUpdate
import os
import sys
import time
import webbrowser
import wx
# Include proper dirs
if hasattr(sys, 'frozen'):
import libs
base_path = os.path.dirname(os.path.dirname(os.path.abspath(libs.__file__)))
else:
base_path = os.path.dirname(os.path.abspath(__file__))
lib_dir = os.path.join(base_path, 'libs')
sys.path.insert(0, base_path)
sys.path.insert(0, lib_dir)
from couchpotato.environment import Env
class TaskBarIcon(wx.TaskBarIcon):
TBMENU_OPEN = wx.NewId()
TBMENU_SETTINGS = wx.NewId()
TBMENU_EXIT = wx.ID_EXIT
closed = False
menu = False
enabled = False
def __init__(self, frame):
wx.TaskBarIcon.__init__(self)
self.frame = frame
icon = wx.Icon('icon.png', wx.BITMAP_TYPE_PNG)
self.SetIcon(icon)
self.Bind(wx.EVT_TASKBAR_LEFT_UP, self.OnTaskBarClick)
self.Bind(wx.EVT_TASKBAR_RIGHT_UP, self.OnTaskBarClick)
self.Bind(wx.EVT_MENU, self.onOpen, id = self.TBMENU_OPEN)
self.Bind(wx.EVT_MENU, self.onSettings, id = self.TBMENU_SETTINGS)
self.Bind(wx.EVT_MENU, self.onTaskBarClose, id = self.TBMENU_EXIT)
def OnTaskBarClick(self, evt):
menu = self.CreatePopupMenu()
self.PopupMenu(menu)
menu.Destroy()
def enable(self):
self.enabled = True
if self.menu:
self.open_menu.Enable(True)
self.setting_menu.Enable(True)
self.open_menu.SetText('Open')
def CreatePopupMenu(self):
if not self.menu:
self.menu = wx.Menu()
self.open_menu = self.menu.Append(self.TBMENU_OPEN, 'Open')
self.setting_menu = self.menu.Append(self.TBMENU_SETTINGS, 'About')
self.exit_menu = self.menu.Append(self.TBMENU_EXIT, 'Quit')
if not self.enabled:
self.open_menu.Enable(False)
self.setting_menu.Enable(False)
self.open_menu.SetText('Loading...')
return self.menu
def onOpen(self, event):
url = self.frame.parent.getSetting('base_url')
webbrowser.open(url)
def onSettings(self, event):
url = self.frame.parent.getSetting('base_url') + 'settings/about/'
webbrowser.open(url)
def onTaskBarClose(self, evt):
if self.closed:
return
self.closed = True
self.RemoveIcon()
wx.CallAfter(self.frame.Close)
def makeIcon(self, img):
if "wxMSW" in wx.PlatformInfo:
img = img.Scale(16, 16)
elif "wxGTK" in wx.PlatformInfo:
img = img.Scale(22, 22)
icon = wx.IconFromBitmap(img.CopyFromBitmap())
return icon
class MainFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, None, style = wx.FRAME_NO_TASKBAR)
self.parent = parent
self.tbicon = TaskBarIcon(self)
class WorkerThread(Thread):
def __init__(self, desktop):
Thread.__init__(self)
self.daemon = True
self._desktop = desktop
self.start()
def run(self):
# Get options via arg
from couchpotato.runner import getOptions
args = ['--quiet']
self.options = getOptions(base_path, args)
# Load settings
settings = Env.get('settings')
settings.setFile(self.options.config_file)
# Create data dir if needed
self.data_dir = os.path.expanduser(Env.setting('data_dir'))
if self.data_dir == '':
from couchpotato.core.helpers.variable import getDataDir
self.data_dir = getDataDir()
if not os.path.isdir(self.data_dir):
os.makedirs(self.data_dir)
# Create logging dir
self.log_dir = os.path.join(self.data_dir, 'logs');
if not os.path.isdir(self.log_dir):
os.mkdir(self.log_dir)
try:
from couchpotato.runner import runCouchPotato
runCouchPotato(self.options, base_path, args, data_dir = self.data_dir, log_dir = self.log_dir, Env = Env, desktop = self._desktop)
except:
pass
self._desktop.frame.Close()
class CouchPotatoApp(wx.App, SoftwareUpdate):
settings = {}
events = {}
restart = False
closing = False
def OnInit(self):
# Updater
base_url = 'https://api.couchpota.to/updates/%s'
self.InitUpdates(base_url % VERSION + '/', 'https://couchpota.to/updates/%s' % 'changelog.html',
icon = wx.Icon('icon.png'))
self.frame = MainFrame(self)
self.frame.Bind(wx.EVT_CLOSE, self.onClose)
# CouchPotato thread
self.worker = WorkerThread(self)
return True
def onAppLoad(self):
self.frame.tbicon.enable()
def setSettings(self, settings = {}):
self.settings = settings
def getSetting(self, name):
return self.settings.get(name)
def addEvents(self, events = {}):
for name in events.iterkeys():
self.events[name] = events[name]
def onClose(self, event):
if not self.closing:
self.closing = True
self.frame.tbicon.onTaskBarClose(event)
onClose = self.events.get('onClose')
onClose(event)
def afterShutdown(self, restart = False):
self.frame.Destroy()
self.restart = restart
self.ExitMainLoop()
if __name__ == '__main__':
app = CouchPotatoApp(redirect = False)
app.MainLoop()
time.sleep(1)
if app.restart:
def appexe_from_executable(exepath):
appdir = appdir_from_executable(exepath)
exename = os.path.basename(exepath)
if sys.platform == "darwin":
if os.path.isdir(os.path.join(appdir, "Contents", "MacOS")):
return os.path.join(appdir, "Contents", "MacOS", exename)
return os.path.join(appdir, exename)
exe = appexe_from_executable(sys.executable)
os.chdir(os.path.dirname(exe))
os.execv(exe, [exe] + sys.argv[1:])
+17 -18
View File
@@ -9,12 +9,13 @@ import os
import time import time
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
views = {} views = {}
template_loader = template.Loader(os.path.join(os.path.dirname(__file__), 'templates')) template_loader = template.Loader(os.path.join(os.path.dirname(__file__), 'templates'))
class BaseHandler(RequestHandler): class BaseHandler(RequestHandler):
def get_current_user(self): def get_current_user(self):
@@ -26,7 +27,6 @@ class BaseHandler(RequestHandler):
else: # Login when no username or password are set else: # Login when no username or password are set
return True return True
# Main web handler # Main web handler
class WebHandler(BaseHandler): class WebHandler(BaseHandler):
@@ -43,13 +43,11 @@ class WebHandler(BaseHandler):
log.error("Failed doing web request '%s': %s", (route, traceback.format_exc())) log.error("Failed doing web request '%s': %s", (route, traceback.format_exc()))
self.write({'success': False, 'error': 'Failed returning results'}) self.write({'success': False, 'error': 'Failed returning results'})
def addView(route, func, static = False): def addView(route, func, static = False):
views[route] = func views[route] = func
def get_session(engine = None):
def get_session(): return Env.getSession(engine)
return Env.getSession()
# Web view # Web view
@@ -57,10 +55,12 @@ def index():
return template_loader.load('index.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env) return template_loader.load('index.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env)
addView('', index) addView('', index)
# API docs # API docs
def apiDocs(): def apiDocs():
routes = list(api.keys()) routes = []
for route in api.iterkeys():
routes.append(route)
if api_docs.get(''): if api_docs.get(''):
del api_docs[''] del api_docs['']
@@ -70,22 +70,21 @@ def apiDocs():
addView('docs', apiDocs) addView('docs', apiDocs)
# Make non basic auth option to get api key # Make non basic auth option to get api key
class KeyHandler(RequestHandler): class KeyHandler(RequestHandler):
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
api_key = None api = None
try: try:
username = Env.setting('username') username = Env.setting('username')
password = Env.setting('password') password = Env.setting('password')
if (self.get_argument('u') == md5(username) or not username) and (self.get_argument('p') == password or not password): if (self.get_argument('u') == md5(username) or not username) and (self.get_argument('p') == password or not password):
api_key = Env.setting('api_key') api = Env.setting('api_key')
self.write({ self.write({
'success': api_key is not None, 'success': api is not None,
'api_key': api_key 'api_key': api
}) })
except: except:
log.error('Failed doing key request: %s', (traceback.format_exc())) log.error('Failed doing key request: %s', (traceback.format_exc()))
@@ -103,21 +102,20 @@ class LoginHandler(BaseHandler):
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
api_key = None api = None
username = Env.setting('username') username = Env.setting('username')
password = Env.setting('password') password = Env.setting('password')
if (self.get_argument('username') == username or not username) and (md5(self.get_argument('password')) == password or not password): if (self.get_argument('username') == username or not username) and (md5(self.get_argument('password')) == password or not password):
api_key = Env.setting('api_key') api = Env.setting('api_key')
if api_key: if api:
remember_me = tryInt(self.get_argument('remember_me', default = 0)) remember_me = tryInt(self.get_argument('remember_me', default = 0))
self.set_secure_cookie('user', api_key, expires_days = 30 if remember_me > 0 else None) self.set_secure_cookie('user', api, expires_days = 30 if remember_me > 0 else None)
self.redirect(Env.get('web_base')) self.redirect(Env.get('web_base'))
class LogoutHandler(BaseHandler): class LogoutHandler(BaseHandler):
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
@@ -138,3 +136,4 @@ def page_not_found(rh):
rh.set_status(404) rh.set_status(404)
rh.write('Wrong API key used') rh.write('Wrong API key used')
+2 -8
View File
@@ -20,7 +20,6 @@ api_nonblock = {}
api_docs = {} api_docs = {}
api_docs_missing = [] api_docs_missing = []
def run_async(func): def run_async(func):
@wraps(func) @wraps(func)
def async_func(*args, **kwargs): def async_func(*args, **kwargs):
@@ -30,7 +29,6 @@ def run_async(func):
return async_func return async_func
# NonBlock API handler # NonBlock API handler
class NonBlockHandler(RequestHandler): class NonBlockHandler(RequestHandler):
@@ -63,7 +61,6 @@ class NonBlockHandler(RequestHandler):
self.stopper = None self.stopper = None
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
@@ -72,7 +69,6 @@ def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
else: else:
api_docs_missing.append(route) api_docs_missing.append(route)
# Blocking API handler # Blocking API handler
class ApiHandler(RequestHandler): class ApiHandler(RequestHandler):
@@ -102,12 +98,11 @@ class ApiHandler(RequestHandler):
@run_async @run_async
def run_handler(callback): def run_handler(callback):
try: try:
res = api[route](**kwargs) result = api[route](**kwargs)
callback(res) callback(result)
except: except:
log.error('Failed doing api request "%s": %s', (route, traceback.format_exc())) log.error('Failed doing api request "%s": %s', (route, traceback.format_exc()))
callback({'success': False, 'error': 'Failed returning results'}) callback({'success': False, 'error': 'Failed returning results'})
result = yield tornado.gen.Task(run_handler) result = yield tornado.gen.Task(run_handler)
# Check JSONP callback # Check JSONP callback
@@ -127,7 +122,6 @@ class ApiHandler(RequestHandler):
api_locks[route].release() api_locks[route].release()
def addApiView(route, func, static = False, docs = None, **kwargs): def addApiView(route, func, static = False, docs = None, **kwargs):
if static: func(route) if static: func(route)
-1
View File
@@ -1,7 +1,6 @@
from .main import Core from .main import Core
from uuid import uuid4 from uuid import uuid4
def start(): def start():
return Core() return Core()
@@ -1,6 +1,5 @@
from .main import ClientScript from .main import ClientScript
def start(): def start():
return ClientScript() return ClientScript()
+3 -4
View File
@@ -49,14 +49,13 @@ class ClientScript(Plugin):
'scripts/page/settings.js', 'scripts/page/settings.js',
'scripts/page/about.js', 'scripts/page/about.js',
'scripts/page/manage.js', 'scripts/page/manage.js',
'scripts/misc/downloaders.js',
], ],
} }
urls = {'style': {}, 'script': {}} urls = {'style': {}, 'script': {}, }
minified = {'style': {}, 'script': {}} minified = {'style': {}, 'script': {}, }
paths = {'style': {}, 'script': {}} paths = {'style': {}, 'script': {}, }
comment = { comment = {
'style': '/*** %s:%d ***/\n', 'style': '/*** %s:%d ***/\n',
'script': '// %s:%d\n' 'script': '// %s:%d\n'
@@ -1,6 +1,5 @@
from .main import Desktop from .main import Desktop
def start(): def start():
return Desktop() return Desktop()
@@ -1,6 +1,5 @@
from .main import Scheduler from .main import Scheduler
def start(): def start():
return Scheduler() return Scheduler()
-12
View File
@@ -17,7 +17,6 @@ class Scheduler(Plugin):
addEvent('schedule.cron', self.cron) addEvent('schedule.cron', self.cron)
addEvent('schedule.interval', self.interval) addEvent('schedule.interval', self.interval)
addEvent('schedule.remove', self.remove) addEvent('schedule.remove', self.remove)
addEvent('schedule.queue', self.queue)
self.sched = Sched(misfire_grace_time = 60) self.sched = Sched(misfire_grace_time = 60)
self.sched.start() self.sched.start()
@@ -65,14 +64,3 @@ class Scheduler(Plugin):
'seconds': seconds, 'seconds': seconds,
'job': self.sched.add_interval_job(handle, hours = hours, minutes = minutes, seconds = seconds) 'job': self.sched.add_interval_job(handle, hours = hours, minutes = minutes, seconds = seconds)
} }
def queue(self, handlers = None):
if not handlers: handlers = []
for h in handlers:
h()
if self.shuttingDown():
break
return True
@@ -2,7 +2,6 @@ from .main import Updater
from couchpotato.environment import Env from couchpotato.environment import Env
import os import os
def start(): def start():
return Updater() return Updater()
+11 -20
View File
@@ -15,7 +15,6 @@ import time
import traceback import traceback
import version import version
import zipfile import zipfile
from six.moves import filter
log = CPLog(__name__) log = CPLog(__name__)
@@ -33,7 +32,6 @@ class Updater(Plugin):
else: else:
self.updater = SourceUpdater() self.updater = SourceUpdater()
addEvent('app.load', self.logVersion, priority = 10000)
addEvent('app.load', self.setCrons) addEvent('app.load', self.setCrons)
addEvent('updater.info', self.info) addEvent('updater.info', self.info)
@@ -55,10 +53,6 @@ class Updater(Plugin):
addEvent('setting.save.updater.enabled.after', self.setCrons) addEvent('setting.save.updater.enabled.after', self.setCrons)
def logVersion(self):
info = self.info()
log.info('=== VERSION %s, using %s ===', (info.get('version', {}).get('repr', 'UNKNOWN'), self.updater.getName()))
def setCrons(self): def setCrons(self):
fireEvent('schedule.remove', 'updater.check', single = True) fireEvent('schedule.remove', 'updater.check', single = True)
@@ -152,9 +146,6 @@ class BaseUpdater(Plugin):
'branch': self.branch, 'branch': self.branch,
} }
def getVersion(self):
pass
def check(self): def check(self):
pass pass
@@ -183,6 +174,7 @@ class BaseUpdater(Plugin):
log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc())) log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc()))
class GitUpdater(BaseUpdater): class GitUpdater(BaseUpdater):
def __init__(self, git_command): def __init__(self, git_command):
@@ -212,12 +204,11 @@ class GitUpdater(BaseUpdater):
output = self.repo.getHead() # Yes, please output = self.repo.getHead() # Yes, please
log.debug('Git version output: %s', output.hash) log.debug('Git version output: %s', output.hash)
self.version = { self.version = {
'repr': 'git:(%s:%s % s) %s (%s)' % (self.repo_user, self.repo_name, self.branch, output.hash[:8], datetime.fromtimestamp(output.getDate())),
'hash': output.hash[:8], 'hash': output.hash[:8],
'date': output.getDate(), 'date': output.getDate(),
'type': 'git', 'type': 'git',
} }
except Exception as e: except Exception, e:
log.error('Failed using GIT updater, running from source, you need to have GIT installed. %s', e) log.error('Failed using GIT updater, running from source, you need to have GIT installed. %s', e)
return 'No GIT' return 'No GIT'
@@ -240,7 +231,7 @@ class GitUpdater(BaseUpdater):
local = self.repo.getHead() local = self.repo.getHead()
remote = branch.getHead() remote = branch.getHead()
log.debug('Versions, local:%s, remote:%s', (local.hash[:8], remote.hash[:8])) log.info('Versions, local:%s, remote:%s', (local.hash[:8], remote.hash[:8]))
if local.getDate() < remote.getDate(): if local.getDate() < remote.getDate():
self.update_version = { self.update_version = {
@@ -253,6 +244,7 @@ class GitUpdater(BaseUpdater):
return False return False
class SourceUpdater(BaseUpdater): class SourceUpdater(BaseUpdater):
def __init__(self): def __init__(self):
@@ -278,9 +270,9 @@ class SourceUpdater(BaseUpdater):
# Extract # Extract
if download_data.get('type') == 'zip': if download_data.get('type') == 'zip':
zip_file = zipfile.ZipFile(destination) zip = zipfile.ZipFile(destination)
zip_file.extractall(extracted_path) zip.extractall(extracted_path)
zip_file.close() zip.close()
else: else:
tar = tarfile.open(destination) tar = tarfile.open(destination)
tar.extractall(path = extracted_path) tar.extractall(path = extracted_path)
@@ -347,12 +339,13 @@ class SourceUpdater(BaseUpdater):
return True return True
def removeDir(self, path): def removeDir(self, path):
try: try:
if os.path.isdir(path): if os.path.isdir(path):
shutil.rmtree(path) shutil.rmtree(path)
except OSError as inst: except OSError, inst:
os.chmod(inst.filename, 0o777) os.chmod(inst.filename, 0777)
self.removeDir(path) self.removeDir(path)
def getVersion(self): def getVersion(self):
@@ -366,8 +359,7 @@ class SourceUpdater(BaseUpdater):
log.debug('Source version output: %s', output) log.debug('Source version output: %s', output)
self.version = output self.version = output
self.version['type'] = 'source' self.version['type'] = 'source'
self.version['repr'] = 'source:(%s:%s % s) %s (%s)' % (self.repo_user, self.repo_name, self.branch, output.get('hash', '')[:8], datetime.fromtimestamp(output.get('date', 0))) except Exception, e:
except Exception as e:
log.error('Failed using source updater. %s', e) log.error('Failed using source updater. %s', e)
return {} return {}
@@ -454,7 +446,6 @@ class DesktopUpdater(BaseUpdater):
def getVersion(self): def getVersion(self):
return { return {
'repr': 'desktop: %s' % self.desktop._esky.active_version,
'hash': self.desktop._esky.active_version, 'hash': self.desktop._esky.active_version,
'date': None, 'date': None,
'type': 'desktop', 'type': 'desktop',
@@ -24,7 +24,7 @@ var UpdaterBase = new Class({
self.doUpdate(); self.doUpdate();
else { else {
App.unBlockPage(); App.unBlockPage();
App.trigger('message', ['No updates available']); App.on('message', 'No updates available');
} }
} }
}) })
-11
View File
@@ -1,5 +1,4 @@
from base64 import b32decode, b16encode from base64 import b32decode, b16encode
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.variable import mergeDicts from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
@@ -43,7 +42,6 @@ class Downloader(Provider):
addEvent('download.remove_failed', self._removeFailed) addEvent('download.remove_failed', self._removeFailed)
addEvent('download.pause', self._pause) addEvent('download.pause', self._pause)
addEvent('download.process_complete', self._processComplete) addEvent('download.process_complete', self._processComplete)
addApiView('download.%s.test' % self.getName().lower(), self._test)
def getEnabledProtocol(self): def getEnabledProtocol(self):
for download_protocol in self.protocol: for download_protocol in self.protocol:
@@ -160,15 +158,6 @@ class Downloader(Provider):
(d_manual and manual or d_manual is False) and \ (d_manual and manual or d_manual is False) and \
(not data or self.isCorrectProtocol(data.get('protocol'))) (not data or self.isCorrectProtocol(data.get('protocol')))
def _test(self):
t = self.test()
if isinstance(t, tuple):
return {'success': t[0], 'msg': t[1]}
return {'success': t}
def test(self):
return False
def _pause(self, release_download, pause = True): def _pause(self, release_download, pause = True):
if self.isDisabled(manual = True, data = {}): if self.isDisabled(manual = True, data = {}):
return return
@@ -1,7 +1,6 @@
from .main import Blackhole from .main import Blackhole
from couchpotato.core.helpers.variable import getDownloadDir from couchpotato.core.helpers.variable import getDownloadDir
def start(): def start():
return Blackhole() return Blackhole()
@@ -1,6 +1,5 @@
from __future__ import with_statement from __future__ import with_statement
from couchpotato.core.downloaders.base import Downloader from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import sp
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.environment import Env from couchpotato.environment import Env
import os import os
@@ -68,20 +67,6 @@ class Blackhole(Downloader):
return False return False
def test(self):
directory = self.conf('directory')
if directory and os.path.isdir(directory):
test_file = sp(os.path.join(directory, 'couchpotato_test.txt'))
# Check if folder is writable
self.createFile(test_file, 'This is a test file')
if os.path.isfile(test_file):
os.remove(test_file)
return True
return False
def getEnabledProtocol(self): def getEnabledProtocol(self):
if self.conf('use_for') == 'both': if self.conf('use_for') == 'both':
return super(Blackhole, self).getEnabledProtocol() return super(Blackhole, self).getEnabledProtocol()
@@ -1,6 +1,5 @@
from .main import Deluge from .main import Deluge
def start(): def start():
return Deluge() return Deluge()
+12 -30
View File
@@ -2,7 +2,7 @@ from base64 import b64encode, b16encode, b32decode
from bencode import bencode as benc, bdecode from bencode import bencode as benc, bdecode
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
from couchpotato.core.helpers.encoding import isInt, sp from couchpotato.core.helpers.encoding import isInt, sp
from couchpotato.core.helpers.variable import tryFloat, cleanHost from couchpotato.core.helpers.variable import tryFloat
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from datetime import timedelta from datetime import timedelta
from hashlib import sha1 from hashlib import sha1
@@ -20,14 +20,14 @@ class Deluge(Downloader):
log = CPLog(__name__) log = CPLog(__name__)
drpc = None drpc = None
def connect(self, reconnect = False): def connect(self):
# Load host from config and split out port. # Load host from config and split out port.
host = cleanHost(self.conf('host'), protocol = False).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
if not self.drpc or reconnect: if not self.drpc:
self.drpc = DelugeRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password')) self.drpc = DelugeRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
return self.drpc return self.drpc
@@ -86,11 +86,6 @@ class Deluge(Downloader):
log.info('Torrent sent to Deluge successfully.') log.info('Torrent sent to Deluge successfully.')
return self.downloadReturnId(remote_torrent) return self.downloadReturnId(remote_torrent)
def test(self):
if self.connect(True) and self.drpc.test():
return True
return False
def getAllDownloadStatus(self, ids): def getAllDownloadStatus(self, ids):
log.debug('Checking Deluge download status.') log.debug('Checking Deluge download status.')
@@ -108,12 +103,7 @@ class Deluge(Downloader):
for torrent_id in queue: for torrent_id in queue:
torrent = queue[torrent_id] torrent = 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', (torrent['name'], torrent['hash'], torrent['save_path'], torrent['move_completed_path'], torrent['hash'], torrent['progress'], torrent['state'], torrent['eta'], torrent['ratio'], torrent['stop_ratio'], torrent['is_seed'], torrent['is_finished'], torrent['paused']))
if not 'hash' in torrent:
# When given a list of ids, deluge will return an empty item for a non-existant torrent.
continue
log.debug('name=%s / id=%s / save_path=%s / move_on_completed=%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', (torrent['name'], torrent['hash'], torrent['save_path'], torrent['move_on_completed'], torrent['move_completed_path'], torrent['hash'], torrent['progress'], torrent['state'], torrent['eta'], torrent['ratio'], torrent['stop_ratio'], torrent['is_seed'], torrent['is_finished'], torrent['paused']))
# Deluge has no easy way to work out if a torrent is stalled or failing. # Deluge has no easy way to work out if a torrent is stalled or failing.
#status = 'failed' #status = 'failed'
@@ -162,7 +152,6 @@ class Deluge(Downloader):
log.debug('Requesting Deluge to remove the torrent %s%s.', (release_download['name'], ' and cleanup the downloaded files' if delete_files else '')) log.debug('Requesting Deluge to remove the torrent %s%s.', (release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
return self.drpc.remove_torrent(release_download['id'], remove_local_data = delete_files) return self.drpc.remove_torrent(release_download['id'], remove_local_data = delete_files)
class DelugeRPC(object): class DelugeRPC(object):
host = 'localhost' host = 'localhost'
@@ -183,13 +172,6 @@ class DelugeRPC(object):
self.client = DelugeClient() self.client = DelugeClient()
self.client.connect(self.host, int(self.port), self.username, self.password) self.client.connect(self.host, int(self.port), self.username, self.password)
def test(self):
try:
self.connect()
except:
return False
return True
def add_torrent_magnet(self, torrent, options): def add_torrent_magnet(self, torrent, options):
torrent_id = False torrent_id = False
try: try:
@@ -200,7 +182,7 @@ class DelugeRPC(object):
if torrent_id and options['label']: if torrent_id and options['label']:
self.client.label.set_torrent(torrent_id, options['label']).get() self.client.label.set_torrent(torrent_id, options['label']).get()
except Exception as err: except Exception, err:
log.error('Failed to add torrent magnet %s: %s %s', (torrent, err, traceback.format_exc())) log.error('Failed to add torrent magnet %s: %s %s', (torrent, err, traceback.format_exc()))
finally: finally:
if self.client: if self.client:
@@ -218,7 +200,7 @@ class DelugeRPC(object):
if torrent_id and options['label']: if torrent_id and options['label']:
self.client.label.set_torrent(torrent_id, options['label']).get() self.client.label.set_torrent(torrent_id, options['label']).get()
except Exception as err: except Exception, err:
log.error('Failed to add torrent file %s: %s %s', (filename, err, traceback.format_exc())) log.error('Failed to add torrent file %s: %s %s', (filename, err, traceback.format_exc()))
finally: finally:
if self.client: if self.client:
@@ -230,8 +212,8 @@ class DelugeRPC(object):
ret = False ret = False
try: try:
self.connect() self.connect()
ret = self.client.core.get_torrents_status({'id': ids}, ('name', 'hash', 'save_path', 'move_completed_path', 'progress', 'state', 'eta', 'ratio', 'stop_ratio', 'is_seed', 'is_finished', 'paused', 'move_on_completed', 'files')).get() ret = self.client.core.get_torrents_status({'id': ids}, {}).get()
except Exception as err: except Exception, err:
log.error('Failed to get all torrents: %s %s', (err, traceback.format_exc())) log.error('Failed to get all torrents: %s %s', (err, traceback.format_exc()))
finally: finally:
if self.client: if self.client:
@@ -242,7 +224,7 @@ class DelugeRPC(object):
try: try:
self.connect() self.connect()
self.client.core.pause_torrent(torrent_ids).get() self.client.core.pause_torrent(torrent_ids).get()
except Exception as err: except Exception, err:
log.error('Failed to pause torrent: %s %s', (err, traceback.format_exc())) log.error('Failed to pause torrent: %s %s', (err, traceback.format_exc()))
finally: finally:
if self.client: if self.client:
@@ -252,7 +234,7 @@ class DelugeRPC(object):
try: try:
self.connect() self.connect()
self.client.core.resume_torrent(torrent_ids).get() self.client.core.resume_torrent(torrent_ids).get()
except Exception as err: except Exception, err:
log.error('Failed to resume torrent: %s %s', (err, traceback.format_exc())) log.error('Failed to resume torrent: %s %s', (err, traceback.format_exc()))
finally: finally:
if self.client: if self.client:
@@ -263,7 +245,7 @@ class DelugeRPC(object):
try: try:
self.connect() self.connect()
ret = self.client.core.remove_torrent(torrent_id, remove_local_data).get() ret = self.client.core.remove_torrent(torrent_id, remove_local_data).get()
except Exception as err: except Exception, err:
log.error('Failed to remove torrent: %s %s', (err, traceback.format_exc())) log.error('Failed to remove torrent: %s %s', (err, traceback.format_exc()))
finally: finally:
if self.client: if self.client:
@@ -1,6 +1,5 @@
from .main import NZBGet from .main import NZBGet
def start(): def start():
return NZBGet() return NZBGet()
+12 -33
View File
@@ -1,7 +1,7 @@
from base64 import standard_b64encode from base64 import standard_b64encode
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
from couchpotato.core.helpers.encoding import ss, sp from couchpotato.core.helpers.encoding import ss, sp
from couchpotato.core.helpers.variable import tryInt, md5, cleanHost from couchpotato.core.helpers.variable import tryInt, md5
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from datetime import timedelta from datetime import timedelta
import re import re
@@ -16,7 +16,8 @@ log = CPLog(__name__)
class NZBGet(Downloader): class NZBGet(Downloader):
protocol = ['nzb'] protocol = ['nzb']
rpc = 'xmlrpc'
url = '%(protocol)s://%(username)s:%(password)s@%(host)s/xmlrpc'
def download(self, data = None, media = None, filedata = None): def download(self, data = None, media = None, filedata = None):
if not media: media = {} if not media: media = {}
@@ -28,10 +29,10 @@ class NZBGet(Downloader):
log.info('Sending "%s" to NZBGet.', data.get('name')) log.info('Sending "%s" to NZBGet.', data.get('name'))
url = self.url % {'protocol': 'https' if self.conf('ssl') else 'http', 'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
nzb_name = ss('%s.nzb' % self.createNzbName(data, media)) nzb_name = ss('%s.nzb' % self.createNzbName(data, media))
rpc = self.getRPC() 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.debug('Successfully connected to NZBGet') log.debug('Successfully connected to NZBGet')
@@ -40,7 +41,7 @@ class NZBGet(Downloader):
except socket.error: except socket.error:
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.') log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
return False return False
except xmlrpclib.ProtocolError as e: except xmlrpclib.ProtocolError, e:
if e.errcode == 401: if e.errcode == 401:
log.error('Password is incorrect.') log.error('Password is incorrect.')
else: else:
@@ -66,32 +67,13 @@ class NZBGet(Downloader):
log.error('NZBGet could not add %s to the queue.', nzb_name) log.error('NZBGet could not add %s to the queue.', nzb_name)
return False return False
def test(self):
rpc = self.getRPC()
try:
if rpc.writelog('INFO', 'CouchPotato connected to test connection'):
log.debug('Successfully connected to NZBGet')
else:
log.info('Successfully connected to NZBGet, but unable to send a message')
except socket.error:
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
return False
except xmlrpclib.ProtocolError as e:
if e.errcode == 401:
log.error('Password is incorrect.')
else:
log.error('Protocol Error: %s', e)
return False
return True
def getAllDownloadStatus(self, ids): def getAllDownloadStatus(self, ids):
log.debug('Checking NZBGet download status.') log.debug('Checking NZBGet download status.')
rpc = self.getRPC() url = self.url % {'protocol': 'https' if self.conf('ssl') else 'http', 'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
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.debug('Successfully connected to NZBGet') log.debug('Successfully connected to NZBGet')
@@ -100,7 +82,7 @@ class NZBGet(Downloader):
except socket.error: except socket.error:
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.') log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
return [] return []
except xmlrpclib.ProtocolError as e: except xmlrpclib.ProtocolError, e:
if e.errcode == 401: if e.errcode == 401:
log.error('Password is incorrect.') log.error('Password is incorrect.')
else: else:
@@ -175,8 +157,9 @@ class NZBGet(Downloader):
log.info('%s failed downloading, deleting...', release_download['name']) log.info('%s failed downloading, deleting...', release_download['name'])
rpc = self.getRPC() url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
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.debug('Successfully connected to NZBGet') log.debug('Successfully connected to NZBGet')
@@ -185,7 +168,7 @@ class NZBGet(Downloader):
except socket.error: except socket.error:
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.') log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
return False return False
except xmlrpclib.ProtocolError as e: except xmlrpclib.ProtocolError, e:
if e.errcode == 401: if e.errcode == 401:
log.error('Password is incorrect.') log.error('Password is incorrect.')
else: else:
@@ -210,7 +193,3 @@ class NZBGet(Downloader):
return False return False
return True return True
def getRPC(self):
url = cleanHost(host = self.conf('host'), ssl = self.conf('ssl'), username = self.conf('username'), password = self.conf('password')) + self.rpc
return xmlrpclib.ServerProxy(url)
@@ -1,6 +1,5 @@
from .main import NZBVortex from .main import NZBVortex
def start(): def start():
return NZBVortex() return NZBVortex()
@@ -23,15 +22,7 @@ config = [{
}, },
{ {
'name': 'host', 'name': 'host',
'default': 'localhost:4321', 'default': 'https://localhost:4321',
'description': 'Hostname with port. Usually <strong>localhost:4321</strong>',
},
{
'name': 'ssl',
'default': 1,
'type': 'bool',
'advanced': True,
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
}, },
{ {
'name': 'api_key', 'name': 'api_key',
+6 -14
View File
@@ -36,20 +36,12 @@ class NZBVortex(Downloader):
time.sleep(10) time.sleep(10)
raw_statuses = self.call('nzb') raw_statuses = self.call('nzb')
nzb_id = [nzb['id'] for nzb in raw_statuses.get('nzbs', []) if os.path.basename(nzb['nzbFileName']) == nzb_filename][0] nzb_id = [nzb['id'] for nzb in raw_statuses.get('nzbs', []) if os.path.basename(item['nzbFileName']) == nzb_filename][0]
return self.downloadReturnId(nzb_id) return self.downloadReturnId(nzb_id)
except: except:
log.error('Something went wrong sending the NZB file: %s', traceback.format_exc()) log.error('Something went wrong sending the NZB file: %s', traceback.format_exc())
return False return False
def test(self):
try:
login_result = self.login()
except:
return False
return login_result
def getAllDownloadStatus(self, ids): def getAllDownloadStatus(self, ids):
raw_statuses = self.call('nzb') raw_statuses = self.call('nzb')
@@ -70,7 +62,7 @@ class NZBVortex(Downloader):
'name': nzb['uiTitle'], 'name': nzb['uiTitle'],
'status': status, 'status': status,
'original_status': nzb['state'], 'original_status': nzb['state'],
'timeleft': -1, 'timeleft':-1,
'folder': sp(nzb['destinationPath']), 'folder': sp(nzb['destinationPath']),
}) })
@@ -110,6 +102,7 @@ class NZBVortex(Downloader):
log.error('Login failed, please check you api-key') log.error('Login failed, please check you api-key')
return False return False
def call(self, call, parameters = None, repeat = False, auth = True, *args, **kwargs): def call(self, call, parameters = None, repeat = False, auth = True, *args, **kwargs):
# Login first # Login first
@@ -123,14 +116,14 @@ class NZBVortex(Downloader):
params = tryUrlencode(parameters) params = tryUrlencode(parameters)
url = cleanHost(self.conf('host'), ssl = self.conf('ssl')) + 'api/' + call url = cleanHost(self.conf('host')) + 'api/' + call
try: try:
data = self.urlopen('%s?%s' % (url, params), *args, **kwargs) data = self.urlopen('%s?%s' % (url, params), *args, **kwargs)
if data: if data:
return json.loads(data) return json.loads(data)
except URLError as e: except URLError, e:
if hasattr(e, 'code') and e.code == 403: if hasattr(e, 'code') and e.code == 403:
# Try login and do again # Try login and do again
if not repeat: if not repeat:
@@ -152,7 +145,7 @@ class NZBVortex(Downloader):
try: try:
data = self.urlopen(url, show_error = False) data = self.urlopen(url, show_error = False)
self.api_level = float(json.loads(data).get('apilevel')) self.api_level = float(json.loads(data).get('apilevel'))
except URLError as e: except URLError, e:
if hasattr(e, 'code') and e.code == 403: if hasattr(e, 'code') and e.code == 403:
log.error('This version of NZBVortex isn\'t supported. Please update to 2.8.6 or higher') log.error('This version of NZBVortex isn\'t supported. Please update to 2.8.6 or higher')
else: else:
@@ -182,7 +175,6 @@ class HTTPSConnection(httplib.HTTPSConnection):
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version = ssl.PROTOCOL_TLSv1) self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version = ssl.PROTOCOL_TLSv1)
class HTTPSHandler(urllib2.HTTPSHandler): class HTTPSHandler(urllib2.HTTPSHandler):
def https_open(self, req): def https_open(self, req):
return self.do_open(HTTPSConnection, req) return self.do_open(HTTPSConnection, req)
@@ -1,6 +1,5 @@
from .main import Pneumatic from .main import Pneumatic
def start(): def start():
return Pneumatic() return Pneumatic()
+6 -21
View File
@@ -1,6 +1,5 @@
from __future__ import with_statement from __future__ import with_statement
from couchpotato.core.downloaders.base import Downloader from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import sp
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
import os import os
import traceback import traceback
@@ -27,26 +26,26 @@ class Pneumatic(Downloader):
log.error('No nzb available!') log.error('No nzb available!')
return False return False
full_path = os.path.join(directory, self.createFileName(data, filedata, media)) fullPath = os.path.join(directory, self.createFileName(data, filedata, media))
try: try:
if not os.path.isfile(full_path): if not os.path.isfile(fullPath):
log.info('Downloading %s to %s.', (data.get('protocol'), full_path)) log.info('Downloading %s to %s.', (data.get('protocol'), fullPath))
with open(full_path, 'wb') as f: with open(fullPath, 'wb') as f:
f.write(filedata) f.write(filedata)
nzb_name = self.createNzbName(data, media) nzb_name = self.createNzbName(data, media)
strm_path = os.path.join(directory, nzb_name) strm_path = os.path.join(directory, nzb_name)
strm_file = open(strm_path + '.strm', 'wb') strm_file = open(strm_path + '.strm', 'wb')
strmContent = self.strm_syntax % (full_path, nzb_name) strmContent = self.strm_syntax % (fullPath, nzb_name)
strm_file.write(strmContent) strm_file.write(strmContent)
strm_file.close() strm_file.close()
return self.downloadReturnId('') return self.downloadReturnId('')
else: else:
log.info('File %s already exists.', full_path) log.info('File %s already exists.', fullPath)
return self.downloadReturnId('') return self.downloadReturnId('')
except: except:
@@ -57,17 +56,3 @@ class Pneumatic(Downloader):
log.info('Failed to download file %s: %s', (data.get('name'), traceback.format_exc())) log.info('Failed to download file %s: %s', (data.get('name'), traceback.format_exc()))
return False return False
return False return False
def test(self):
directory = self.conf('directory')
if directory and os.path.isdir(directory):
test_file = sp(os.path.join(directory, 'couchpotato_test.txt'))
# Check if folder is writable
self.createFile(test_file, 'This is a test file')
if os.path.isfile(test_file):
os.remove(test_file)
return True
return False
@@ -1,6 +1,5 @@
from .main import rTorrent from .main import rTorrent
def start(): def start():
return rTorrent() return rTorrent()
@@ -21,32 +20,11 @@ config = [{
'type': 'enabler', 'type': 'enabler',
'radio_group': 'torrent', 'radio_group': 'torrent',
}, },
# @RuudBurger: How do I migrate this?
# {
# 'name': 'url',
# 'default': 'http://localhost:80/RPC2',
# 'description': 'XML-RPC Endpoint URI. Usually <strong>scgi://localhost:5000</strong> '
# 'or <strong>http://localhost:80/RPC2</strong>'
# },
{ {
'name': 'host', 'name': 'url',
'default': 'localhost:80', 'default': 'http://localhost:80/RPC2',
'description': 'RPC Communication URI. Usually <strong>scgi://localhost:5000</strong>, ' 'description': 'XML-RPC Endpoint URI. Usually <strong>scgi://localhost:5000</strong> '
'<strong>httprpc://localhost/rutorrent</strong> or <strong>localhost:80</strong>' 'or <strong>http://localhost:80/RPC2</strong>'
},
{
'name': 'ssl',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
},
{
'name': 'rpc_url',
'type': 'string',
'default': 'RPC2',
'advanced': True,
'description': 'Change if your RPC mount is at a different path.',
}, },
{ {
'name': 'username', 'name': 'username',
+26 -89
View File
@@ -1,15 +1,12 @@
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.encoding import sp
from couchpotato.core.helpers.variable import cleanHost, splitString
from couchpotato.core.logger import CPLog
from base64 import b16encode, b32decode from base64 import b16encode, b32decode
from bencode import bencode, bdecode from bencode import bencode, bdecode
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
from couchpotato.core.helpers.encoding import sp
from couchpotato.core.logger import CPLog
from datetime import timedelta from datetime import timedelta
from hashlib import sha1 from hashlib import sha1
from rtorrent import RTorrent from rtorrent import RTorrent
from rtorrent.err import MethodError from rtorrent.err import MethodError
from urlparse import urlparse
import os import os
log = CPLog(__name__) log = CPLog(__name__)
@@ -19,75 +16,29 @@ class rTorrent(Downloader):
protocol = ['torrent', 'torrent_magnet'] protocol = ['torrent', 'torrent_magnet']
rt = None rt = None
error_msg = ''
# Migration url to host options def connect(self):
def __init__(self):
super(rTorrent, self).__init__()
addEvent('app.load', self.migrate)
addEvent('setting.save.rtorrent.*.after', self.settingsChanged)
def migrate(self):
url = self.conf('url')
if url:
host_split = splitString(url.split('://')[-1], split_on = '/')
self.conf('ssl', value = url.startswith('https'))
self.conf('host', value = host_split[0].strip())
self.conf('rpc_url', value = '/'.join(host_split[1:]))
self.deleteConf('url')
def settingsChanged(self):
# Reset active connection if settings have changed
if self.rt:
log.debug('Settings have changed, closing active connection')
self.rt = None
return True
def connect(self, reconnect = False):
# Already connected? # Already connected?
if not reconnect and self.rt is not None: if self.rt is not None:
return self.rt return self.rt
url = cleanHost(self.conf('host'), protocol = True, ssl = self.conf('ssl')) # Ensure url is set
parsed = urlparse(url) if not self.conf('url'):
log.error('Config properties are not filled in correctly, url is missing.')
# rpc_url is only used on http/https scgi pass-through return False
if parsed.scheme in ['http', 'https']:
url += self.conf('rpc_url')
if self.conf('username') and self.conf('password'): if self.conf('username') and self.conf('password'):
self.rt = RTorrent( self.rt = RTorrent(
url, self.conf('url'),
self.conf('username'), self.conf('username'),
self.conf('password') self.conf('password')
) )
else: else:
self.rt = RTorrent(url) self.rt = RTorrent(self.conf('url'))
self.error_msg = ''
try:
self.rt._verify_conn()
except AssertionError as e:
self.error_msg = e.message
self.rt = None
return self.rt return self.rt
def test(self): def _update_provider_group(self, name, data):
if self.connect(True):
return True
if self.error_msg:
return False, 'Connection failed: ' + self.error_msg
return False
def updateProviderGroup(self, name, data):
if data.get('seed_time'): if data.get('seed_time'):
log.info('seeding time ignored, not supported') log.info('seeding time ignored, not supported')
@@ -119,7 +70,7 @@ class rTorrent(Downloader):
# Reset group action and disable it # Reset group action and disable it
group.set_command() group.set_command()
group.disable() group.disable()
except MethodError as err: except MethodError, err:
log.error('Unable to set group options: %s', err.msg) log.error('Unable to set group options: %s', err.msg)
return False return False
@@ -136,13 +87,14 @@ class rTorrent(Downloader):
return False return False
group_name = 'cp_' + data.get('provider').lower() group_name = 'cp_' + data.get('provider').lower()
if not self.updateProviderGroup(group_name, data): if not self._update_provider_group(group_name, data):
return False return False
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('protocol') == '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
@@ -166,7 +118,7 @@ class rTorrent(Downloader):
# Send request to rTorrent # Send request to rTorrent
try: try:
# Send torrent to rTorrent # Send torrent to rTorrent
torrent = self.rt.load_torrent(filedata, verify_retries=10) torrent = self.rt.load_torrent(filedata)
if not torrent: if not torrent:
log.error('Unable to find the torrent, did it fail to load?') log.error('Unable to find the torrent, did it fail to load?')
@@ -187,25 +139,10 @@ class rTorrent(Downloader):
torrent.start() torrent.start()
return self.downloadReturnId(torrent_hash) return self.downloadReturnId(torrent_hash)
except Exception as err: except Exception, err:
log.error('Failed to send torrent to rTorrent: %s', err) log.error('Failed to send torrent to rTorrent: %s', err)
return False return False
def getTorrentStatus(self, torrent):
if torrent.hashing or torrent.hash_checking or torrent.message:
return 'busy'
if not torrent.complete:
return 'busy'
if not torrent.open:
return 'completed'
if torrent.state and torrent.active:
return 'seeding'
return 'busy'
def getAllDownloadStatus(self, ids): def getAllDownloadStatus(self, ids):
log.debug('Checking rTorrent download status.') log.debug('Checking rTorrent download status.')
@@ -219,21 +156,21 @@ class rTorrent(Downloader):
for torrent in torrents: for torrent in torrents:
if torrent.info_hash in ids: if torrent.info_hash in ids:
torrent_directory = os.path.normpath(torrent.directory)
torrent_files = [] torrent_files = []
for file_item in torrent.get_files():
torrent_files.append(sp(os.path.join(torrent.directory, file_item.path)))
for file in torrent.get_files(): status = 'busy'
if not os.path.normpath(file.path).startswith(torrent_directory): if torrent.complete:
file_path = os.path.join(torrent_directory, file.path.lstrip('/')) if torrent.active:
status = 'seeding'
else: else:
file_path = file.path status = 'completed'
torrent_files.append(sp(file_path))
release_downloads.append({ release_downloads.append({
'id': torrent.info_hash, 'id': torrent.info_hash,
'name': torrent.name, 'name': torrent.name,
'status': self.getTorrentStatus(torrent), 'status': status,
'seed_ratio': torrent.ratio, 'seed_ratio': torrent.ratio,
'original_status': torrent.state, 'original_status': torrent.state,
'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1, 'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1,
@@ -243,7 +180,7 @@ class rTorrent(Downloader):
return release_downloads return release_downloads
except Exception as err: except Exception, err:
log.error('Failed to get status from rTorrent: %s', err) log.error('Failed to get status from rTorrent: %s', err)
return [] return []
@@ -1,6 +1,5 @@
from .main import Sabnzbd from .main import Sabnzbd
def start(): def start():
return Sabnzbd() return Sabnzbd()
@@ -25,13 +24,6 @@ config = [{
'name': 'host', 'name': 'host',
'default': 'localhost:8080', 'default': 'localhost:8080',
}, },
{
'name': 'ssl',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
},
{ {
'name': 'api_key', 'name': 'api_key',
'label': 'Api Key', 'label': 'Api Key',
+1 -21
View File
@@ -64,26 +64,6 @@ class Sabnzbd(Downloader):
log.error('Error getting data from SABNZBd: %s', sab_data) log.error('Error getting data from SABNZBd: %s', sab_data)
return False return False
def test(self):
try:
sab_data = self.call({
'mode': 'version',
})
v = sab_data.split('.')
if int(v[0]) == 0 and int(v[1]) < 7:
return False, 'Your Sabnzbd client is too old, please update to newest version.'
# the version check will work even with wrong api key, so we need the next check as well
sab_data = self.call({
'mode': 'qstatus',
})
if not sab_data:
return False
except:
return False
return True
def getAllDownloadStatus(self, ids): def getAllDownloadStatus(self, ids):
log.debug('Checking SABnzbd download status.') log.debug('Checking SABnzbd download status.')
@@ -185,7 +165,7 @@ class Sabnzbd(Downloader):
def call(self, request_params, use_json = True, **kwargs): def call(self, request_params, use_json = True, **kwargs):
url = cleanHost(self.conf('host'), ssl = self.conf('ssl')) + 'api?' + tryUrlencode(mergeDicts(request_params, { url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(request_params, {
'apikey': self.conf('api_key'), 'apikey': self.conf('api_key'),
'output': 'json' 'output': 'json'
})) }))
@@ -1,6 +1,5 @@
from .main import Synology from .main import Synology
def start(): def start():
return Synology() return Synology()
+4 -19
View File
@@ -1,6 +1,5 @@
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.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
import json import json
import requests import requests
@@ -22,7 +21,7 @@ class Synology(Downloader):
log.error('Sending "%s" (%s) to Synology.', (data['name'], data['protocol'])) 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 = cleanHost(self.conf('host'), protocol = False).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
@@ -45,16 +44,6 @@ class Synology(Downloader):
finally: finally:
return self.downloadReturnId('') if response else False return self.downloadReturnId('') if response else False
def test(self):
host = cleanHost(self.conf('host'), protocol = False).split(':')
try:
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
test_result = srpc.test()
except:
return False
return test_result
def getEnabledProtocol(self): def getEnabledProtocol(self):
if self.conf('use_for') == 'both': if self.conf('use_for') == 'both':
return super(Synology, self).getEnabledProtocol() return super(Synology, self).getEnabledProtocol()
@@ -75,7 +64,6 @@ class Synology(Downloader):
return super(Synology, self).isEnabled(manual, data) and\ return super(Synology, self).isEnabled(manual, data) and\
((self.conf('use_for') in for_protocol)) ((self.conf('use_for') in for_protocol))
class SynologyRPC(object): class SynologyRPC(object):
"""SynologyRPC lite library""" """SynologyRPC lite library"""
@@ -118,11 +106,11 @@ class SynologyRPC(object):
if response['success']: if response['success']:
log.info('Synology action successfull') log.info('Synology action successfull')
return response return response
except requests.ConnectionError as err: except requests.ConnectionError, err:
log.error('Synology connection error, check your config %s', err) log.error('Synology connection error, check your config %s', err)
except requests.HTTPError as err: except requests.HTTPError, err:
log.error('SynologyRPC HTTPError: %s', err) log.error('SynologyRPC HTTPError: %s', err)
except Exception as err: except Exception, err:
log.error('Exception: %s', err) log.error('Exception: %s', err)
finally: finally:
return response return response
@@ -157,6 +145,3 @@ class SynologyRPC(object):
self._logout() self._logout()
return result return result
def test(self):
return bool(self._login())
@@ -1,6 +1,5 @@
from .main import Transmission from .main import Transmission
def start(): def start():
return Transmission() return Transmission()
@@ -1,7 +1,7 @@
from base64 import b64encode from base64 import b64encode
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
from couchpotato.core.helpers.encoding import isInt, sp from couchpotato.core.helpers.encoding import isInt, sp
from couchpotato.core.helpers.variable import tryInt, tryFloat, cleanHost 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 datetime import timedelta
import httplib import httplib
@@ -19,15 +19,15 @@ class Transmission(Downloader):
log = CPLog(__name__) log = CPLog(__name__)
trpc = None trpc = None
def connect(self, reconnect = False): def connect(self):
# Load host from config and split out port. # Load host from config and split out port.
host = cleanHost(self.conf('host'), protocol = False).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
if not self.trpc or reconnect: if not self.trpc:
self.trpc = TransmissionRPC(host[0], port = host[1], rpc_url = self.conf('rpc_url').strip('/ '), username = self.conf('username'), password = self.conf('password')) self.trpc = TransmissionRPC(host[0], port = host[1], rpc_url = self.conf('rpc_url'), username = self.conf('username'), password = self.conf('password'))
return self.trpc return self.trpc
@@ -83,11 +83,6 @@ class Transmission(Downloader):
log.info('Torrent sent to Transmission successfully.') log.info('Torrent sent to Transmission successfully.')
return self.downloadReturnId(remote_torrent['torrent-added']['hashString']) return self.downloadReturnId(remote_torrent['torrent-added']['hashString'])
def test(self):
if self.connect(True) and self.trpc.get_session():
return True
return False
def getAllDownloadStatus(self, ids): def getAllDownloadStatus(self, ids):
log.debug('Checking Transmission download status.') log.debug('Checking Transmission download status.')
@@ -101,7 +96,6 @@ class Transmission(Downloader):
'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isStalled', 'isFinished', 'downloadDir', 'uploadRatio', 'secondsSeeding', 'seedIdleLimit', 'files'] 'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isStalled', 'isFinished', 'downloadDir', 'uploadRatio', 'secondsSeeding', 'seedIdleLimit', 'files']
} }
session = self.trpc.get_session()
queue = self.trpc.get_alltorrents(return_params) queue = self.trpc.get_alltorrents(return_params)
if not (queue and queue.get('torrents')): if not (queue and queue.get('torrents')):
log.debug('Nothing in queue or error') log.debug('Nothing in queue or error')
@@ -109,8 +103,12 @@ class Transmission(Downloader):
for torrent in queue['torrents']: for torrent in queue['torrents']:
if torrent['hashString'] in ids: if torrent['hashString'] in ids:
log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / isStalled=%s / eta=%s / uploadRatio=%s / isFinished=%s / incomplete-dir-enabled=%s / incomplete-dir=%s', log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / isStalled=%s / eta=%s / uploadRatio=%s / isFinished=%s',
(torrent['name'], torrent['id'], torrent['downloadDir'], torrent['hashString'], torrent['percentDone'], torrent['status'], torrent.get('isStalled', 'N/A'), torrent['eta'], torrent['uploadRatio'], torrent['isFinished'], session['incomplete-dir-enabled'], session['incomplete-dir'])) (torrent['name'], torrent['id'], torrent['downloadDir'], torrent['hashString'], torrent['percentDone'], torrent['status'], torrent.get('isStalled', 'N/A'), torrent['eta'], torrent['uploadRatio'], torrent['isFinished']))
torrent_files = []
for file_item in torrent['files']:
torrent_files.append(sp(os.path.join(torrent['downloadDir'], file_item['name'])))
status = 'busy' status = 'busy'
if torrent.get('isStalled') and not torrent['percentDone'] == 1 and self.conf('stalled_as_failed'): if torrent.get('isStalled') and not torrent['percentDone'] == 1 and self.conf('stalled_as_failed'):
@@ -120,15 +118,6 @@ class Transmission(Downloader):
elif torrent['status'] in [5, 6]: elif torrent['status'] in [5, 6]:
status = 'seeding' status = 'seeding'
if session['incomplete-dir-enabled'] and status == 'busy':
torrent_folder = session['incomplete-dir']
else:
torrent_folder = torrent['downloadDir']
torrent_files = []
for file_item in torrent['files']:
torrent_files.append(sp(os.path.join(torrent_folder, file_item['name'])))
release_downloads.append({ release_downloads.append({
'id': torrent['hashString'], 'id': torrent['hashString'],
'name': torrent['name'], 'name': torrent['name'],
@@ -136,7 +125,7 @@ class Transmission(Downloader):
'original_status': torrent['status'], 'original_status': torrent['status'],
'seed_ratio': torrent['uploadRatio'], 'seed_ratio': torrent['uploadRatio'],
'timeleft': str(timedelta(seconds = torrent['eta'])), 'timeleft': str(timedelta(seconds = torrent['eta'])),
'folder': sp(torrent_folder if len(torrent_files) == 1 else os.path.join(torrent_folder, torrent['name'])), 'folder': sp(torrent['downloadDir'] if len(torrent_files) == 1 else os.path.join(torrent['downloadDir'], torrent['name'])),
'files': '|'.join(torrent_files) 'files': '|'.join(torrent_files)
}) })
@@ -192,10 +181,10 @@ class TransmissionRPC(object):
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'])
return False return False
except httplib.InvalidURL as err: except httplib.InvalidURL, err:
log.error('Invalid Transmission host, check your config %s', err) log.error('Invalid Transmission host, check your config %s', err)
return False return False
except urllib2.HTTPError as err: except urllib2.HTTPError, err:
if err.code == 401: if err.code == 401:
log.error('Invalid Transmission Username or Password, check your config') log.error('Invalid Transmission Username or Password, check your config')
return False return False
@@ -213,7 +202,7 @@ class TransmissionRPC(object):
log.error('Unable to get Transmission Session-Id %s', err) log.error('Unable to get Transmission Session-Id %s', err)
else: else:
log.error('TransmissionRPC HTTPError: %s', err) log.error('TransmissionRPC HTTPError: %s', err)
except urllib2.URLError as err: except urllib2.URLError, err:
log.error('Unable to connect to Transmission %s', err) log.error('Unable to connect to Transmission %s', err)
def get_session(self): def get_session(self):
@@ -1,6 +1,5 @@
from .main import uTorrent from .main import uTorrent
def start(): def start():
return uTorrent() return uTorrent()
@@ -24,7 +23,7 @@ config = [{
{ {
'name': 'host', 'name': 'host',
'default': 'localhost:8000', 'default': 'localhost:8000',
'description': 'Port can be found in settings when enabling WebUI.', 'description': 'Hostname with port. Usually <strong>localhost:8000</strong>',
}, },
{ {
'name': 'username', 'name': 'username',
+7 -25
View File
@@ -2,7 +2,7 @@ from base64 import b16encode, b32decode
from bencode import bencode as benc, bdecode from bencode import bencode as benc, bdecode
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
from couchpotato.core.helpers.encoding import isInt, ss, sp from couchpotato.core.helpers.encoding import isInt, ss, sp
from couchpotato.core.helpers.variable import tryInt, tryFloat, cleanHost 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 datetime import timedelta
from hashlib import sha1 from hashlib import sha1
@@ -37,7 +37,7 @@ class uTorrent(Downloader):
def connect(self): def connect(self):
# Load host from config and split out port. # Load host from config and split out port.
host = cleanHost(self.conf('host'), protocol = False).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
@@ -115,17 +115,6 @@ class uTorrent(Downloader):
return self.downloadReturnId(torrent_hash) return self.downloadReturnId(torrent_hash)
def test(self):
if self.connect():
build_version = self.utorrent_api.get_build()
if not build_version:
return False
if build_version < 25406: # This build corresponds to version 3.0.0 stable
return False, 'Your uTorrent client is too old, please update to newest version.'
return True
return False
def getAllDownloadStatus(self, ids): def getAllDownloadStatus(self, ids):
log.debug('Checking uTorrent download status.') log.debug('Checking uTorrent download status.')
@@ -242,14 +231,14 @@ class uTorrentAPI(object):
return response return response
else: else:
log.debug('Unknown failure sending command to uTorrent. Return text is: %s', response) log.debug('Unknown failure sending command to uTorrent. Return text is: %s', response)
except httplib.InvalidURL as err: except httplib.InvalidURL, err:
log.error('Invalid uTorrent host, check your config %s', err) log.error('Invalid uTorrent host, check your config %s', err)
except urllib2.HTTPError as err: except urllib2.HTTPError, err:
if err.code == 401: if err.code == 401:
log.error('Invalid uTorrent Username or Password, check your config') log.error('Invalid uTorrent Username or Password, check your config')
else: else:
log.error('uTorrent HTTPError: %s', err) log.error('uTorrent HTTPError: %s', err)
except urllib2.URLError as err: except urllib2.URLError, err:
log.error('Unable to connect to uTorrent %s', err) log.error('Unable to connect to uTorrent %s', err)
return False return False
@@ -272,7 +261,7 @@ class uTorrentAPI(object):
def set_torrent(self, hash, params): def set_torrent(self, hash, params):
action = 'action=setprops&hash=%s' % hash action = 'action=setprops&hash=%s' % hash
for k, v in params.items(): for k, v in params.iteritems():
action += '&s=%s&v=%s' % (k, v) action += '&s=%s&v=%s' % (k, v)
return self._request(action) return self._request(action)
@@ -315,7 +304,7 @@ class uTorrentAPI(object):
#log.debug('uTorrent settings: %s', settings_dict) #log.debug('uTorrent settings: %s', settings_dict)
except Exception as err: except Exception, err:
log.error('Failed to get settings from uTorrent: %s', err) log.error('Failed to get settings from uTorrent: %s', err)
return settings_dict return settings_dict
@@ -333,10 +322,3 @@ class uTorrentAPI(object):
def get_files(self, hash): def get_files(self, hash):
action = 'action=getfiles&hash=%s' % hash action = 'action=getfiles&hash=%s' % hash
return self._request(action) return self._request(action)
def get_build(self):
data = self._request('')
if not data:
return False
response = json.loads(data)
return int(response.get('build'))
+9 -14
View File
@@ -1,5 +1,5 @@
from axl.axel import Event from axl.axel import Event
from couchpotato.core.helpers.variable import mergeDicts, natsortKey from couchpotato.core.helpers.variable import mergeDicts, natcmp
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
import threading import threading
import traceback import traceback
@@ -7,7 +7,6 @@ import traceback
log = CPLog(__name__) log = CPLog(__name__)
events = {} events = {}
def runHandler(name, handler, *args, **kwargs): def runHandler(name, handler, *args, **kwargs):
try: try:
return handler(*args, **kwargs) return handler(*args, **kwargs)
@@ -15,7 +14,6 @@ def runHandler(name, handler, *args, **kwargs):
from couchpotato.environment import Env from couchpotato.environment import Env
log.error('Error in event "%s", that wasn\'t caught: %s%s', (name, traceback.format_exc(), Env.all() if not Env.get('dev') else '')) log.error('Error in event "%s", that wasn\'t caught: %s%s', (name, traceback.format_exc(), Env.all() if not Env.get('dev') else ''))
def addEvent(name, handler, priority = 100): def addEvent(name, handler, priority = 100):
if not events.get(name): if not events.get(name):
@@ -29,7 +27,7 @@ def addEvent(name, handler, priority = 100):
has_parent = hasattr(handler, 'im_self') has_parent = hasattr(handler, 'im_self')
parent = None parent = None
if has_parent: if has_parent:
parent = handler.__self__ parent = handler.im_self
bc = hasattr(parent, 'beforeCall') bc = hasattr(parent, 'beforeCall')
if bc: parent.beforeCall(handler) if bc: parent.beforeCall(handler)
@@ -50,9 +48,12 @@ def addEvent(name, handler, priority = 100):
'priority': priority, 'priority': priority,
}) })
def removeEvent(name, handler):
e = events[name]
e -= handler
def fireEvent(name, *args, **kwargs): def fireEvent(name, *args, **kwargs):
if name not in events: return if not events.has_key(name): return
#log.debug('Firing event %s', name) #log.debug('Firing event %s', name)
try: try:
@@ -100,14 +101,11 @@ def fireEvent(name, *args, **kwargs):
# Fire # Fire
result = e(*args, **kwargs) result = e(*args, **kwargs)
result_keys = result.keys()
result_keys.sort(key = natsortKey)
if options['single'] and not options['merge']: if options['single'] and not options['merge']:
results = None results = None
# Loop over results, stop when first not None result is found. # Loop over results, stop when first not None result is found.
for r_key in result_keys: for r_key in sorted(result.iterkeys(), cmp = natcmp):
r = result[r_key] r = result[r_key]
if r[0] is True and r[1] is not None: if r[0] is True and r[1] is not None:
results = r[1] results = r[1]
@@ -119,7 +117,7 @@ def fireEvent(name, *args, **kwargs):
else: else:
results = [] results = []
for r_key in result_keys: for r_key in sorted(result.iterkeys(), cmp = natcmp):
r = result[r_key] r = result[r_key]
if r[0] == True and r[1]: if r[0] == True and r[1]:
results.append(r[1]) results.append(r[1])
@@ -162,21 +160,18 @@ def fireEvent(name, *args, **kwargs):
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):
try: try:
t = threading.Thread(target = fireEvent, args = args, kwargs = kwargs) t = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
t.setDaemon(True) t.setDaemon(True)
t.start() t.start()
return True return True
except Exception as e: except Exception, e:
log.error('%s: %s', (args[0], e)) log.error('%s: %s', (args[0], e))
def errorHandler(error): def errorHandler(error):
etype, value, tb = error etype, value, tb = error
log.error(''.join(traceback.format_exception(etype, value, tb))) log.error(''.join(traceback.format_exception(etype, value, tb)))
def getEvent(name): def getEvent(name):
return events[name] return events[name]
+8 -17
View File
@@ -5,32 +5,29 @@ import os
import re import re
import traceback import traceback
import unicodedata import unicodedata
import six
log = CPLog(__name__) log = CPLog(__name__)
def toSafeString(original): def toSafeString(original):
valid_chars = "-_.() %s%s" % (ascii_letters, digits) valid_chars = "-_.() %s%s" % (ascii_letters, digits)
cleaned_filename = unicodedata.normalize('NFKD', toUnicode(original)).encode('ASCII', 'ignore') cleanedFilename = unicodedata.normalize('NFKD', toUnicode(original)).encode('ASCII', 'ignore')
valid_string = ''.join(c for c in cleaned_filename if c in valid_chars) valid_string = ''.join(c for c in cleanedFilename if c in valid_chars)
return ' '.join(valid_string.split()) return ' '.join(valid_string.split())
def simplifyString(original): def simplifyString(original):
string = stripAccents(original.lower()) string = stripAccents(original.lower())
string = toSafeString(' '.join(re.split('\W+', string))) string = toSafeString(' '.join(re.split('\W+', string)))
split = re.split('\W+|_', string.lower()) split = re.split('\W+|_', string.lower())
return toUnicode(' '.join(split)) return toUnicode(' '.join(split))
def toUnicode(original, *args): def toUnicode(original, *args):
try: try:
if isinstance(original, unicode): if isinstance(original, unicode):
return original return original
else: else:
try: try:
return six.text_type(original, *args) return unicode(original, *args)
except: except:
try: try:
return ek(original, *args) return ek(original, *args)
@@ -41,18 +38,16 @@ def toUnicode(original, *args):
ascii_text = str(original).encode('string_escape') ascii_text = str(original).encode('string_escape')
return toUnicode(ascii_text) return toUnicode(ascii_text)
def ss(original, *args): def ss(original, *args):
u_original = toUnicode(original, *args) u_original = toUnicode(original, *args)
try: try:
from couchpotato.environment import Env from couchpotato.environment import Env
return u_original.encode(Env.get('encoding')) return u_original.encode(Env.get('encoding'))
except Exception as e: except Exception, e:
log.debug('Failed ss encoding char, force UTF8: %s', e) log.debug('Failed ss encoding char, force UTF8: %s', e)
return u_original.encode('UTF-8') return u_original.encode('UTF-8')
def sp(path, *args): def sp(path, *args):
# Standardise encoding, normalise case, path and strip trailing '/' or '\' # Standardise encoding, normalise case, path and strip trailing '/' or '\'
@@ -63,7 +58,7 @@ def sp(path, *args):
if os.path.sep == '/' and '\\' in path: if os.path.sep == '/' and '\\' in path:
path = '/' + path.replace(':', '').replace('\\', '/') path = '/' + path.replace(':', '').replace('\\', '/')
path = os.path.normpath(ss(path, *args)) path = os.path.normcase(os.path.normpath(ss(path, *args)))
# Remove any trailing path separators # Remove any trailing path separators
if path != os.path.sep: if path != os.path.sep:
@@ -78,7 +73,6 @@ def sp(path, *args):
return path return path
def ek(original, *args): def ek(original, *args):
if isinstance(original, (str, unicode)): if isinstance(original, (str, unicode)):
try: try:
@@ -89,7 +83,6 @@ def ek(original, *args):
return original return original
def isInt(value): def isInt(value):
try: try:
int(value) int(value)
@@ -97,16 +90,14 @@ def isInt(value):
except ValueError: except ValueError:
return False return False
def stripAccents(s): def stripAccents(s):
return ''.join((c for c in unicodedata.normalize('NFD', toUnicode(s)) if unicodedata.category(c) != 'Mn')) return ''.join((c for c in unicodedata.normalize('NFD', toUnicode(s)) if unicodedata.category(c) != 'Mn'))
def tryUrlencode(s): def tryUrlencode(s):
new = six.u('') new = u''
if isinstance(s, dict): if isinstance(s, dict):
for key, value in s.items(): for key, value in s.iteritems():
new += six.u('&%s=%s') % (key, tryUrlencode(value)) new += u'&%s=%s' % (key, tryUrlencode(value))
return new[1:] return new[1:]
else: else:
+4 -13
View File
@@ -1,5 +1,5 @@
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import natsortKey from couchpotato.core.helpers.variable import natcmp
from urllib import unquote from urllib import unquote
import re import re
@@ -8,13 +8,8 @@ def getParams(params):
reg = re.compile('^[a-z0-9_\.]+$') reg = re.compile('^[a-z0-9_\.]+$')
# Sort keys
param_keys = params.keys()
param_keys.sort(key = natsortKey)
temp = {} temp = {}
for param in param_keys: for param, value in sorted(params.iteritems()):
value = params[param]
nest = re.split("([\[\]]+)", param) nest = re.split("([\[\]]+)", param)
if len(nest) > 1: if len(nest) > 1:
@@ -42,17 +37,13 @@ def getParams(params):
return dictToList(temp) return dictToList(temp)
def dictToList(params): def dictToList(params):
if type(params) is dict: if type(params) is dict:
new = {} new = {}
for x, value in params.items(): for x, value in params.iteritems():
try: try:
convert = lambda text: int(text) if text.isdigit() else text.lower() new_value = [dictToList(value[k]) for k in sorted(value.iterkeys(), cmp = natcmp)]
alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)]
sorted_keys = sorted(value.keys(), key = alphanum_key)
new_value = [dictToList(value[k]) for k in sorted_keys]
except: except:
new_value = value new_value = value
+1 -2
View File
@@ -3,7 +3,6 @@ import xml.etree.ElementTree as XMLTree
log = CPLog(__name__) log = CPLog(__name__)
class RSS(object): class RSS(object):
def getTextElements(self, xml, path): def getTextElements(self, xml, path):
@@ -47,6 +46,6 @@ class RSS(object):
def getItems(self, data, path = 'channel/item'): def getItems(self, data, path = 'channel/item'):
try: try:
return XMLTree.parse(data).findall(path) return XMLTree.parse(data).findall(path)
except Exception as e: except Exception, e:
log.error('Error parsing RSS. %s', e) log.error('Error parsing RSS. %s', e)
return [] return []
+20 -96
View File
@@ -8,32 +8,26 @@ import random
import re import re
import string import string
import sys import sys
import six
from six.moves import map, zip, filter
log = CPLog(__name__) log = CPLog(__name__)
def fnEscape(pattern): def fnEscape(pattern):
return pattern.replace('[', '[[').replace(']', '[]]').replace('[[', '[[]') return pattern.replace('[','[[').replace(']','[]]').replace('[[','[[]')
def link(src, dst): def link(src, dst):
if os.name == 'nt': if os.name == 'nt':
import ctypes import ctypes
if ctypes.windll.kernel32.CreateHardLinkW(six.text_type(dst), six.text_type(src), 0) == 0: raise ctypes.WinError() if ctypes.windll.kernel32.CreateHardLinkW(unicode(dst), unicode(src), 0) == 0: raise ctypes.WinError()
else: else:
os.link(src, dst) os.link(src, dst)
def symlink(src, dst): def symlink(src, dst):
if os.name == 'nt': if os.name == 'nt':
import ctypes import ctypes
if ctypes.windll.kernel32.CreateSymbolicLinkW(six.text_type(dst), six.text_type(src), 1 if os.path.isdir(src) else 0) in [0, 1280]: raise ctypes.WinError() if ctypes.windll.kernel32.CreateSymbolicLinkW(unicode(dst), unicode(src), 1 if os.path.isdir(src) else 0) in [0, 1280]: raise ctypes.WinError()
else: else:
os.symlink(src, dst) os.symlink(src, dst)
def getUserDir(): def getUserDir():
try: try:
import pwd import pwd
@@ -43,7 +37,6 @@ def getUserDir():
return os.path.expanduser('~') return os.path.expanduser('~')
def getDownloadDir(): def getDownloadDir():
user_dir = getUserDir() user_dir = getUserDir()
@@ -56,7 +49,6 @@ def getDownloadDir():
return user_dir return user_dir
def getDataDir(): def getDataDir():
# Windows # Windows
@@ -76,10 +68,8 @@ def getDataDir():
# Linux # Linux
return os.path.join(user_dir, '.couchpotato') return os.path.join(user_dir, '.couchpotato')
def isDict(object):
def isDict(obj): return isinstance(object, dict)
return isinstance(obj, dict)
def mergeDicts(a, b, prepend_list = False): def mergeDicts(a, b, prepend_list = False):
assert isDict(a), isDict(b) assert isDict(a), isDict(b)
@@ -101,7 +91,6 @@ def mergeDicts(a, b, prepend_list = False):
current_dst[key] = current_src[key] current_dst[key] = current_src[key]
return dst return dst
def removeListDuplicates(seq): def removeListDuplicates(seq):
checked = [] checked = []
for e in seq: for e in seq:
@@ -109,73 +98,35 @@ def removeListDuplicates(seq):
checked.append(e) checked.append(e)
return checked return checked
def flattenList(l): def flattenList(l):
if isinstance(l, list): if isinstance(l, list):
return sum(map(flattenList, l)) return sum(map(flattenList, l))
else: else:
return l return l
def md5(text): def md5(text):
return hashlib.md5(ss(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): def isLocalIP(ip):
ip = ip.lstrip('htps:/') ip = ip.lstrip('htps:/')
regex = '/(^127\.)|(^192\.168\.)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^::1)$/' 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.' 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:]
def cleanHost(host):
if not host.startswith(('http://', 'https://')):
host = 'http://' + host
def cleanHost(host, protocol = True, ssl = False, username = None, password = None): host = host.rstrip('/')
"""Return a cleaned up host with given url options set
Changes protocol to https if ssl is set to True and http if ssl is set to false.
>>> cleanHost("localhost:80", ssl=True)
'https://localhost:80/'
>>> cleanHost("localhost:80", ssl=False)
'http://localhost:80/'
Username and password is managed with the username and password variables
>>> cleanHost("localhost:80", username="user", password="passwd")
'http://user:passwd@localhost:80/'
Output without scheme (protocol) can be forced with protocol=False
>>> cleanHost("localhost:80", protocol=False)
'localhost:80'
"""
if not '://' in host and protocol:
host = ('https://' if ssl else 'http://') + host
if not protocol:
host = host.split('://', 1)[-1]
if protocol and username and password:
try:
auth = re.findall('^(?:.+?//)(.+?):(.+?)@(?:.+)$', host)
if auth:
log.error('Cleanhost error: auth already defined in url: %s, please remove BasicAuth from url.', host)
else:
host = host.replace('://', '://%s:%s@' % (username, password), 1)
except:
pass
host = host.rstrip('/ ')
if protocol:
host += '/' host += '/'
return host return host
def getImdb(txt, check_inside = False, multiple = False): def getImdb(txt, check_inside = False, multiple = False):
if not check_inside: if not check_inside:
@@ -192,7 +143,7 @@ def getImdb(txt, check_inside = False, multiple = False):
ids = re.findall('(tt\d{4,7})', txt) ids = re.findall('(tt\d{4,7})', txt)
if multiple: if multiple:
return removeDuplicate(['tt%07d' % tryInt(x[2:]) for x in ids]) if len(ids) > 0 else [] return list(set(['tt%07d' % tryInt(x[2:]) for x in ids])) if len(ids) > 0 else []
return 'tt%07d' % tryInt(ids[0][2:]) return 'tt%07d' % tryInt(ids[0][2:])
except IndexError: except IndexError:
@@ -200,12 +151,10 @@ def getImdb(txt, check_inside = False, multiple = False):
return False return False
def tryInt(s, default = 0): def tryInt(s, default = 0):
try: return int(s) try: return int(s)
except: return default except: return default
def tryFloat(s): def tryFloat(s):
try: try:
if isinstance(s, str): if isinstance(s, str):
@@ -214,17 +163,17 @@ def tryFloat(s):
return float(s) return float(s)
except: return 0 except: return 0
def natsortKey(string_): def natsortKey(s):
"""See http://www.codinghorror.com/blog/archives/001018.html""" return map(tryInt, re.findall(r'(\d+|\D+)', s))
return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', string_)]
def natcmp(a, b):
return cmp(natsortKey(a), natsortKey(b))
def toIterable(value): def toIterable(value):
if isinstance(value, collections.Iterable): if type(value) in [list, tuple]:
return value return value
return [value] return [value]
def getTitle(library_dict): def getTitle(library_dict):
try: try:
try: try:
@@ -247,7 +196,6 @@ def getTitle(library_dict):
log.error('Could not get title for library item: %s', library_dict) log.error('Could not get title for library item: %s', library_dict)
return None return None
def possibleTitles(raw_title): def possibleTitles(raw_title):
titles = [ titles = [
@@ -260,42 +208,18 @@ def possibleTitles(raw_title):
new_title = raw_title.replace('&', 'and') new_title = raw_title.replace('&', 'and')
titles.append(simplifyString(new_title)) titles.append(simplifyString(new_title))
return removeDuplicate(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 = ',', clean = True): def splitString(str, split_on = ',', clean = True):
l = [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 removeEmpty(l) if clean else l return filter(None, list) if clean else list
def removeEmpty(l):
return list(filter(None, l))
def removeDuplicate(l):
seen = set()
return [x for x in l if x not in seen and not seen.add(x)]
def dictIsSubset(a, b): def dictIsSubset(a, b):
return all([k in b and b[k] == v for k, v in a.items()]) return all([k in b and b[k] == v for k, v in a.items()])
def isSubFolder(sub_folder, base_folder): def isSubFolder(sub_folder, base_folder):
# Returns True if sub_folder is the same as or inside base_folder # Returns True is sub_folder is the same as or in base_folder
return base_folder and sub_folder and ss(os.path.normpath(base_folder).rstrip(os.path.sep) + os.path.sep) in ss(os.path.normpath(sub_folder).rstrip(os.path.sep) + os.path.sep) return base_folder.rstrip(os.path.sep) + os.path.sep in sub_folder.rstrip(os.path.sep) + os.path.sep
# From SABNZBD
re_password = [re.compile(r'([^/\\]+)[/\\](.+)'), re.compile(r'(.+){{([^{}]+)}}$'), re.compile(r'(.+)\s+password\s*=\s*(.+)$', re.I)]
def scanForPassword(name):
m = None
for reg in re_password:
m = reg.search(name)
if m: break
if m:
return m.group(1).strip('. '), m.group(2).strip()
+4 -5
View File
@@ -1,10 +1,9 @@
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from importhelper import import_module from importlib import import_module
import os import os
import sys import sys
import traceback import traceback
import six
log = CPLog(__name__) log = CPLog(__name__)
@@ -38,7 +37,7 @@ class Loader(object):
self.paths['custom_plugins'] = (30, '', custom_plugin_dir) self.paths['custom_plugins'] = (30, '', custom_plugin_dir)
# Loop over all paths and add to module list # Loop over all paths and add to module list
for plugin_type, plugin_tuple in self.paths.items(): for plugin_type, plugin_tuple in self.paths.iteritems():
priority, module, dir_name = plugin_tuple priority, module, dir_name = plugin_tuple
self.addFromDir(plugin_type, priority, module, dir_name) self.addFromDir(plugin_type, priority, module, dir_name)
@@ -46,7 +45,7 @@ class Loader(object):
did_save = 0 did_save = 0
for priority in sorted(self.modules): for priority in sorted(self.modules):
for module_name, plugin in sorted(self.modules[priority].items()): for module_name, plugin in sorted(self.modules[priority].iteritems()):
# Load module # Load module
try: try:
@@ -82,7 +81,7 @@ class Loader(object):
for filename in os.listdir(root_path): for filename in os.listdir(root_path):
path = os.path.join(root_path, filename) path = os.path.join(root_path, filename)
if os.path.isdir(path) and filename[:2] != '__': if os.path.isdir(path) and filename[:2] != '__':
if six.u('__init__.py') in os.listdir(path): if u'__init__.py' in os.listdir(path):
new_base_path = ''.join(s + '.' for s in base_path) + filename new_base_path = ''.join(s + '.' for s in base_path) + filename
self.paths[new_base_path.replace('.', '_')] = (priority, new_base_path, path) self.paths[new_base_path.replace('.', '_')] = (priority, new_base_path, path)
+4 -5
View File
@@ -1,7 +1,6 @@
import logging import logging
import re import re
class CPLog(object): class CPLog(object):
context = '' context = ''
@@ -38,7 +37,7 @@ class CPLog(object):
def safeMessage(self, msg, replace_tuple = ()): def safeMessage(self, msg, replace_tuple = ()):
from couchpotato.environment import Env from couchpotato.environment import Env
from couchpotato.core.helpers.encoding import ss, toUnicode from couchpotato.core.helpers.encoding import ss
msg = ss(msg) msg = ss(msg)
@@ -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 Exception as e: except Exception, e:
self.logger.error('Failed encoding stuff to log "%s": %s' % (msg, e)) self.logger.error(u'Failed encoding stuff to log "%s": %s' % (msg, e))
if not Env.get('dev'): if not Env.get('dev'):
@@ -67,4 +66,4 @@ class CPLog(object):
except: except:
pass pass
return toUnicode(msg) return msg
+15 -24
View File
@@ -1,11 +1,9 @@
import traceback from couchpotato import get_session
from couchpotato import get_session, CPLog
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Media from couchpotato.core.settings.model import Media
log = CPLog(__name__)
class MediaBase(Plugin): class MediaBase(Plugin):
@@ -13,13 +11,20 @@ class MediaBase(Plugin):
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': {}, 'category': {},
} }
search_dict = mergeDicts({
'library': {
'related_libraries': {},
'root_library': {}
},
}, default_dict)
def initType(self): def initType(self):
addEvent('media.types', self.getType) addEvent('media.types', self.getType)
@@ -29,33 +34,19 @@ class MediaBase(Plugin):
def createOnComplete(self, id): def createOnComplete(self, id):
def onComplete(): def onComplete():
try:
db = get_session() db = get_session()
media = db.query(Media).filter_by(id = id).first() media = db.query(Media).filter_by(id = id).first()
media_dict = media.to_dict(self.default_dict) fireEventAsync('%s.searcher.single' % media.type, media.to_dict(self.search_dict), on_complete = self.createNotifyFront(id))
event_name = '%s.searcher.single' % media.type db.expire_all()
fireEvent(event_name, media_dict, on_complete = self.createNotifyFront(id))
except:
log.error('Failed creating onComplete: %s', traceback.format_exc())
finally:
db.close()
return onComplete return onComplete
def createNotifyFront(self, media_id): def createNotifyFront(self, media_id):
def notifyFront(): def notifyFront():
try:
db = get_session() db = get_session()
media = db.query(Media).filter_by(id = media_id).first() media = db.query(Media).filter_by(id = media_id).first()
media_dict = media.to_dict(self.default_dict) fireEvent('notify.frontend', type = '%s.update' % media.type, data = media.to_dict(self.default_dict))
event_name = '%s.update' % media.type db.expire_all()
fireEvent('notify.frontend', type = event_name, data = media_dict)
except:
log.error('Failed creating onComplete: %s', traceback.format_exc())
finally:
db.close()
return notifyFront return notifyFront
@@ -1,13 +1,6 @@
from couchpotato.core.event import addEvent from .main import Library
from couchpotato.core.plugins.base import Plugin
def start():
return Library()
class LibraryBase(Plugin): config = []
_type = None
def initType(self):
addEvent('library.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,18 @@
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.media._base.library.base import LibraryBase
class Library(LibraryBase):
def __init__(self):
addEvent('library.title', self.title)
def title(self, library):
return fireEvent(
'library.query',
library,
condense = False,
include_year = False,
include_identifier = False,
single = True
)
@@ -0,0 +1,6 @@
from .main import Matcher
def start():
return Matcher()
config = []
@@ -0,0 +1,84 @@
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import simplifyString
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
log = CPLog(__name__)
class MatcherBase(Plugin):
type = None
def __init__(self):
if self.type:
addEvent('%s.matcher.correct' % self.type, self.correct)
def correct(self, chain, release, media, quality):
raise NotImplementedError()
def flattenInfo(self, info):
# Flatten dictionary of matches (chain info)
if isinstance(info, dict):
return dict([(key, self.flattenInfo(value)) for key, value in info.items()])
# Flatten matches
result = None
for match in info:
if isinstance(match, dict):
if result is None:
result = {}
for key, value in match.items():
if key not in result:
result[key] = []
result[key].append(value)
else:
if result is None:
result = []
result.append(match)
return result
def constructFromRaw(self, match):
if not match:
return None
parts = [
''.join([
y for y in x[1:] if y
]) for x in match
]
return ''.join(parts)[:-1].strip()
def simplifyValue(self, value):
if not value:
return value
if isinstance(value, basestring):
return simplifyString(value)
if isinstance(value, list):
return [self.simplifyValue(x) for x in value]
raise ValueError("Unsupported value type")
def chainMatch(self, chain, group, tags):
info = self.flattenInfo(chain.info[group])
found_tags = []
for tag, accepted in tags.items():
values = [self.simplifyValue(x) for x in info.get(tag, [None])]
if any([val in accepted for val in values]):
found_tags.append(tag)
log.debug('tags found: %s, required: %s' % (found_tags, tags.keys()))
if set(tags.keys()) == set(found_tags):
return True
return all([key in found_tags for key, value in tags.items()])
@@ -0,0 +1,88 @@
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import possibleTitles
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.matcher.base import MatcherBase
from caper import Caper
log = CPLog(__name__)
class Matcher(MatcherBase):
def __init__(self):
super(Matcher, self).__init__()
self.caper = Caper()
addEvent('matcher.parse', self.parse)
addEvent('matcher.match', self.match)
addEvent('matcher.flatten_info', self.flattenInfo)
addEvent('matcher.construct_from_raw', self.constructFromRaw)
addEvent('matcher.correct_title', self.correctTitle)
addEvent('matcher.correct_quality', self.correctQuality)
def parse(self, name, parser='scene'):
return self.caper.parse(name, parser)
def match(self, release, media, quality):
match = fireEvent('matcher.parse', release['name'], single = True)
if len(match.chains) < 1:
log.info2('Wrong: %s, unable to parse release name (no chains)', release['name'])
return False
for chain in match.chains:
if fireEvent('%s.matcher.correct' % media['type'], chain, release, media, quality, single = True):
return chain
return False
def correctTitle(self, chain, media):
root_library = media['library']['root_library']
if 'show_name' not in chain.info or not len(chain.info['show_name']):
log.info('Wrong: missing show name in parsed result')
return False
# Get the lower-case parsed show name from the chain
chain_words = [x.lower() for x in chain.info['show_name']]
# Build a list of possible titles of the media we are searching for
titles = root_library['info']['titles']
# Add year suffix titles (will result in ['<name_one>', '<name_one> <suffix_one>', '<name_two>', ...])
suffixes = [None, root_library['info']['year']]
titles = [
title + ((' %s' % suffix) if suffix else '')
for title in titles
for suffix in suffixes
]
# Check show titles match
# TODO check xem names
for title in titles:
for valid_words in [x.split(' ') for x in possibleTitles(title)]:
if valid_words == chain_words:
return True
return False
def correctQuality(self, chain, quality, quality_map):
if quality['identifier'] not in quality_map:
log.info2('Wrong: unknown preferred quality %s', quality['identifier'])
return False
if 'video' not in chain.info:
log.info2('Wrong: no video tags found')
return False
video_tags = quality_map[quality['identifier']]
if not self.chainMatch(chain, 'video', video_tags):
log.info2('Wrong: %s tags not in chain', video_tags)
return False
return True
@@ -1,6 +1,5 @@
from .main import MediaPlugin from .main import MediaPlugin
def start(): def start():
return MediaPlugin() return MediaPlugin()
+22 -44
View File
@@ -1,9 +1,8 @@
import traceback from couchpotato import get_session
from couchpotato import get_session, tryInt
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 from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import mergeDicts, splitString, getImdb, getTitle from couchpotato.core.helpers.variable import mergeDicts, splitString, getImdb
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media import MediaBase from couchpotato.core.media import MediaBase
from couchpotato.core.settings.model import Library, LibraryTitle, Release, \ from couchpotato.core.settings.model import Library, LibraryTitle, Release, \
@@ -71,39 +70,26 @@ class MediaPlugin(MediaBase):
addEvent('media.restatus', self.restatus) addEvent('media.restatus', self.restatus)
def refresh(self, id = '', **kwargs): def refresh(self, id = '', **kwargs):
handlers = [] db = get_session()
ids = splitString(id)
for x in ids: for x in splitString(id):
media = db.query(Media).filter_by(id = x).first()
refresh_handler = self.createRefreshHandler(x) if media:
if refresh_handler: # Get current selected title
handlers.append(refresh_handler) default_title = ''
for title in media.library.titles:
if title.default: default_title = title.title
fireEvent('notify.frontend', type = 'media.busy', data = {'id': [tryInt(x) for x in ids]}) fireEvent('notify.frontend', type = '%s.busy' % media.type, data = {'id': x})
fireEventAsync('schedule.queue', handlers = handlers) fireEventAsync('library.update.%s' % media.type, identifier = media.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x))
db.expire_all()
return { return {
'success': True, 'success': True,
} }
def createRefreshHandler(self, id):
db = get_session()
media = db.query(Media).filter_by(id = id).first()
if media:
default_title = getTitle(media.library)
identifier = media.library.identifier
event = 'library.update.%s' % media.type
def handler():
fireEvent(event, identifier = identifier, default_title = default_title, on_complete = self.createOnComplete(id))
if handler:
return handler
def addSingleRefreshView(self): def addSingleRefreshView(self):
for media_type in fireEvent('media.types', merge = True): for media_type in fireEvent('media.types', merge = True):
@@ -124,6 +110,7 @@ class MediaPlugin(MediaBase):
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 getView(self, id = None, **kwargs): def getView(self, id = None, **kwargs):
@@ -216,14 +203,14 @@ class MediaPlugin(MediaBase):
# List release statuses # List release statuses
releases = db.query(Release) \ releases = db.query(Release) \
.filter(Release.movie_id.in_(media_ids)) \ .filter(Release.media_id.in_(media_ids)) \
.all() .all()
release_statuses = dict((m, set()) for m in media_ids) release_statuses = dict((m, set()) for m in media_ids)
releases_count = dict((m, 0) for m in media_ids) releases_count = dict((m, 0) for m in media_ids)
for release in releases: for release in releases:
release_statuses[release.movie_id].add('%d,%d' % (release.status_id, release.quality_id)) release_statuses[release.media_id].add('%d,%d' % (release.status_id, release.quality_id))
releases_count[release.movie_id] += 1 releases_count[release.media_id] += 1
# Get main movie data # Get main movie data
q2 = db.query(Media) \ q2 = db.query(Media) \
@@ -252,13 +239,14 @@ class MediaPlugin(MediaBase):
# Merge releases with movie dict # Merge releases with movie dict
movies.append(mergeDicts(movie_dict[media_id].to_dict({ movies.append(mergeDicts(movie_dict[media_id].to_dict({
'library': {'titles': {}, 'files': {}}, 'library': {'titles': {}, 'files':{}},
'files': {}, 'files': {},
}), { }), {
'releases': releases, 'releases': releases,
'releases_count': releases_count.get(media_id), 'releases_count': releases_count.get(media_id),
})) }))
db.expire_all()
return total_count, movies return total_count, movies
def listView(self, **kwargs): def listView(self, **kwargs):
@@ -352,6 +340,7 @@ class MediaPlugin(MediaBase):
if len(chars) == 25: if len(chars) == 25:
break break
db.expire_all()
return ''.join(sorted(chars)) return ''.join(sorted(chars))
def charView(self, **kwargs): def charView(self, **kwargs):
@@ -376,7 +365,6 @@ class MediaPlugin(MediaBase):
def delete(self, media_id, delete_from = None): def delete(self, media_id, delete_from = None):
try:
db = get_session() db = get_session()
media = db.query(Media).filter_by(id = media_id).first() media = db.query(Media).filter_by(id = media_id).first()
@@ -419,12 +407,8 @@ class MediaPlugin(MediaBase):
if deleted: if deleted:
fireEvent('notify.frontend', type = 'movie.deleted', data = media.to_dict()) fireEvent('notify.frontend', type = 'movie.deleted', data = media.to_dict())
except:
log.error('Failed deleting media: %s', traceback.format_exc())
db.rollback()
finally:
db.close()
db.expire_all()
return True return True
def deleteView(self, id = '', **kwargs): def deleteView(self, id = '', **kwargs):
@@ -448,7 +432,6 @@ class MediaPlugin(MediaBase):
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True) active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
try:
db = get_session() db = get_session()
m = db.query(Media).filter_by(id = media_id).first() m = db.query(Media).filter_by(id = media_id).first()
@@ -472,9 +455,4 @@ class MediaPlugin(MediaBase):
db.commit() db.commit()
return True return True
except:
log.error('Failed restatus: %s', traceback.format_exc())
db.rollback()
finally:
db.close()
@@ -1,6 +1,5 @@
from .main import Search from .main import Search
def start(): def start():
return Search() return Search()
@@ -1,6 +1,5 @@
from .main import Searcher from .main import Searcher
def start(): def start():
return Searcher() return Searcher()
@@ -12,6 +12,7 @@ class SearcherBase(Plugin):
def __init__(self): def __init__(self):
super(SearcherBase, self).__init__() super(SearcherBase, self).__init__()
addEvent('searcher.progress', self.getProgress) addEvent('searcher.progress', self.getProgress)
addEvent('%s.searcher.progress' % self.getType(), self.getProgress) addEvent('%s.searcher.progress' % self.getType(), self.getProgress)
@@ -25,6 +26,7 @@ class SearcherBase(Plugin):
_type = self.getType() _type = self.getType()
def setCrons(): def setCrons():
fireEvent('schedule.cron', '%s.searcher.all' % _type, self.searchAll, fireEvent('schedule.cron', '%s.searcher.all' % _type, self.searchAll,
day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute')) day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
+14 -8
View File
@@ -1,11 +1,17 @@
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 simplifyString from couchpotato.core.helpers.encoding import simplifyString, toUnicode
from couchpotato.core.helpers.variable import splitString, removeEmpty, removeDuplicate from couchpotato.core.helpers.variable import md5, getTitle, splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.searcher.base import SearcherBase from couchpotato.core.media._base.searcher.base import SearcherBase
from couchpotato.core.settings.model import Media, Release, ReleaseInfo
from couchpotato.environment import Env
from inspect import ismethod, isfunction
import datetime import datetime
import re import re
import time
import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -150,12 +156,12 @@ class Searcher(SearcherBase):
try: check_names.append(max(re.findall(r'[^[]*\[([^]]*)\]', check_name), key = len).strip()) try: check_names.append(max(re.findall(r'[^[]*\[([^]]*)\]', check_name), key = len).strip())
except: pass except: pass
for check_name in removeDuplicate(check_names): for check_name in list(set(check_names)):
check_movie = fireEvent('scanner.name_year', check_name, single = True) check_movie = fireEvent('scanner.name_year', check_name, single = True)
try: try:
check_words = removeEmpty(re.split('\W+', check_movie.get('name', ''))) check_words = filter(None, re.split('\W+', check_movie.get('name', '')))
movie_words = removeEmpty(re.split('\W+', simplifyString(movie_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: if len(check_words) > 0 and len(movie_words) > 0 and len(list(set(check_words) - set(movie_words))) == 0:
return True return True
@@ -165,7 +171,7 @@ class Searcher(SearcherBase):
return False return False
def correctWords(self, rel_name, media): def correctWords(self, rel_name, media):
media_title = fireEvent('searcher.get_search_title', media, single = True) media_title = fireEvent('library.title', media['library'], single = True)
media_words = re.split('\W+', simplifyString(media_title)) media_words = re.split('\W+', simplifyString(media_title))
rel_name = simplifyString(rel_name) rel_name = simplifyString(rel_name)
@@ -173,7 +179,7 @@ class Searcher(SearcherBase):
# Make sure it has required words # Make sure it has required words
required_words = splitString(self.conf('required_words', section = 'searcher').lower()) required_words = splitString(self.conf('required_words', section = 'searcher').lower())
try: required_words = removeDuplicate(required_words + splitString(media['category']['required'].lower())) try: required_words = list(set(required_words + splitString(media['category']['required'].lower())))
except: pass except: pass
req_match = 0 req_match = 0
@@ -187,7 +193,7 @@ class Searcher(SearcherBase):
# Ignore releases # Ignore releases
ignored_words = splitString(self.conf('ignored_words', section = 'searcher').lower()) ignored_words = splitString(self.conf('ignored_words', section = 'searcher').lower())
try: ignored_words = removeDuplicate(ignored_words + splitString(media['category']['ignored'].lower())) try: ignored_words = list(set(ignored_words + splitString(media['category']['ignored'].lower())))
except: pass except: pass
ignored_match = 0 ignored_match = 0
@@ -1,6 +1,5 @@
from .main import MovieBase from .main import MovieBase
def start(): def start():
return MovieBase() return MovieBase()
+6 -28
View File
@@ -1,9 +1,8 @@
import traceback
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, fireEventAsync, addEvent from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import splitString, tryInt, getTitle from couchpotato.core.helpers.variable import splitString, tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media.movie import MovieTypeBase from couchpotato.core.media.movie import MovieTypeBase
from couchpotato.core.settings.model import Media from couchpotato.core.settings.model import Media
@@ -62,6 +61,7 @@ class MovieBase(MovieTypeBase):
except: except:
pass pass
library = fireEvent('library.add.movie', single = True, attrs = params, update_after = update_library) library = fireEvent('library.add.movie', single = True, attrs = params, update_after = update_library)
# Status # Status
@@ -71,7 +71,6 @@ class MovieBase(MovieTypeBase):
default_profile = fireEvent('profile.default', single = True) default_profile = fireEvent('profile.default', single = True)
cat_id = params.get('category_id') cat_id = params.get('category_id')
try:
db = get_session() db = get_session()
m = db.query(Media).filter_by(library_id = library.get('id')).first() m = db.query(Media).filter_by(library_id = library.get('id')).first()
added = True added = True
@@ -130,22 +129,10 @@ class MovieBase(MovieTypeBase):
onComplete() onComplete()
if added: if added:
if params.get('title'): fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = 'Successfully added "%s" to your wanted list.' % params.get('title', ''))
message = 'Successfully added "%s" to your wanted list.' % params.get('title', '')
else:
title = getTitle(m.library)
if title:
message = 'Successfully added "%s" to your wanted list.' % title
else:
message = 'Succesfully added to your wanted list.'
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = message)
db.expire_all()
return movie_dict return movie_dict
except:
log.error('Failed deleting media: %s', traceback.format_exc())
db.rollback()
finally:
db.close()
def addView(self, **kwargs): def addView(self, **kwargs):
add_dict = self.add(params = kwargs) add_dict = self.add(params = kwargs)
@@ -157,7 +144,6 @@ class MovieBase(MovieTypeBase):
def edit(self, id = '', **kwargs): def edit(self, id = '', **kwargs):
try:
db = get_session() db = get_session()
available_status = fireEvent('status.get', 'available', single = True) available_status = fireEvent('status.get', 'available', single = True)
@@ -190,18 +176,10 @@ class MovieBase(MovieTypeBase):
fireEvent('media.restatus', m.id) fireEvent('media.restatus', m.id)
movie_dict = m.to_dict(self.default_dict) movie_dict = m.to_dict(self.search_dict)
fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(media_id)) fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(media_id))
db.expire_all()
return { return {
'success': True, 'success': True,
} }
except:
log.error('Failed deleting media: %s', traceback.format_exc())
db.rollback()
finally:
db.close()
return {
'success': False,
}
@@ -36,10 +36,10 @@ var Movie = new Class({
App.on('movie.update', self.global_events['movie.update']); App.on('movie.update', self.global_events['movie.update']);
// Add spinner on load / search // Add spinner on load / search
['media.busy', 'movie.searcher.started'].each(function(listener){ ['movie.busy', 'movie.searcher.started'].each(function(listener){
self.global_events[listener] = function(notification){ self.global_events[listener] = function(notification){
if(notification.data && (self.data.id == notification.data.id || (typeOf(notification.data.id) == 'array' && notification.data.id.indexOf(self.data.id) > -1))) if(notification.data && self.data.id == notification.data.id)
self.busy(true); self.busy(true)
} }
App.on(listener, self.global_events[listener]); App.on(listener, self.global_events[listener]);
}) })
@@ -54,7 +54,7 @@ var Movie = new Class({
// Reload when releases have updated // Reload when releases have updated
self.global_events['release.update_status'] = function(notification){ self.global_events['release.update_status'] = function(notification){
var data = notification.data var data = notification.data
if(data && self.data.id == data.movie_id){ if(data && self.data.id == data.media_id){
if(!self.data.releases) if(!self.data.releases)
self.data.releases = []; self.data.releases = [];
@@ -181,7 +181,7 @@ Block.Search.MovieItem = new Class({
if(categories.length == 0) if(categories.length == 0)
self.category_select.hide(); self.category_select.hide();
else { else {
self.category_select.show(); self.category_select.movie();
categories.each(function(category){ categories.each(function(category){
new Element('option', { new Element('option', {
'value': category.data.id, 'value': category.data.id,
@@ -1,6 +1,5 @@
from .main import MovieLibraryPlugin from .main import MovieLibraryPlugin
def start(): def start():
return MovieLibraryPlugin() return MovieLibraryPlugin()
@@ -2,43 +2,60 @@ 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.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.library import LibraryBase from couchpotato.core.media._base.library.base 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
import traceback import traceback
import six
log = CPLog(__name__) log = CPLog(__name__)
class MovieLibraryPlugin(LibraryBase): class MovieLibraryPlugin(LibraryBase):
default_dict = {'titles': {}, 'files': {}} default_dict = {'titles': {}, 'files':{}}
def __init__(self): def __init__(self):
addEvent('library.query', self.query)
addEvent('library.add.movie', self.add) addEvent('library.add.movie', self.add)
addEvent('library.update.movie', self.update) addEvent('library.update.movie', self.update)
addEvent('library.update.movie.release_date', self.updateReleaseDate) addEvent('library.update.movie.release_date', self.updateReleaseDate)
def add(self, attrs = None, update_after = True): def query(self, library, first = True, include_year = True, **kwargs):
if not attrs: attrs = {} if library.get('type') != 'movie':
return
titles = [title['title'] for title in library['titles']]
# Add year identifier to titles
if include_year:
titles = [title + (' %s' % str(library['year'])) for title in titles]
if first:
return titles[0] if titles else None
return titles
def add(self, attrs = {}, update_after = True):
# movies don't yet contain these, so lets make sure to set defaults
type = attrs.get('type', 'movie')
primary_provider = attrs.get('primary_provider', 'imdb') primary_provider = attrs.get('primary_provider', 'imdb')
try:
db = get_session() db = get_session()
l = db.query(Library).filter_by(identifier = attrs.get('identifier')).first() l = db.query(Library).filter_by(type = type, identifier = attrs.get('identifier')).first()
if not l: if not l:
status = fireEvent('status.get', 'needs_update', single = True) status = fireEvent('status.get', 'needs_update', single = True)
l = Library( l = Library(
type = type,
primary_provider = primary_provider,
year = attrs.get('year'), year = attrs.get('year'),
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 = {} info = {},
parent = None
) )
title = LibraryTitle( title = LibraryTitle(
@@ -57,27 +74,26 @@ class MovieLibraryPlugin(LibraryBase):
handle('library.update.movie', 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
except:
log.error('Failed adding media: %s', traceback.format_exc())
db.rollback()
finally:
db.close()
return {} def update(self, identifier, default_title = '', force = False):
def update(self, identifier, default_title = '', extended = False):
if self.shuttingDown(): if self.shuttingDown():
return return
try:
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)
info = fireEvent('movie.info', merge = True, extended = extended, identifier = identifier) library_dict = None
if library:
library_dict = library.to_dict(self.default_dict)
do_update = True
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']
@@ -90,6 +106,7 @@ class MovieLibraryPlugin(LibraryBase):
return False return False
# Main info # Main info
if do_update:
library.plot = toUnicode(info.get('plot', '')) library.plot = toUnicode(info.get('plot', ''))
library.tagline = toUnicode(info.get('tagline', '')) library.tagline = toUnicode(info.get('tagline', ''))
library.year = info.get('year', 0) library.year = info.get('year', 0)
@@ -104,17 +121,6 @@ class MovieLibraryPlugin(LibraryBase):
titles = info.get('titles', []) titles = info.get('titles', [])
log.debug('Adding titles: %s', titles) log.debug('Adding titles: %s', titles)
counter = 0 counter = 0
def_title = None
for title in titles:
if (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == six.u('') and toUnicode(titles[0]) == title):
def_title = toUnicode(title)
break
counter += 1
if not def_title:
def_title = toUnicode(titles[0])
for title in titles: for title in titles:
if not title: if not title:
continue continue
@@ -122,9 +128,10 @@ class MovieLibraryPlugin(LibraryBase):
t = LibraryTitle( t = LibraryTitle(
title = title, title = title,
simple_title = self.simplifyTitle(title), simple_title = self.simplifyTitle(title),
default = title == def_title default = (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == u'' and toUnicode(titles[0]) == title)
) )
library.titles.append(t) library.titles.append(t)
counter += 1
db.commit() db.commit()
@@ -146,43 +153,30 @@ class MovieLibraryPlugin(LibraryBase):
break break
except: except:
log.debug('Failed to attach to library: %s', traceback.format_exc()) log.debug('Failed to attach to library: %s', traceback.format_exc())
db.rollback()
library_dict = library.to_dict(self.default_dict) library_dict = library.to_dict(self.default_dict)
return library_dict
except:
log.error('Failed update media: %s', traceback.format_exc())
db.rollback()
finally:
db.close()
return {} db.expire_all()
return library_dict
def updateReleaseDate(self, identifier): def updateReleaseDate(self, identifier):
try:
db = get_session() db = get_session()
library = db.query(Library).filter_by(identifier = identifier).first() library = db.query(Library).filter_by(identifier = identifier).first()
if not library.info: if not library.info:
library_dict = self.update(identifier) library_dict = self.update(identifier, force = True)
dates = library_dict.get('info', {}).get('release_date') dates = library_dict.get('info', {}).get('release_date')
else: else:
dates = library.info.get('release_date') dates = library.info.get('release_date')
if dates and (dates.get('expires', 0) < time.time() or dates.get('expires', 0) > time.time() + (604800 * 4)) or not dates: if dates and (dates.get('expires', 0) < time.time() or dates.get('expires', 0) > time.time() + (604800 * 4)) or not dates:
dates = fireEvent('movie.release_date', identifier = identifier, merge = True) dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
library.info.update({'release_date': dates}) library.info.update({'release_date': dates })
db.commit() db.commit()
db.expire_all()
return dates return dates
except:
log.error('Failed updating release dates: %s', traceback.format_exc())
db.rollback()
finally:
db.close()
return {}
def simplifyTitle(self, title): def simplifyTitle(self, title):
@@ -1,7 +1,6 @@
from .main import MovieSearcher from .main import MovieSearcher
import random import random
def start(): def start():
return MovieSearcher() return MovieSearcher()
+19 -32
View File
@@ -30,7 +30,6 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
addEvent('movie.searcher.try_next_release', self.tryNextRelease) addEvent('movie.searcher.try_next_release', self.tryNextRelease)
addEvent('movie.searcher.could_be_released', self.couldBeReleased) addEvent('movie.searcher.could_be_released', self.couldBeReleased)
addEvent('searcher.correct_release', self.correctRelease) addEvent('searcher.correct_release', self.correctRelease)
addEvent('searcher.get_search_title', self.getSearchTitle)
addApiView('movie.searcher.try_next', self.tryNextReleaseView, docs = { addApiView('movie.searcher.try_next', self.tryNextReleaseView, docs = {
'desc': 'Marks the snatched results as ignored and try the next best release', 'desc': 'Marks the snatched results as ignored and try the next best release',
@@ -73,21 +72,10 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
db = get_session() db = get_session()
movies_raw = db.query(Media).filter( movies = db.query(Media).filter(
Media.status.has(identifier = 'active') Media.status.has(identifier = 'active')
).all() ).all()
random.shuffle(movies)
random.shuffle(movies_raw)
movies = []
for m in movies_raw:
movies.append(m.to_dict({
'category': {},
'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files': {}},
'files': {},
}))
self.in_progress = { self.in_progress = {
'total': len(movies), 'total': len(movies),
@@ -98,14 +86,21 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
search_protocols = fireEvent('searcher.protocols', single = True) search_protocols = fireEvent('searcher.protocols', single = True)
for movie in movies: for movie in movies:
movie_dict = movie.to_dict({
'category': {},
'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files':{}},
'files': {},
})
try: try:
self.single(movie, search_protocols) self.single(movie_dict, search_protocols)
except IndexError: except IndexError:
log.error('Forcing library update for %s, if you see this often, please report: %s', (movie['library']['identifier'], traceback.format_exc())) 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['library']['identifier']) fireEvent('library.update.movie', movie_dict['library']['identifier'], force = True)
except: except:
log.error('Search failed for %s: %s', (movie['library']['identifier'], traceback.format_exc())) log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
self.in_progress['to_go'] -= 1 self.in_progress['to_go'] -= 1
@@ -121,7 +116,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
def single(self, movie, search_protocols = None, manual = False): def single(self, movie, search_protocols = None, manual = False):
# movies don't contain 'type' yet, so just set to default here # movies don't contain 'type' yet, so just set to default here
if 'type' not in movie: if not movie.has_key('type'):
movie['type'] = 'movie' movie['type'] = 'movie'
# Find out search type # Find out search type
@@ -137,6 +132,8 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
log.debug('Movie doesn\'t have a profile or already done, assuming in manage tab.') log.debug('Movie doesn\'t have a profile or already done, assuming in manage tab.')
return return
db = get_session()
pre_releases = fireEvent('quality.pre_releases', single = True) pre_releases = fireEvent('quality.pre_releases', single = True)
release_dates = fireEvent('library.update.movie.release_date', identifier = movie['library']['identifier'], merge = 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) available_status, ignored_status, failed_status = fireEvent('status.get', ['available', 'ignored', 'failed'], single = True)
@@ -152,7 +149,6 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
fireEvent('notify.frontend', type = 'movie.searcher.started', data = {'id': movie['id']}, message = 'Searching for "%s"' % default_title) fireEvent('notify.frontend', type = 'movie.searcher.started', data = {'id': movie['id']}, message = 'Searching for "%s"' % default_title)
db = get_session()
ret = False ret = False
for quality_type in movie['profile']['types']: for quality_type in movie['profile']['types']:
@@ -213,7 +209,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
if media.get('type') != 'movie': return if media.get('type') != 'movie': return
media_title = fireEvent('searcher.get_search_title', media, single = True) media_title = fireEvent('library.title', media['library'], single = True)
imdb_results = kwargs.get('imdb_results', False) imdb_results = kwargs.get('imdb_results', False)
retention = Env.setting('retention', section = 'nzb') retention = Env.setting('retention', section = 'nzb')
@@ -282,15 +278,13 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
now = int(time.time()) now = int(time.time())
now_year = date.today().year now_year = date.today().year
now_month = date.today().month
if (year is None or year < now_year - 1) and (not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0)): 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 return True
else: else:
# Don't allow movies with years to far in the future # Don't allow movies with years to far in the future
add_year = 1 if now_month > 10 else 0 # Only allow +1 year if end of the year if year is not None and year > now_year + 1:
if year is not None and year > (now_year + add_year):
return False return False
# For movies before 1972 # For movies before 1972
@@ -334,7 +328,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
try: try:
db = get_session() db = get_session()
rels = db.query(Release) \ rels = db.query(Release) \
.filter_by(movie_id = media_id) \ .filter_by(media_id = media_id) \
.filter(Release.status_id.in_([snatched_status.get('id'), done_status.get('id')])) \ .filter(Release.status_id.in_([snatched_status.get('id'), done_status.get('id')])) \
.all() .all()
@@ -350,14 +344,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
except: except:
log.error('Failed searching for next release: %s', traceback.format_exc()) log.error('Failed searching for next release: %s', traceback.format_exc())
db.rollback()
return False return False
finally:
db.close()
def getSearchTitle(self, media):
if media['type'] == 'movie':
return getTitle(media['library'])
class SearchSetupError(Exception): class SearchSetupError(Exception):
pass pass
@@ -1,6 +1,5 @@
from .main import Suggestion from .main import Suggestion
def start(): def start():
return Suggestion() return Suggestion()
@@ -1,7 +1,7 @@
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.variable import splitString, removeDuplicate from couchpotato.core.helpers.variable import splitString
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Media, Library from couchpotato.core.settings.model import Media, Library
from couchpotato.environment import Env from couchpotato.environment import Env
@@ -79,10 +79,8 @@ class Suggestion(Plugin):
seen = [] if not seen else seen seen = [] if not seen else seen
if ignore_imdb: if ignore_imdb:
suggested_imdbs = []
for cs in cached_suggestion: for cs in cached_suggestion:
if cs.get('imdb') != ignore_imdb and cs.get('imdb') not in suggested_imdbs: if cs.get('imdb') != ignore_imdb:
suggested_imdbs.append(cs.get('imdb'))
new_suggestions.append(cs) new_suggestions.append(cs)
# Get new results and add them # Get new results and add them
@@ -99,7 +97,7 @@ class Suggestion(Plugin):
movies.extend(seen) movies.extend(seen)
ignored.extend([x.get('imdb') for x in cached_suggestion]) ignored.extend([x.get('imdb') for x in cached_suggestion])
suggestions = fireEvent('movie.suggest', movies = movies, ignore = removeDuplicate(ignored), single = True) suggestions = fireEvent('movie.suggest', movies = movies, ignore = list(set(ignored)), single = True)
if suggestions: if suggestions:
new_suggestions.extend(suggestions) new_suggestions.extend(suggestions)
@@ -101,7 +101,7 @@ var SuggestList = new Class({
// Add rating // Add rating
m.info_container.adopt( m.info_container.adopt(
m.rating = m.info.rating && m.info.rating.imdb && m.info.rating.imdb.length == 2 && parseFloat(m.info.rating.imdb[0]) > 0 ? new Element('span.rating', { m.rating = m.info.rating && m.info.rating.imdb.length == 2 && parseFloat(m.info.rating.imdb[0]) > 0 ? new Element('span.rating', {
'text': parseFloat(m.info.rating.imdb[0]), 'text': parseFloat(m.info.rating.imdb[0]),
'title': parseInt(m.info.rating.imdb[1]) + ' votes' 'title': parseInt(m.info.rating.imdb[1]) + ' votes'
}) : null, }) : null,
@@ -0,0 +1,6 @@
from .main import ShowBase
def start():
return ShowBase()
config = []
+239
View File
@@ -0,0 +1,239 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.media import MediaBase
from couchpotato.core.settings.model import Media
import time
log = CPLog(__name__)
class ShowBase(MediaBase):
_type = 'show'
def __init__(self):
super(ShowBase, self).__init__()
addApiView('show.add', self.addView, docs = {
'desc': 'Add new movie to the wanted list',
'params': {
'identifier': {'desc': 'IMDB id of the movie your want to add.'},
'profile_id': {'desc': 'ID of quality profile you want the add the movie in. If empty will use the default profile.'},
'title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'},
}
})
addEvent('show.add', self.add)
def addView(self, **kwargs):
add_dict = self.add(params = kwargs)
return {
'success': True if add_dict else False,
'show': add_dict,
}
def add(self, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None):
"""
params
{'category_id': u'-1',
'identifier': u'tt1519931',
'profile_id': u'12',
'thetvdb_id': u'158661',
'title': u'Haven'}
"""
log.debug("show.add")
# Add show parent to db first; need to update library so maps will be in place (if any)
parent = self.addToDatabase(params = params, update_library = True, type = 'show')
# TODO: add by airdate
# Add by Season/Episode numbers
self.addBySeasonEpisode(parent,
params = params,
force_readd = force_readd,
search_after = search_after,
update_library = update_library,
status_id = status_id
)
def addBySeasonEpisode(self, parent, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None):
identifier = params.get('id')
# 'tvdb' will always be the master for our purpose. All mapped data can be mapped
# to another source for downloading, but it will always be remapped back to tvdb numbering
# when renamed so media can be used in media players that use tvdb for info provider
#
# This currently means the episode must actually exist in tvdb in order to be found but
# the numbering can be different
#master = 'tvdb'
#destination = 'scene'
#destination = 'anidb'
#destination = 'rage'
#destination = 'trakt'
# TODO: auto mode. if anime exists use it. if scene exists use it else use tvdb
# XXX: We should abort adding show, etc if either tvdb or xem is down or we will have incorrent mappings
# I think if tvdb gets error we wont have anydata anyway, but we must make sure XEM returns!!!!
# Only the master should return results here; all other info providers should just return False
# since we are just interested in the structure at this point.
seasons = fireEvent('season.info', merge = True, identifier = identifier)
if seasons is not None:
for season in seasons:
# Make sure we are only dealing with 'tvdb' responses at this point
if season.get('primary_provider', None) != 'thetvdb':
continue
season_id = season.get('id', None)
if season_id is None: continue
season_params = {'season_identifier': season_id}
# Calling all info providers; merge your info now for individual season
single_season = fireEvent('season.info', merge = True, identifier = identifier, params = season_params)
single_season['category_id'] = params.get('category_id')
single_season['profile_id'] = params.get('profile_id')
single_season['title'] = single_season.get('original_title', None)
single_season['identifier'] = season_id
single_season['parent_identifier'] = identifier
log.info("Adding Season %s" % season_id)
s = self.addToDatabase(params = single_season, type = "season")
episode_params = {'season_identifier': season_id}
episodes = fireEvent('episode.info', merge = True, identifier = identifier, params = episode_params)
if episodes is not None:
for episode in episodes:
# Make sure we are only dealing with 'tvdb' responses at this point
if episode.get('primary_provider', None) != 'thetvdb':
continue
episode_id = episode.get('id', None)
if episode_id is None: continue
try:
episode_number = int(episode.get('episodenumber', None))
except (ValueError, TypeError):
continue
try:
absolute_number = int(episode.get('absolute_number', None))
except (ValueError, TypeError):
absolute_number = None
episode_params = {'season_identifier': season_id,
'episode_identifier': episode_id,
'episode': episode_number}
if absolute_number:
episode_params['absolute'] = absolute_number
# Calling all info providers; merge your info now for individual episode
single_episode = fireEvent('episode.info', merge = True, identifier = identifier, params = episode_params)
single_episode['category_id'] = params.get('category_id')
single_episode['profile_id'] = params.get('profile_id')
single_episode['title'] = single_episode.get('original_title', None)
single_episode['identifier'] = episode_id
single_episode['parent_identifier'] = single_season['identifier']
log.info("Adding [%sx%s] %s - %s" % (season_id,
episode_number,
params['title'],
single_episode.get('original_title', '')))
e = self.addToDatabase(params = single_episode, type = "episode")
# Start searching now that all the media has been added
if search_after:
onComplete = self.createOnComplete(parent['id'])
onComplete()
return parent
def addToDatabase(self, params = {}, type = "show", force_readd = True, search_after = False, update_library = False, status_id = None):
log.debug("show.addToDatabase")
if not params.get('identifier'):
msg = 'Can\'t add show without imdb identifier.'
log.error(msg)
fireEvent('notify.frontend', type = 'show.is_tvshow', message = msg)
return False
#else:
#try:
#is_show = fireEvent('movie.is_show', identifier = params.get('identifier'), single = True)
#if not is_show:
#msg = 'Can\'t add show, seems to be a TV show.'
#log.error(msg)
#fireEvent('notify.frontend', type = 'show.is_tvshow', message = msg)
#return False
#except:
#pass
library = fireEvent('library.add.%s' % type, single = True, attrs = params, update_after = update_library)
if not library:
return False
# Status
status_active, snatched_status, ignored_status, done_status, downloaded_status = \
fireEvent('status.get', ['active', 'snatched', 'ignored', 'done', 'downloaded'], single = True)
default_profile = fireEvent('profile.default', single = True)
cat_id = params.get('category_id', None)
db = get_session()
m = db.query(Media).filter_by(library_id = library.get('id')).first()
added = True
do_search = False
if not m:
m = Media(
type = type,
library_id = library.get('id'),
profile_id = params.get('profile_id', default_profile.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.commit()
onComplete = None
if search_after:
onComplete = self.createOnComplete(m.id)
fireEventAsync('library.update.%s' % type, params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete)
search_after = False
elif force_readd:
# Clean snatched history
for release in m.releases:
if release.status_id in [downloaded_status.get('id'), snatched_status.get('id'), done_status.get('id')]:
if params.get('ignore_previous', False):
release.status_id = ignored_status.get('id')
else:
fireEvent('release.delete', release.id, single = True)
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 None
else:
log.debug('Show already exists, not updating: %s', params)
added = False
if force_readd:
m.status_id = status_id if status_id else status_active.get('id')
m.last_edit = int(time.time())
do_search = True
db.commit()
# Remove releases
available_status = fireEvent('status.get', 'available', single = True)
for rel in m.releases:
if rel.status_id is available_status.get('id'):
db.delete(rel)
db.commit()
show_dict = m.to_dict(self.default_dict)
if do_search and search_after:
onComplete = self.createOnComplete(m.id)
onComplete()
if added:
fireEvent('notify.frontend', type = 'show.added', data = show_dict, message = 'Successfully added "%s" to your wanted list.' % params.get('title', ''))
db.expire_all()
return show_dict
@@ -0,0 +1,232 @@
Block.Search.ShowItem = new Class({
Implements: [Options, Events],
initialize: function(info, options){
var self = this;
self.setOptions(options);
self.info = info;
self.alternative_titles = [];
self.create();
},
create: function(){
var self = this,
info = self.info;
self.el = new Element('div.media_result', {
'id': info.id
}).adopt(
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
'src': info.images.poster[0],
'height': null,
'width': null
}) : null,
self.options_el = new Element('div.options.inlay'),
self.data_container = new Element('div.data', {
'events': {
'click': self.showOptions.bind(self)
}
}).adopt(
self.info_container = new Element('div.info').adopt(
new Element('h2').adopt(
self.title = new Element('span.title', {
'text': info.titles && info.titles.length > 0 ? info.titles[0] : 'Unknown'
}),
self.year = info.year ? new Element('span.year', {
'text': info.year
}) : null
)
)
)
)
if(info.titles)
info.titles.each(function(title){
self.alternativeTitle({
'title': title
});
})
},
alternativeTitle: function(alternative){
var self = this;
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(){
var self = this;
self.createOptions();
self.data_container.addClass('open');
self.el.addEvent('outerClick', self.closeOptions.bind(self))
},
closeOptions: function(){
var self = this;
self.data_container.removeClass('open');
self.el.removeEvents('outerClick')
},
add: function(e){
var self = this;
if(e)
(e).preventDefault();
self.loadingMask();
Api.request('show.add', {
'data': {
'identifier': self.info.id,
'id': self.info.id,
'type': self.info.type,
'primary_provider': self.info.primary_provider,
'title': self.title_select.get('value'),
'profile_id': self.profile_select.get('value'),
'category_id': self.category_select.get('value')
},
'onComplete': function(json){
self.options_el.empty();
self.options_el.adopt(
new Element('div.message', {
'text': json.added ? 'Show successfully added.' : 'Show didn\'t add properly. Check logs'
})
);
self.mask.fade('out');
self.fireEvent('added');
},
'onFailure': function(){
self.options_el.empty();
self.options_el.adopt(
new Element('div.message', {
'text': 'Something went wrong, check the logs for more info.'
})
);
self.mask.fade('out');
}
});
},
createOptions: function(){
var self = this,
info = self.info;
if(!self.options_el.hasClass('set')){
if(self.info.in_library){
var in_library = [];
self.info.in_library.releases.each(function(release){
in_library.include(release.quality.label)
});
}
self.options_el.grab(
new Element('div', {
'class': self.info.in_wanted && self.info.in_wanted.profile_id || in_library ? 'in_library_wanted' : ''
}).adopt(
self.info.in_wanted && self.info.in_wanted.profile_id ? new Element('span.in_wanted', {
'text': 'Already in wanted list: ' + Quality.getProfile(self.info.in_wanted.profile_id).get('label')
}) : (in_library ? new Element('span.in_library', {
'text': 'Already in library: ' + in_library.join(', ')
}) : null),
self.title_select = new Element('select', {
'name': 'title'
}),
self.profile_select = new Element('select', {
'name': 'profile'
}),
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',
'events': {
'click': self.add.bind(self)
}
})
)
);
Array.each(self.alternative_titles, function(alt){
new Element('option', {
'text': alt.title
}).inject(self.title_select)
})
// 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', {
'value': profile.id ? profile.id : profile.data.id,
'text': profile.label ? profile.label : profile.data.label
}).inject(self.profile_select)
});
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();
}
},
loadingMask: function(){
var self = this;
self.mask = new Element('div.mask').inject(self.el).fade('hide')
createSpinner(self.mask)
self.mask.fade('in')
},
toElement: function(){
return this.el
}
});
@@ -0,0 +1,6 @@
from .main import EpisodeLibraryPlugin
def start():
return EpisodeLibraryPlugin()
config = []
@@ -0,0 +1,266 @@
from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.logger import CPLog
from couchpotato.core.settings.model import EpisodeLibrary, SeasonLibrary, LibraryTitle, File
from couchpotato.core.media._base.library.base import LibraryBase
from couchpotato.core.helpers.variable import tryInt
from string import ascii_letters
import time
import traceback
log = CPLog(__name__)
class EpisodeLibraryPlugin(LibraryBase):
default_dict = {'titles': {}, 'files':{}}
def __init__(self):
addEvent('library.query', self.query)
addEvent('library.identifier', self.identifier)
addEvent('library.add.episode', self.add)
addEvent('library.update.episode', self.update)
addEvent('library.update.episode_release_date', self.updateReleaseDate)
def query(self, library, first = True, condense = True, include_identifier = True, **kwargs):
if library is list or library.get('type') != 'episode':
return
# Get the titles of the season
if not library.get('related_libraries', {}).get('season', []):
log.warning('Invalid library, unable to determine title.')
return
titles = fireEvent(
'library.query',
library['related_libraries']['season'][0],
first=False,
include_identifier=include_identifier,
condense=condense,
single=True
)
identifier = fireEvent('library.identifier', library, single = True)
# Add episode identifier to titles
if include_identifier and identifier.get('episode'):
titles = [title + ('E%02d' % identifier['episode']) for title in titles]
if first:
return titles[0] if titles else None
return titles
def identifier(self, library):
if library.get('type') != 'episode':
return
identifier = {
'season': None,
'episode': None
}
scene_map = library['info'].get('map_episode', {}).get('scene')
if scene_map:
# Use scene mappings if they are available
identifier['season'] = scene_map.get('season')
identifier['episode'] = scene_map.get('episode')
else:
# Fallback to normal season/episode numbers
identifier['season'] = library.get('season_number')
identifier['episode'] = library.get('episode_number')
# Cast identifiers to integers
# TODO this will need changing to support identifiers with trailing 'a', 'b' characters
identifier['season'] = tryInt(identifier['season'], None)
identifier['episode'] = tryInt(identifier['episode'], None)
return identifier
def add(self, attrs = {}, update_after = True):
type = attrs.get('type', 'episode')
primary_provider = attrs.get('primary_provider', 'thetvdb')
db = get_session()
parent_identifier = attrs.get('parent_identifier', None)
parent = None
if parent_identifier:
parent = db.query(SeasonLibrary).filter_by(primary_provider = primary_provider, identifier = attrs.get('parent_identifier')).first()
l = db.query(EpisodeLibrary).filter_by(type = type, identifier = attrs.get('identifier')).first()
if not l:
status = fireEvent('status.get', 'needs_update', single = True)
l = EpisodeLibrary(
type = type,
primary_provider = primary_provider,
year = attrs.get('year'),
identifier = attrs.get('identifier'),
plot = toUnicode(attrs.get('plot')),
tagline = toUnicode(attrs.get('tagline')),
status_id = status.get('id'),
info = {},
parent = parent,
season_number = tryInt(attrs.get('seasonnumber', None)),
episode_number = tryInt(attrs.get('episodenumber', None)),
absolute_number = tryInt(attrs.get('absolute_number', None))
)
title = LibraryTitle(
title = toUnicode(attrs.get('title')),
simple_title = self.simplifyTitle(attrs.get('title')),
)
l.titles.append(title)
db.add(l)
db.commit()
# Update library info
if update_after is not False:
handle = fireEventAsync if update_after is 'async' else fireEvent
handle('library.update.episode', identifier = l.identifier, default_title = toUnicode(attrs.get('title', '')))
library_dict = l.to_dict(self.default_dict)
db.expire_all()
return library_dict
def update(self, identifier, default_title = '', force = False):
if self.shuttingDown():
return
db = get_session()
library = db.query(EpisodeLibrary).filter_by(identifier = identifier).first()
done_status = fireEvent('status.get', 'done', single = True)
if library:
library_dict = library.to_dict(self.default_dict)
do_update = True
parent_identifier = None
if library.parent is not None:
parent_identifier = library.parent.identifier
if library.status_id == done_status.get('id') and not force:
do_update = False
episode_params = {'season_identifier': parent_identifier,
'episode_identifier': identifier,
'episode': library.episode_number,
'absolute': library.absolute_number,}
info = fireEvent('episode.info', merge = True, params = episode_params)
# Don't need those here
try: del info['in_wanted']
except: pass
try: del info['in_library']
except: pass
if not info or len(info) == 0:
log.error('Could not update, no movie info to work with: %s', identifier)
return False
# Main info
if do_update:
library.plot = toUnicode(info.get('plot', ''))
library.tagline = toUnicode(info.get('tagline', ''))
library.year = info.get('year', 0)
library.status_id = done_status.get('id')
library.season_number = tryInt(info.get('seasonnumber', None))
library.episode_number = tryInt(info.get('episodenumber', None))
library.absolute_number = tryInt(info.get('absolute_number', None))
try:
library.last_updated = int(info.get('lastupdated'))
except:
library.last_updated = int(time.time())
library.info.update(info)
db.commit()
# Titles
[db.delete(title) for title in library.titles]
db.commit()
titles = info.get('titles', [])
log.debug('Adding titles: %s', titles)
counter = 0
for title in titles:
if not title:
continue
title = toUnicode(title)
t = LibraryTitle(
title = title,
simple_title = self.simplifyTitle(title),
default = (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == u'' and toUnicode(titles[0]) == title)
)
library.titles.append(t)
counter += 1
db.commit()
# Files
images = info.get('images', [])
for image_type in ['poster']:
for image in images.get(image_type, []):
if not isinstance(image, (str, unicode)):
continue
file_path = fireEvent('file.download', url = image, single = True)
if file_path:
file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', image_type), single = True)
try:
file_obj = db.query(File).filter_by(id = file_obj.get('id')).one()
library.files.append(file_obj)
db.commit()
break
except:
log.debug('Failed to attach to library: %s', traceback.format_exc())
library_dict = library.to_dict(self.default_dict)
db.expire_all()
return library_dict
def updateReleaseDate(self, identifier):
'''XXX: Not sure what this is for yet in relation to an episode'''
pass
#db = get_session()
#library = db.query(EpisodeLibrary).filter_by(identifier = identifier).first()
#if not library.info:
#library_dict = self.update(identifier, force = True)
#dates = library_dict.get('info', {}).get('release_date')
#else:
#dates = library.info.get('release_date')
#if dates and dates.get('expires', 0) < time.time() or not dates:
#dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
#library.info.update({'release_date': dates })
#db.commit()
#db.expire_all()
#return dates
#TODO: Add to base class
def simplifyTitle(self, title):
title = toUnicode(title)
nr_prefix = '' if title[0] in ascii_letters else '#'
title = simplifyString(title)
for prefix in ['the ']:
if prefix == title[:len(prefix)]:
title = title[len(prefix):]
break
return nr_prefix + title
@@ -0,0 +1,6 @@
from .main import SeasonLibraryPlugin
def start():
return SeasonLibraryPlugin()
config = []
@@ -0,0 +1,242 @@
from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.logger import CPLog
from couchpotato.core.settings.model import SeasonLibrary, ShowLibrary, LibraryTitle, File
from couchpotato.core.media._base.library.base import LibraryBase
from couchpotato.core.helpers.variable import tryInt
from string import ascii_letters
import time
import traceback
log = CPLog(__name__)
class SeasonLibraryPlugin(LibraryBase):
default_dict = {'titles': {}, 'files':{}}
def __init__(self):
addEvent('library.query', self.query)
addEvent('library.identifier', self.identifier)
addEvent('library.add.season', self.add)
addEvent('library.update.season', self.update)
addEvent('library.update.season_release_date', self.updateReleaseDate)
def query(self, library, first = True, condense = True, include_identifier = True, **kwargs):
if library is list or library.get('type') != 'season':
return
# Get the titles of the show
if not library.get('related_libraries', {}).get('show', []):
log.warning('Invalid library, unable to determine title.')
return
titles = fireEvent(
'library.query',
library['related_libraries']['show'][0],
first=False,
condense=condense,
single=True
)
# Add season map_names if they exist
if 'map_names' in library['info']:
season_names = library['info']['map_names'].get(str(library['season_number']), {})
# Add titles from all locations
# TODO only add name maps from a specific location
for location, names in season_names.items():
titles += [name for name in names if name and name not in titles]
identifier = fireEvent('library.identifier', library, single = True)
# Add season identifier to titles
if include_identifier and identifier.get('season') is not None:
titles = [title + (' S%02d' % identifier['season']) for title in titles]
if first:
return titles[0] if titles else None
return titles
def identifier(self, library):
if library.get('type') != 'season':
return
return {
'season': tryInt(library['season_number'], None)
}
def add(self, attrs = {}, update_after = True):
type = attrs.get('type', 'season')
primary_provider = attrs.get('primary_provider', 'thetvdb')
db = get_session()
parent_identifier = attrs.get('parent_identifier', None)
parent = None
if parent_identifier:
parent = db.query(ShowLibrary).filter_by(primary_provider = primary_provider, identifier = attrs.get('parent_identifier')).first()
l = db.query(SeasonLibrary).filter_by(type = type, identifier = attrs.get('identifier')).first()
if not l:
status = fireEvent('status.get', 'needs_update', single = True)
l = SeasonLibrary(
type = type,
primary_provider = primary_provider,
year = attrs.get('year'),
identifier = attrs.get('identifier'),
plot = toUnicode(attrs.get('plot')),
tagline = toUnicode(attrs.get('tagline')),
status_id = status.get('id'),
info = {},
parent = parent,
)
title = LibraryTitle(
title = toUnicode(attrs.get('title')),
simple_title = self.simplifyTitle(attrs.get('title')),
)
l.titles.append(title)
db.add(l)
db.commit()
# Update library info
if update_after is not False:
handle = fireEventAsync if update_after is 'async' else fireEvent
handle('library.update.season', identifier = l.identifier, default_title = toUnicode(attrs.get('title', '')))
library_dict = l.to_dict(self.default_dict)
db.expire_all()
return library_dict
def update(self, identifier, default_title = '', force = False):
if self.shuttingDown():
return
db = get_session()
library = db.query(SeasonLibrary).filter_by(identifier = identifier).first()
done_status = fireEvent('status.get', 'done', single = True)
if library:
library_dict = library.to_dict(self.default_dict)
do_update = True
parent_identifier = None
if library.parent is not None:
parent_identifier = library.parent.identifier
if library.status_id == done_status.get('id') and not force:
do_update = False
season_params = {'season_identifier': identifier}
info = fireEvent('season.info', merge = True, identifier = parent_identifier, params = season_params)
# Don't need those here
try: del info['in_wanted']
except: pass
try: del info['in_library']
except: pass
if not info or len(info) == 0:
log.error('Could not update, no movie info to work with: %s', identifier)
return False
# Main info
if do_update:
library.plot = toUnicode(info.get('plot', ''))
library.tagline = toUnicode(info.get('tagline', ''))
library.year = info.get('year', 0)
library.status_id = done_status.get('id')
library.season_number = tryInt(info.get('seasonnumber', None))
library.info.update(info)
db.commit()
# Titles
[db.delete(title) for title in library.titles]
db.commit()
titles = info.get('titles', [])
log.debug('Adding titles: %s', titles)
counter = 0
for title in titles:
if not title:
continue
title = toUnicode(title)
t = LibraryTitle(
title = title,
simple_title = self.simplifyTitle(title),
# XXX: default was None; so added a quick hack since we don't really need titiles for seasons anyway
#default = (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == u'' and toUnicode(titles[0]) == title)
default = True,
)
library.titles.append(t)
counter += 1
db.commit()
# Files
images = info.get('images', [])
for image_type in ['poster']:
for image in images.get(image_type, []):
if not isinstance(image, (str, unicode)):
continue
file_path = fireEvent('file.download', url = image, single = True)
if file_path:
file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', image_type), single = True)
try:
file_obj = db.query(File).filter_by(id = file_obj.get('id')).one()
library.files.append(file_obj)
db.commit()
break
except:
log.debug('Failed to attach to library: %s', traceback.format_exc())
library_dict = library.to_dict(self.default_dict)
db.expire_all()
return library_dict
def updateReleaseDate(self, identifier):
'''XXX: Not sure what this is for yet in relation to a tvshow'''
pass
#db = get_session()
#library = db.query(SeasonLibrary).filter_by(identifier = identifier).first()
#if not library.info:
#library_dict = self.update(identifier, force = True)
#dates = library_dict.get('info', {}).get('release_date')
#else:
#dates = library.info.get('release_date')
#if dates and dates.get('expires', 0) < time.time() or not dates:
#dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
#library.info.update({'release_date': dates })
#db.commit()
#db.expire_all()
#return dates
#TODO: Add to base class
def simplifyTitle(self, title):
title = toUnicode(title)
nr_prefix = '' if title[0] in ascii_letters else '#'
title = simplifyString(title)
for prefix in ['the ']:
if prefix == title[:len(prefix)]:
title = title[len(prefix):]
break
return nr_prefix + title
@@ -0,0 +1,6 @@
from .main import ShowLibraryPlugin
def start():
return ShowLibraryPlugin()
config = []
@@ -0,0 +1,229 @@
from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.logger import CPLog
from couchpotato.core.settings.model import ShowLibrary, LibraryTitle, File
from couchpotato.core.media._base.library.base import LibraryBase
from qcond.helpers import simplify
from qcond import QueryCondenser
from string import ascii_letters
import time
import traceback
log = CPLog(__name__)
class ShowLibraryPlugin(LibraryBase):
default_dict = {'titles': {}, 'files':{}}
def __init__(self):
self.query_condenser = QueryCondenser()
addEvent('library.query', self.query)
addEvent('library.add.show', self.add)
addEvent('library.update.show', self.update)
addEvent('library.update.show_release_date', self.updateReleaseDate)
def query(self, library, first = True, condense = True, **kwargs):
if library is list or library.get('type') != 'show':
return
titles = [title['title'] for title in library['titles']]
if condense:
# Use QueryCondenser to build a list of optimal search titles
condensed_titles = self.query_condenser.distinct(titles)
if condensed_titles:
# Use condensed titles if we got a valid result
titles = condensed_titles
else:
# Fallback to simplifying titles
titles = [simplify(title) for title in titles]
if first:
return titles[0] if titles else None
return titles
def add(self, attrs = {}, update_after = True):
type = attrs.get('type', 'show')
primary_provider = attrs.get('primary_provider', 'thetvdb')
db = get_session()
l = db.query(ShowLibrary).filter_by(type = type, identifier = attrs.get('identifier')).first()
if not l:
status = fireEvent('status.get', 'needs_update', single = True)
l = ShowLibrary(
type = type,
primary_provider = primary_provider,
year = attrs.get('year'),
identifier = attrs.get('identifier'),
plot = toUnicode(attrs.get('plot')),
tagline = toUnicode(attrs.get('tagline')),
status_id = status.get('id'),
info = {},
parent = None,
)
title = LibraryTitle(
title = toUnicode(attrs.get('title')),
simple_title = self.simplifyTitle(attrs.get('title')),
)
l.titles.append(title)
db.add(l)
db.commit()
# Update library info
if update_after is not False:
handle = fireEventAsync if update_after is 'async' else fireEvent
handle('library.update.show', identifier = l.identifier, default_title = toUnicode(attrs.get('title', '')))
library_dict = l.to_dict(self.default_dict)
db.expire_all()
return library_dict
def update(self, identifier, default_title = '', force = False):
if self.shuttingDown():
return
db = get_session()
library = db.query(ShowLibrary).filter_by(identifier = identifier).first()
done_status = fireEvent('status.get', 'done', single = True)
if library:
library_dict = library.to_dict(self.default_dict)
do_update = True
info = fireEvent('show.info', merge = True, identifier = identifier)
# Don't need those here
try: del info['in_wanted']
except: pass
try: del info['in_library']
except: pass
if not info or len(info) == 0:
log.error('Could not update, no show info to work with: %s', identifier)
return False
# Main info
if do_update:
library.plot = toUnicode(info.get('plot', ''))
library.tagline = toUnicode(info.get('tagline', ''))
library.year = info.get('year', 0)
library.status_id = done_status.get('id')
library.show_status = toUnicode(info.get('status', '').lower())
library.airs_time = info.get('airs_time', None)
# Bits
days_of_week_map = {
u'Monday': 1,
u'Tuesday': 2,
u'Wednesday': 4,
u'Thursday': 8,
u'Friday': 16,
u'Saturday': 32,
u'Sunday': 64,
u'Daily': 127,
}
try:
library.airs_dayofweek = days_of_week_map.get(info.get('airs_dayofweek'))
except:
library.airs_dayofweek = 0
try:
library.last_updated = int(info.get('lastupdated'))
except:
library.last_updated = int(time.time())
library.info.update(info)
db.commit()
# Titles
[db.delete(title) for title in library.titles]
db.commit()
titles = info.get('titles', [])
log.debug('Adding titles: %s', titles)
counter = 0
for title in titles:
if not title:
continue
title = toUnicode(title)
t = LibraryTitle(
title = title,
simple_title = self.simplifyTitle(title),
default = (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == u'' and toUnicode(titles[0]) == title)
)
library.titles.append(t)
counter += 1
db.commit()
# Files
images = info.get('images', [])
for image_type in ['poster']:
for image in images.get(image_type, []):
if not isinstance(image, (str, unicode)):
continue
file_path = fireEvent('file.download', url = image, single = True)
if file_path:
file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', image_type), single = True)
try:
file_obj = db.query(File).filter_by(id = file_obj.get('id')).one()
library.files.append(file_obj)
db.commit()
break
except:
log.debug('Failed to attach to library: %s', traceback.format_exc())
library_dict = library.to_dict(self.default_dict)
db.expire_all()
return library_dict
def updateReleaseDate(self, identifier):
'''XXX: Not sure what this is for yet in relation to a show'''
pass
#db = get_session()
#library = db.query(ShowLibrary).filter_by(identifier = identifier).first()
#if not library.info:
#library_dict = self.update(identifier, force = True)
#dates = library_dict.get('info', {}).get('release_date')
#else:
#dates = library.info.get('release_date')
#if dates and dates.get('expires', 0) < time.time() or not dates:
#dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
#library.info.update({'release_date': dates })
#db.commit()
#db.expire_all()
#return dates
#TODO: Add to base class
def simplifyTitle(self, title):
title = toUnicode(title)
nr_prefix = '' if title[0] in ascii_letters else '#'
title = simplifyString(title)
for prefix in ['the ']:
if prefix == title[:len(prefix)]:
title = title[len(prefix):]
break
return nr_prefix + title
@@ -0,0 +1,6 @@
from .main import ShowMatcher
def start():
return ShowMatcher()
config = []
+127
View File
@@ -0,0 +1,127 @@
from couchpotato import CPLog
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import dictIsSubset, tryInt, toIterable
from couchpotato.core.media._base.matcher.base import MatcherBase
from couchpotato.core.providers.base import MultiProvider
log = CPLog(__name__)
class ShowMatcher(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Base(MatcherBase):
# TODO come back to this later, think this could be handled better, this is starting to get out of hand....
quality_map = {
'bluray_1080p': {'resolution': ['1080p'], 'source': ['bluray']},
'bluray_720p': {'resolution': ['720p'], 'source': ['bluray']},
'bdrip_1080p': {'resolution': ['1080p'], 'source': ['BDRip']},
'bdrip_720p': {'resolution': ['720p'], 'source': ['BDRip']},
'brrip_1080p': {'resolution': ['1080p'], 'source': ['BRRip']},
'brrip_720p': {'resolution': ['720p'], 'source': ['BRRip']},
'webdl_1080p': {'resolution': ['1080p'], 'source': ['webdl', ['web', 'dl']]},
'webdl_720p': {'resolution': ['720p'], 'source': ['webdl', ['web', 'dl']]},
'webdl_480p': {'resolution': ['480p'], 'source': ['webdl', ['web', 'dl']]},
'hdtv_720p': {'resolution': ['720p'], 'source': ['hdtv']},
'hdtv_sd': {'resolution': ['480p', None], 'source': ['hdtv']},
}
def __init__(self):
super(Base, self).__init__()
addEvent('%s.matcher.correct_identifier' % self.type, self.correctIdentifier)
def correct(self, chain, release, media, quality):
log.info("Checking if '%s' is valid", release['name'])
log.info2('Release parsed as: %s', chain.info)
if not fireEvent('matcher.correct_quality', chain, quality, self.quality_map, single = True):
log.info('Wrong: %s, quality does not match', release['name'])
return False
if not fireEvent('%s.matcher.correct_identifier' % self.type, chain, media):
log.info('Wrong: %s, identifier does not match', release['name'])
return False
if not fireEvent('matcher.correct_title', chain, media):
log.info("Wrong: '%s', undetermined naming.", (' '.join(chain.info['show_name'])))
return False
return True
def correctIdentifier(self, chain, media):
raise NotImplementedError()
def getChainIdentifier(self, chain):
if 'identifier' not in chain.info:
return None
identifier = self.flattenInfo(chain.info['identifier'])
# Try cast values to integers
for key, value in identifier.items():
if isinstance(value, list):
if len(value) <= 1:
value = value[0]
else:
log.warning('Wrong: identifier contains multiple season or episode values, unsupported')
return None
identifier[key] = tryInt(value, value)
return identifier
class Episode(Base):
type = 'episode'
def correctIdentifier(self, chain, media):
identifier = self.getChainIdentifier(chain)
if not identifier:
log.info2('Wrong: release identifier is not valid (unsupported or missing identifier)')
return False
# TODO - Parse episode ranges from identifier to determine if they are multi-part episodes
if any([x in identifier for x in ['episode_from', 'episode_to']]):
log.info2('Wrong: releases with identifier ranges are not supported yet')
return False
required = fireEvent('library.identifier', media['library'], single = True)
# TODO - Support air by date episodes
# TODO - Support episode parts
if identifier != required:
log.info2('Wrong: required identifier (%s) does not match release identifier (%s)', (required, identifier))
return False
return True
class Season(Base):
type = 'season'
def correctIdentifier(self, chain, media):
identifier = self.getChainIdentifier(chain)
if not identifier:
log.info2('Wrong: release identifier is not valid (unsupported or missing identifier)')
return False
# TODO - Parse episode ranges from identifier to determine if they are season packs
if any([x in identifier for x in ['episode_from', 'episode_to']]):
log.info2('Wrong: releases with identifier ranges are not supported yet')
return False
required = fireEvent('library.identifier', media['library'], single = True)
if identifier != required:
log.info2('Wrong: required identifier (%s) does not match release identifier (%s)', (required, identifier))
return False
return True
@@ -0,0 +1,7 @@
from .main import ShowSearcher
import random
def start():
return ShowSearcher()
config = []
@@ -0,0 +1,189 @@
from couchpotato import Env, get_session
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import getTitle, toIterable
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.searcher.main import SearchSetupError
from couchpotato.core.media.show._base import ShowBase
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Media
from qcond import QueryCondenser
from qcond.helpers import simplify
log = CPLog(__name__)
class ShowSearcher(Plugin):
type = ['show', 'season', 'episode']
in_progress = False
def __init__(self):
super(ShowSearcher, self).__init__()
self.query_condenser = QueryCondenser()
for type in toIterable(self.type):
addEvent('%s.searcher.single' % type, self.single)
addEvent('searcher.correct_release', self.correctRelease)
def single(self, media, search_protocols = None, manual = False):
show, season, episode = self.getLibraries(media['library'])
db = get_session()
if media['type'] == 'show':
for library in season:
# TODO ideally we shouldn't need to fetch the media for each season library here
m = db.query(Media).filter_by(library_id = library['library_id']).first()
fireEvent('season.searcher.single', m.to_dict(ShowBase.search_dict))
return
# Find out search type
try:
if not search_protocols:
search_protocols = fireEvent('searcher.protocols', single = True)
except SearchSetupError:
return
done_status, available_status, ignored_status, failed_status = fireEvent('status.get', ['done', 'available', 'ignored', 'failed'], single = True)
if not media['profile'] or media['status_id'] == done_status.get('id'):
log.debug('Episode doesn\'t have a profile or already done, assuming in manage tab.')
return
#pre_releases = fireEvent('quality.pre_releases', single = True)
found_releases = []
too_early_to_search = []
default_title = fireEvent('library.query', media['library'], condense = False, single=True)
if not default_title:
log.error('No proper info found for episode, removing it from library to cause it from having more issues.')
#fireEvent('episode.delete', episode['id'], single = True)
return
if not show or not season:
log.error('Unable to find show or season library in database, missing required data for searching')
return
fireEvent('notify.frontend', type = 'show.searcher.started.%s' % media['id'], data = True, message = 'Searching for "%s"' % default_title)
ret = False
has_better_quality = None
for quality_type in media['profile']['types']:
# TODO check air date?
#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 media['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 S%02d%s in %s', (
getTitle(show),
season['season_number'],
"E%02d" % episode['episode_number'] if episode and len(episode) == 1 else "",
quality_type['quality']['label'])
)
quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True)
results = fireEvent('searcher.search', search_protocols, media, quality, single = True)
if len(results) == 0:
log.debug('Nothing found for %s in %s', (default_title, quality_type['quality']['label']))
# Check if movie isn't deleted while searching
if not db.query(Media).filter_by(id = media.get('id')).first():
break
# Add them to this movie releases list
found_releases += fireEvent('release.create_from_search', results, media, quality_type, single = True)
# Try find a valid result and download it
if fireEvent('release.try_download_result', results, media, quality_type, manual, single = True):
ret = True
# Remove releases that aren't found anymore
for release in media.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('media.restatus', media['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))
elif media['type'] == 'season' and not ret and has_better_quality is 0:
# If nothing was found, start searching for episodes individually
log.info('No season pack found, starting individual episode search')
for library in episode:
# TODO ideally we shouldn't need to fetch the media for each episode library here
m = db.query(Media).filter_by(library_id = library['library_id']).first()
fireEvent('episode.searcher.single', m.to_dict(ShowBase.search_dict))
fireEvent('notify.frontend', type = 'show.searcher.ended.%s' % media['id'], data = True)
return ret
def correctRelease(self, release = None, media = None, quality = None, **kwargs):
if media.get('type') not in ['season', 'episode']: return
retention = Env.setting('retention', section = 'nzb')
if release.get('seeders') is None and 0 < retention < release.get('age', 0):
log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (release['age'], retention, release['name']))
return False
# Check for required and ignored words
if not fireEvent('searcher.correct_words', release['name'], media, single = True):
return False
# TODO Matching is quite costly, maybe we should be caching release matches somehow? (also look at caper optimizations)
match = fireEvent('matcher.match', release, media, quality, single = True)
if match:
return match.weight
return False
def getLibraries(self, library):
if 'related_libraries' not in library:
log.warning("'related_libraries' missing from media library, unable to continue searching")
return None, None, None
libraries = library['related_libraries']
# Show always collapses as there can never be any multiples
show = libraries.get('show', [])
show = show[0] if len(show) else None
# Season collapses if the subject is a season or episode
season = libraries.get('season', [])
if library['type'] in ['season', 'episode']:
season = season[0] if len(season) else None
# Episode collapses if the subject is a episode
episode = libraries.get('episode', [])
if library['type'] == 'episode':
episode = episode[0] if len(episode) else None
return show, season, episode
@@ -13,6 +13,5 @@ def upgrade(migrate_engine):
create_column(category_column, movie) create_column(category_column, movie)
Index('ix_movie_category_id', movie.c.category_id).create() Index('ix_movie_category_id', movie.c.category_id).create()
def downgrade(migrate_engine): def downgrade(migrate_engine):
pass pass
@@ -1,6 +1,5 @@
from .main import Boxcar from .main import Boxcar
def start(): def start():
return Boxcar() return Boxcar()
@@ -1,34 +0,0 @@
from .main import Boxcar2
def start():
return Boxcar2()
config = [{
'name': 'boxcar2',
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'boxcar2',
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
},
{
'name': 'token',
'description': ('Your Boxcar access token.', 'Can be found in the app under settings')
},
{
'name': 'on_snatch',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Also send message when movie is snatched.',
},
],
}
],
}]
@@ -1,39 +0,0 @@
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
log = CPLog(__name__)
class Boxcar2(Notification):
url = 'https://new.boxcar.io/api/notifications'
def notify(self, message = '', data = None, listener = None):
if not data: data = {}
try:
message = message.strip()
long_message = ''
if listener == 'test':
long_message = 'This is a test message'
elif data.get('identifier'):
long_message = 'More movie info <a href="http://www.imdb.com/title/%s/">on IMDB</a>' % data['identifier']
data = {
'user_credentials': self.conf('token'),
'notification[title]': toUnicode(message),
'notification[long_message]': toUnicode(long_message),
}
self.urlopen(self.url, data = data)
except:
log.error('Make sure the token provided is for the correct device')
return False
log.info('Boxcar notification successful.')
return True
def isEnabled(self):
return super(Boxcar2, self).isEnabled() and self.conf('token')
@@ -1,6 +1,5 @@
from .main import CoreNotifier from .main import CoreNotifier
def start(): def start():
return CoreNotifier() return CoreNotifier()
+2 -22
View File
@@ -67,21 +67,15 @@ class CoreNotifier(Notification):
def clean(self): def clean(self):
try:
db = get_session() db = get_session()
db.query(Notif).filter(Notif.added <= (int(time.time()) - 2419200)).delete() db.query(Notif).filter(Notif.added <= (int(time.time()) - 2419200)).delete()
db.commit() db.commit()
except:
log.error('Failed cleaning notification: %s', traceback.format_exc())
db.rollback()
finally:
db.close()
def markAsRead(self, ids = None, **kwargs): def markAsRead(self, ids = None, **kwargs):
ids = splitString(ids) if ids else None ids = splitString(ids) if ids else None
try:
db = get_session() db = get_session()
if ids: if ids:
@@ -90,20 +84,12 @@ class CoreNotifier(Notification):
q = db.query(Notif).filter_by(read = False) q = db.query(Notif).filter_by(read = False)
q.update({Notif.read: True}) q.update({Notif.read: True})
db.commit() db.commit()
return { return {
'success': True 'success': True
} }
except:
log.error('Failed mark as read: %s', traceback.format_exc())
db.rollback()
finally:
db.close()
return {
'success': False
}
def listView(self, limit_offset = None, **kwargs): def listView(self, limit_offset = None, **kwargs):
@@ -154,7 +140,6 @@ class CoreNotifier(Notification):
def notify(self, message = '', data = None, listener = None): def notify(self, message = '', data = None, listener = None):
if not data: data = {} if not data: data = {}
try:
db = get_session() db = get_session()
data['notification_type'] = listener if listener else 'unknown' data['notification_type'] = listener if listener else 'unknown'
@@ -173,11 +158,6 @@ class CoreNotifier(Notification):
self.frontend(type = listener, data = data) self.frontend(type = listener, data = data)
return True return True
except:
log.error('Failed notify: %s', traceback.format_exc())
db.rollback()
finally:
db.close()
def frontend(self, type = 'notification', data = None, message = None): def frontend(self, type = 'notification', data = None, message = None):
if not data: data = {} if not data: data = {}
@@ -147,7 +147,7 @@ var NotificationBase = new Class({
// Process data // Process data
if(json){ if(json){
Array.each(json.result, function(result){ Array.each(json.result, function(result){
App.trigger(result.type, [result]); App.trigger(result.type, result);
if(result.message && result.read === undefined) if(result.message && result.read === undefined)
self.showMessage(result.message); self.showMessage(result.message);
}) })
@@ -1,6 +1,5 @@
from .main import Email from .main import Email
def start(): def start():
return Email() return Email()
+1 -1
View File
@@ -40,7 +40,7 @@ class Email(Notification):
log.debug("SMTP over SSL %s", ("enabled" if ssl == 1 else "disabled")) log.debug("SMTP over SSL %s", ("enabled" if ssl == 1 else "disabled"))
mailserver = smtplib.SMTP_SSL(smtp_server) if ssl == 1 else smtplib.SMTP(smtp_server) mailserver = smtplib.SMTP_SSL(smtp_server) if ssl == 1 else smtplib.SMTP(smtp_server)
if starttls: if (starttls):
log.debug("Using StartTLS to initiate the connection with the SMTP server") log.debug("Using StartTLS to initiate the connection with the SMTP server")
mailserver.starttls() mailserver.starttls()
@@ -1,6 +1,5 @@
from .main import Growl from .main import Growl
def start(): def start():
return Growl() return Growl()
+1 -1
View File
@@ -37,7 +37,7 @@ class Growl(Notification):
) )
self.growl.register() self.growl.register()
self.registered = True self.registered = True
except Exception as e: except Exception, e:
if 'timed out' in str(e): if 'timed out' in str(e):
self.registered = True self.registered = True
else: else:
@@ -1,6 +1,5 @@
from .main import NMJ from .main import NMJ
def start(): def start():
return NMJ() return NMJ()
+4 -3
View File
@@ -86,17 +86,18 @@ class NMJ(Notification):
'arg3': '', 'arg3': '',
} }
params = tryUrlencode(params) params = tryUrlencode(params)
update_url = 'http://%(host)s:8008/metadata_database?%(params)s' % {'host': host, 'params': params} UPDATE_URL = 'http://%(host)s:8008/metadata_database?%(params)s'
updateUrl = UPDATE_URL % {'host': host, 'params': params}
try: try:
response = self.urlopen(update_url) response = self.urlopen(updateUrl)
except: except:
return False return False
try: try:
et = etree.fromstring(response) et = etree.fromstring(response)
result = et.findtext('returnValue') result = et.findtext('returnValue')
except SyntaxError as 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
@@ -1,6 +1,5 @@
from .main import NotifyMyAndroid from .main import NotifyMyAndroid
def start(): def start():
return NotifyMyAndroid() return NotifyMyAndroid()
@@ -2,7 +2,6 @@ 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
import pynma import pynma
import six
log = CPLog(__name__) log = CPLog(__name__)
@@ -27,7 +26,7 @@ class NotifyMyAndroid(Notification):
successful = 0 successful = 0
for key in keys: for key in keys:
if not response[str(key)]['code'] == six.u('200'): if not response[str(key)]['code'] == u'200':
log.error('Could not send notification to NotifyMyAndroid (%s). %s', (key, response[key]['message'])) log.error('Could not send notification to NotifyMyAndroid (%s). %s', (key, response[key]['message']))
else: else:
successful += 1 successful += 1
@@ -1,6 +1,5 @@
from .main import NotifyMyWP from .main import NotifyMyWP
def start(): def start():
return NotifyMyWP() return NotifyMyWP()
@@ -2,15 +2,13 @@ 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 pynmwp import PyNMWP from pynmwp import PyNMWP
import six
log = CPLog(__name__) log = CPLog(__name__)
class NotifyMyWP(Notification): class NotifyMyWP(Notification):
def notify(self, message = '', data = None, listener = None): def notify(self, message = '', data = {}, listener = None):
if not data: data = {}
keys = splitString(self.conf('api_key')) keys = splitString(self.conf('api_key'))
p = PyNMWP(keys, self.conf('dev_key')) p = PyNMWP(keys, self.conf('dev_key'))
@@ -18,7 +16,7 @@ class NotifyMyWP(Notification):
response = p.push(application = self.default_title, event = message, description = message, priority = self.conf('priority'), batch_mode = len(keys) > 1) response = p.push(application = self.default_title, event = message, description = message, priority = self.conf('priority'), batch_mode = len(keys) > 1)
for key in keys: for key in keys:
if not response[key]['Code'] == six.u('200'): if not response[key]['Code'] == u'200':
log.error('Could not send notification to NotifyMyWindowsPhone (%s). %s', (key, response[key]['message'])) log.error('Could not send notification to NotifyMyWindowsPhone (%s). %s', (key, response[key]['message']))
return False return False
@@ -1,6 +1,5 @@
from .main import Plex from .main import Plex
def start(): def start():
return Plex() return Plex()
@@ -29,7 +29,7 @@ class PlexClientHTTP(PlexClientProtocol):
try: try:
self.plex.urlopen(url, headers = headers, timeout = 3, show_error = False) self.plex.urlopen(url, headers = headers, timeout = 3, show_error = False)
except Exception as err: except Exception, err:
log.error("Couldn't sent command to Plex: %s", err) log.error("Couldn't sent command to Plex: %s", err)
return False return False
@@ -68,7 +68,7 @@ class PlexClientJSON(PlexClientProtocol):
try: try:
requests.post(url, headers = headers, timeout = 3, data = json.dumps(request)) requests.post(url, headers = headers, timeout = 3, data = json.dumps(request))
except Exception as err: except Exception, err:
log.error("Couldn't sent command to Plex: %s", err) log.error("Couldn't sent command to Plex: %s", err)
return False return False
+3 -4
View File
@@ -23,9 +23,9 @@ class Plex(Notification):
addEvent('renamer.after', self.addToLibrary) addEvent('renamer.after', self.addToLibrary)
def addToLibrary(self, message = None, group = None):
def addToLibrary(self, message = None, group = {}):
if self.isDisabled(): return if self.isDisabled(): return
if not group: group = {}
return self.server.refresh() return self.server.refresh()
@@ -57,8 +57,7 @@ class Plex(Notification):
return success return success
def notify(self, message = '', data = None, listener = None): def notify(self, message = '', data = {}, listener = None):
if not data: data = {}
return self.notifyClients(message, self.getClientNames()) return self.notifyClients(message, self.getClientNames())
def test(self, **kwargs): def test(self, **kwargs):

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