Compare commits

..

171 Commits

Author SHA1 Message Date
Ruud Burger 9a01b274ba Merge pull request #4715 from BillNasty/develop
Update torrentshack URL
2015-03-16 20:08:00 +01:00
Ruud f7ed835ba5 Change minimum movie size
fix #4730
2015-03-09 16:36:09 +01:00
Ruud 8af49b9508 Yify not return data
fix #4725
2015-03-09 16:31:22 +01:00
Ruud 998049a39d Python 2.6 parseerror
fix #4701
2015-03-09 16:07:05 +01:00
BillNasty 111bd1b07c Fix Description from old URL
Fix provider description from old url to new url
2015-03-04 14:09:03 -05:00
BillNasty a09c10aab6 Update torrentshack URL
Update from temporary url to new main url
2015-03-01 22:32:16 -05:00
Ruud adb744a526 Don't show double updater type 2015-02-22 17:42:29 +01:00
Ruud 0f82cda811 Remove podnapisi from subtile list 2015-02-22 16:09:22 +01:00
Ruud 0d6c3c8ecb Yify, only use data when available 2015-02-22 16:06:07 +01:00
Ruud 6598f53fd4 Quality check improve 2015-02-22 15:55:54 +01:00
Ruud 6b8458d87f Hadouken apikey check not using correct settingskey
fix #4674
2015-02-22 14:49:37 +01:00
Ruud 99a0621238 Use keep-alive connection 2015-02-22 14:30:50 +01:00
Ruud Burger c52666309a Merge pull request #4676 from peerster/develop
Update torrentshack with new URL
2015-02-22 13:39:21 +01:00
Ruud 84a458d40b Add user-agent and type to omdbapi 2015-02-22 13:06:29 +01:00
Ruud f8631c6d53 Add extra category for TorrentLeech
fix #4683
2015-02-21 21:29:37 +01:00
Ruud b19b0775c7 Force update to new poster on refresh
fix #4671
2015-02-20 22:16:12 +01:00
peerster 2dc1c1dd38 Update torrentshack with new URL 2015-02-19 20:07:22 +01:00
Ruud 7db8b233c8 Don't decode string if confidence isn't high enough 2015-02-18 17:21:24 +01:00
Ruud 427c77a9ef Remove podnapisi 2015-02-15 19:23:45 +01:00
Ruud 94c3969f10 Use https for yify proxy 2015-02-10 20:52:15 +01:00
Ruud debd1855dd Move Yify to v2 2015-02-10 20:47:19 +01:00
Ruud 9f77597c11 Torrentz search on title
fix #4510
2015-02-10 17:15:53 +01:00
Ruud afc9039625 Also search lower qualities on OMGWTF
fix #4527
2015-02-10 16:50:53 +01:00
Ruud 920d3cb44e Don't verify SYNO downloader thingymajig
fix #4641
2015-02-10 16:27:13 +01:00
Ruud b1fc8ad862 Letterboxed new html markup
fix #4640
2015-02-10 16:21:32 +01:00
Ruud 11b9bc39ab Show tried to often error for TD 2015-02-10 15:40:55 +01:00
Ruud 6dcb3f3bf2 Change bitsoup category id
fixes #4629
2015-02-10 14:55:22 +01:00
Ruud ce768f45c5 Make RottenTomato logging more clear
close #4618
2015-02-10 14:36:54 +01:00
Ruud 9b91d1d6c0 Remove favor, link to api key page 2015-02-10 14:10:55 +01:00
Ruud d9c7a97604 Merge branch 'develop' of git://github.com/jonnyboy/CouchPotatoServer into jonnyboy-develop 2015-02-10 14:03:06 +01:00
Ruud 0fd01aa697 Cleanup 2015-02-10 14:01:51 +01:00
Ruud 58615e6f9b Merge branch 'develop' of git://github.com/grasshide/CouchPotatoServer into grasshide-develop 2015-02-10 13:54:13 +01:00
Ruud 2277322e57 Traceback import missing 2015-02-10 13:47:22 +01:00
Ruud Burger 18020e609e Merge pull request #4479 from sjlu/develop
Adding the ability to receive notifications through Webhooks
2015-02-10 13:19:59 +01:00
Ruud 6a31b920ac Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2015-02-10 13:15:43 +01:00
Ruud c1266a36e4 Re-use resursion code 2015-02-10 13:15:08 +01:00
Ruud 578effc538 Merge branch 'develop' of git://github.com/dumaresq/CouchPotatoServer into dumaresq-develop 2015-02-10 13:09:12 +01:00
Ruud Burger d881120013 Merge pull request #4513 from starkers/remotes/origin/develop
added touch and chown to the $PID_FILE
2015-02-10 13:07:25 +01:00
Ruud Burger da5318033a Merge pull request #4380 from mannkind/develop
Initial support for Plex Media Server w/Plex Home
2015-02-10 13:04:27 +01:00
Ruud Burger 31df5bce01 Merge pull request #4612 from maikhorma/maikhorma-#2782
Simple workaround for #2782
2015-02-10 13:02:24 +01:00
Ruud d5622b7cba Remove www from torrentday domain 2015-02-10 13:01:19 +01:00
Ruud Burger 26ad1b354f Merge pull request #4552 from coolius/patch-1
Update torrentday url
2015-02-10 12:53:52 +01:00
Ruud 7a616a81f7 Remove www from iptorrents 2015-02-10 12:52:05 +01:00
Ruud Burger 275aefc3cc Merge pull request #4553 from coolius/patch-2
Update iptorrents url
2015-02-10 12:51:02 +01:00
Ruud Burger 2b32490f72 Merge pull request #4649 from sammy2142/patch-1
Update kickass url from kickass.so to kickass.to
2015-02-10 12:49:16 +01:00
sammy2142 7b9043c16b Update kickass url from kickass.so to kickass.to
Kickass has reverted back to the .to domain as the .so domain was seized:
http://torrentfreak.com/kickasstorrents-taken-domain-name-seizure-150209/
2015-02-10 11:11:30 +00:00
maikhorma cf83f99be0 Updated UI
Tried to make it a bit cleaner.
2015-02-01 15:28:05 -05:00
maikhorma fb8a66d207 Shortcut to address #2782
Until there is a more elegant solution to avoid unwanted white space
trimming, this will let users disable that feature if it is not
something they need.
2015-02-01 14:43:16 -05:00
Ruud e8a3645bc6 Log failed folder getting 2015-02-01 12:18:31 +01:00
Ruud 592e40993c Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2015-01-31 10:32:24 +01:00
Ruud b00e69e222 TorrentBytes cut of longer titles
fix #4590
2015-01-31 10:32:15 +01:00
Ruud c9b4c8167f Actual include host in log 2015-01-28 11:35:26 +01:00
coolius cdb9cfe756 Update iptorrents.py
Updated iptorrents url to blockade-free iptorrents.eu
2015-01-19 17:18:56 +00:00
coolius e52f50b204 Update torrentday.py
Updated torrentday url to blockade-free torrentday.eu
2015-01-19 17:17:31 +00:00
Ruud 770c2be14c Create detail url if permalink is false 2015-01-17 13:04:47 +01:00
Ruud ab61961a64 Use detail url 2015-01-14 16:59:29 +01:00
Ruud 6aca799bbb Newznab: use guid for detail url 2015-01-14 16:55:30 +01:00
David Stark 89836be1d1 added touch and chown to the $PID_FILE 2015-01-12 17:37:26 +01:00
Andrew Dumaresq 20e1283627 better way to find the folder 2015-01-11 11:57:14 -05:00
Andrew Dumaresq ee8406e026 Minor text change 2015-01-11 11:45:29 -05:00
Andrew Dumaresq 514941b785 Merge branch 'develop' of https://github.com/dumaresq/CouchPotatoServer into develop 2015-01-11 11:42:52 -05:00
Ruud 1510e37652 Update Tornado 2015-01-11 16:18:22 +01:00
Ruud e1e39cd3f4 Update requests 2015-01-11 16:17:33 +01:00
Ruud e1bb8c5419 Update Chardet 2015-01-11 16:15:52 +01:00
Ruud 17fa33a496 Update user agent 2015-01-11 00:25:58 +01:00
Ruud 601f0b54cf Send CP header when downloading from newznab 2015-01-11 00:25:51 +01:00
dumaresq 51d44bfc3e Merge pull request #1 from RuudBurger/develop
Develop
2015-01-10 17:01:43 -05:00
Ruud 12148217a2 Log failed notification 2015-01-10 13:41:17 +01:00
Ruud 132fa12ef4 Late list not loaded on home 2015-01-10 12:17:47 +01:00
Ruud 1827c2e4cd Don't parse omgwtfnzb if no results are returned 2015-01-10 12:17:30 +01:00
Ruud f423bca06b Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2015-01-09 20:13:42 +01:00
Ruud e7b089edf5 Give better XML issues 2015-01-09 20:13:17 +01:00
Ruud Burger b8b7d94a6a Merge pull request #4456 from dumaresq/develop
Bug fixes and new features for putio
2015-01-09 20:08:30 +01:00
Ruud 2c080fec3d TorrentBytes nbsp issue
fix #4026
2015-01-08 16:56:38 +01:00
Ruud 4c68566c77 Use new OMGWTFNZB api
fix #4471
2015-01-08 14:59:53 +01:00
Steven Lu a3af784c18 Adding the ability to receive notifications through Webhooks 2015-01-06 18:47:19 -05:00
grasshide ac6f295c93 New algogithm to use some kind of crowd logic on newznab powered
providers.
2015-01-05 15:00:40 +01:00
Andrew Dumaresq 2c72cd7d9f Added new folder option and fixed but in callback url 2015-01-04 17:10:40 -05:00
Andrew Dumaresq d012dc5c85 Added new folder option 2015-01-04 17:10:16 -05:00
Andrew Dumaresq 038b4c63ee Updated to follow putio API changes 2015-01-04 17:09:36 -05:00
Ruud Burger 17e37996c4 Add remux category for TorrentShack
close #4427
2015-01-02 18:18:08 +01:00
jonnyboy 9318e19347 New torrent search provider hdaccess.net 2014-12-31 08:21:58 -05:00
Ruud 8f4e03d04b Use detected encoding
#4388
2014-12-27 13:46:25 +01:00
Ruud 229d67c086 Don't toUnicode loop 2014-12-22 22:01:47 +01:00
Dustin Brewer d84897ff33 Initial support for Plex Media Server w/Plex Home 2014-12-21 16:18:11 -08:00
Ruud 387a711538 TorrentBytes not encoding name
fix #4377
2014-12-21 21:14:38 +01:00
Ruud 7a1b914824 Return nonblock results in main thread 2014-12-21 20:19:53 +01:00
Ruud 5e62801666 Send data through finish not write 2014-12-21 20:19:30 +01:00
Ruud 00d887153f Return data in main thread 2014-12-21 19:39:16 +01:00
Ruud 6d5882001a Notificaton.list not returning anything
fix #4348
2014-12-20 22:32:18 +01:00
Ruud 4a6b45c65c SCC not finding seeders 2014-12-20 22:24:00 +01:00
Ruud b0d1fe5c33 Return false if no media is found on try_next
fix #4345
2014-12-20 22:17:43 +01:00
Ruud a6e49098c8 Add robots.txt 2014-12-20 22:15:27 +01:00
Ruud ffcd36cbf4 IOLoop callback hanging 2014-12-20 21:45:15 +01:00
Ruud 3bf2d844a0 Release api lock on connection close or finish
fix #4372
2014-12-20 20:13:49 +01:00
Ruud dd24eb8893 Revert "Give response back to the main thread on api calls"
This reverts commit 576bcb9f4b.

Conflicts:
	couchpotato/api.py
2014-12-20 18:49:35 +01:00
Ruud 538f51dd5b Log ipv6 failed bind 2014-12-19 14:16:20 +01:00
Ruud eea9f40501 Use current 2014-12-19 09:01:52 +01:00
Ruud 576bcb9f4b Give response back to the main thread on api calls
fix #4337
2014-12-19 08:57:24 +01:00
Ruud Burger 62c5365329 Merge pull request #4356 from rtaibah/ReaddTypoFix
Change Readd in tooltip to Re-add. Former is confusing and not an Englis...
2014-12-17 11:36:53 +01:00
Rami Taibah ddf575a86e Change Readd in tooltip to Re-add. Former is confusing and not an English word 2014-12-17 13:00:54 +03:00
Ruud Burger 6b9383ce92 Merge pull request #4342 from mano3m/develop_fixsize
Fix TorrentShack size
2014-12-16 07:52:08 +01:00
mano3m cb8d24ef1f Fix TorrentShack size 2014-12-15 22:26:29 +01:00
Ruud 814ddfb79f Don't return password fields
fix #4300
2014-12-14 12:33:28 +01:00
Ruud 766f819c0b Userscript for RT not parsing URL correctly 2014-12-14 12:06:03 +01:00
Ruud ff43df9ef1 Comments comments comments 2014-12-02 15:38:55 +01:00
Ruud 2e907e93e7 Whiteline 2014-12-02 12:02:49 +01:00
Ruud 4d329d6a36 Revert "Remove torrentleech"
This reverts commit dacc3d8f47.
2014-12-02 11:45:17 +01:00
Ruud 752191bc23 Comments 2014-12-02 11:43:10 +01:00
Ruud 1d73fd9d7e Import optimize 2014-12-02 11:15:29 +01:00
Ruud 79688c412a Merge branch 'develop' of git://github.com/hadouken/CouchPotatoServer into hadouken-develop 2014-12-02 11:07:54 +01:00
Ruud fc1c95fefb Description 2014-12-01 23:00:59 +01:00
Ruud 6a174716af underscored variables 2014-12-01 22:52:10 +01:00
Ruud defe256f1b Correct url 2014-12-01 16:52:43 +01:00
Ruud 8a5f154d9e Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-12-01 16:52:04 +01:00
Ruud fe56a69e8f Put.IO cleanup 2014-12-01 16:49:27 +01:00
Ruud c6d326f973 Move put.io API 2014-12-01 15:50:03 +01:00
Ruud 9e5f670feb Merge branch 'putio' of git://github.com/dumaresq/CouchPotatoServer into develop 2014-12-01 15:42:43 +01:00
Ruud Burger 9ebacf8816 Merge pull request #4258 from glibix/develop
Transmission status 16 is for "Stopped". So we need to detect a download...
2014-12-01 15:41:41 +01:00
Ruud df2d7ec9c2 Remove debug code 2014-12-01 15:39:33 +01:00
Ruud ddab74582b Merge branch 'develop' of git://github.com/psaab/CouchPotatoServer into psaab-develop 2014-12-01 15:28:11 +01:00
Ruud 2801079bc8 Merge branch 'develop_3845' of git://github.com/voidstarstar/CouchPotatoServer into voidstarstar-develop_3845 2014-12-01 15:14:26 +01:00
Ruud Burger 1deb49b524 Merge pull request #4261 from voidstarstar/develop_4211
Added renamer.progress API function. Fixes #4211.
2014-12-01 15:12:21 +01:00
Ruud Burger 49d550f652 Merge pull request #4270 from sammy2142/patch-1
Update Kickass url to https://kickass.so
2014-11-30 22:05:36 +01:00
sammy2142 1a43ce6ecc Update Kickass url to https://kickass.so
Kickass has recently changed its web address from https://kickass.to 
to https://kickass.so
2014-11-30 20:48:31 +00:00
voidstarstar 15a0131587 Added renamer.progress API function. Fixes #4211.
This function reports the status of the renamer.
Progress value True means the renamer is currently running.
Progress value False means the renamer is not currently running.
2014-11-27 21:51:30 -05:00
voidstarstar 0dca34958c Added a parameter to the renamer API. Fixes #3845.
The renamer now has a new 'to_folder' parameter.
This parameter specifies where movies are moved to.
2014-11-27 21:43:19 -05:00
Mathew Paret 4b231e36ea Merge branch 'feature/3967_add_imdb_link_to_tweet' into develop 2014-11-27 18:16:41 +05:30
Mathew Paret 52478a00db Revert "Feature #3967 - Added IMDB link to download complete tweet"
This reverts commit 87338760ad.
2014-11-27 18:13:41 +05:30
Mathew Paret e177766270 Merge branch 'feature/3967_add_imdb_link_to_tweet' into develop 2014-11-27 18:06:38 +05:30
Ruud Burger ff8da7c8f8 Merge pull request #4068 from ofir123/subscenter_support
Added support for subscenter.
2014-11-26 21:51:14 +01:00
Ruud Burger 89c8c5a0c7 Merge pull request #4203 from rkokkelk/develop
Fix startup script Debian/Ubuntu
2014-11-26 21:48:40 +01:00
Ruud 38c6266f9c Use single quotes 2014-11-26 21:47:39 +01:00
Ruud Burger 16f8e7e123 Merge pull request #4205 from kamillus/develop
adding a fix to handle missing directories in the file browser in webkit browsers
2014-11-26 21:46:10 +01:00
Ruud Burger 7110c7a11f Merge pull request #4249 from clinton-hall/patch-1
NZBGet 13 includes more status information
2014-11-25 07:55:39 +01:00
Clinton Hall 6d79f316a6 NZBGet 13 includes more status information
nzb['Status'] returns total (SUCESS/ALL) status and also failed status in V13+
This is particularly important when using fake detector scripts or stopping download due to health checks etc.
http://nzbget.net/RPC_API_reference#Method_.22history.22
https://couchpota.to/forum/viewtopic.php?f=5&t=4644
2014-11-25 11:02:47 +10:30
Paul Saab c1b6811b8a Tornado requires two sockets to support IPv6
Tornado sets setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
to force IPv6 sockets to only be used for IPv6 connections.  create a
separate socket to allow for CouchPotato to be used over IPv6.
2014-11-17 22:54:56 -08:00
Kamil 7d7b76b2e9 adding a fix to handle missing directories in the file browser in webkit browsers 2014-11-10 20:18:38 -05:00
Roy Kokkelkoren 657aa52fa7 Merge branch 'develop' of https://github.com/rkokkelk/CouchPotatoServer into develop 2014-11-10 15:40:41 +01:00
Roy Kokkelkoren 8e9ef8db39 Merge remote-tracking branch 'upstream/develop' into develop 2014-11-10 15:39:45 +01:00
test 92a0096b54 Merge remote-tracking branch 'upstream/develop' into develop 2014-11-10 08:29:27 -06:00
Mathew Paret 87338760ad Feature #3967 - Added IMDB link to download complete tweet 2014-11-10 18:47:37 +05:30
Mathew Paret 28019b0a09 Transmission status 16 is for "Stopped". So we need to detect a download as completed even if it is stopped but percent done is 100 2014-11-10 18:39:58 +05:30
Ruud Burger 248b007f4a Merge pull request #4094 from georgewhewell/hdbits-add-internal-only
add option for internal-only for hdbits provider
2014-11-09 14:39:50 +01:00
Ruud Burger 9e31c59de8 Merge pull request #4188 from DjSlash/patch-2
Update SSL protocol for Deluge connections
2014-11-09 14:37:23 +01:00
Ruud 269e785888 Yify, don't include quality in search
fix #4190
2014-11-09 14:30:22 +01:00
Ruud 3669aef42d is_movie param 2014-11-09 14:14:06 +01:00
Ruud 1087eb3a06 Add adding parameter to is_movie 2014-11-09 14:10:23 +01:00
Rutger van Sleen 43af80a137 Update SSL protocol for Deluge connections
Since Deluge 1.3.10 the SSL protocol is updated to TLSv1 instead of SSLv3. This resulted in CP not being able to add new torrents. Link to change in Deluge: http://git.deluge-torrent.org/deluge/commit/?h=1.3-stable&id=26f5be17609a8312c4ba06aa120ed208cd7876f2
2014-11-06 14:33:38 +01:00
Roy Kokkelkoren 0766a27a71 Fixed bug in init.d script which prevented the writing of the PID file.
Altered default value of DATA_DIR to /var/opt/couchpotato in order to comply to linux file structure
2014-11-06 04:40:39 -06:00
Ruud a12f049d14 Bit-HDTV http -> https
fix #3570
2014-11-01 17:30:11 +01:00
Ruud 6afe2fd9cf IPTorrents webdl category
fix #4150
2014-11-01 15:36:54 +01:00
Viktor Elofsson 61f634a21e Refactored Hadouken downloader. 2014-10-21 16:52:28 +02:00
georgewhewell d626fda710 add option for internal only for hdbits provider 2014-10-17 15:14:44 +01:00
Viktor Elofsson b40d1f3463 Merge pull request #1 from RuudBurger/develop
Sync upstream
2014-10-14 13:18:48 +02:00
Ofir Brukner 1030d0d748 Added support for subscenter.
Updated both plugin and lib.
2014-10-13 00:50:29 +03:00
Viktor Elofsson 2e52c8124a Implemented a downloader for Hadouken. 2014-10-02 20:34:43 +02:00
Andrew Dumaresq 8de5fcdac6 fixed button name 2014-09-20 19:39:35 -04:00
Andrew Dumaresq 4aa9801be4 general code cleanup 2014-09-20 19:39:12 -04:00
dumaresq 3e58378490 figured out how to make the check work better 2014-09-19 21:41:58 -04:00
Andrew Dumaresq 2c40db3074 removed un-needed variable 2014-09-19 20:28:03 -04:00
dumaresq fba228fd9d fixing check function 2014-09-19 20:26:54 -04:00
Andrew Dumaresq ef2b8e88b4 better download checking 2014-09-19 07:07:23 -04:00
Andrew Dumaresq c77b270fa8 Cleaned up OAUTH and made the download asyc 2014-09-18 06:00:09 -04:00
dumaresq 872a4f4650 Worked on geting Oauth and adding download status 2014-09-07 17:59:16 -04:00
Ruud d0f1e7c6a3 Update put.io code 2014-08-29 12:30:31 +02:00
Ruud 53e7e383a3 put.io rename 2014-08-29 11:38:28 +02:00
Ruud c06e1f3135 Merge branch 'develop' of git://github.com/dumaresq/CouchPotatoServer into dumaresq-develop 2014-08-29 11:37:49 +02:00
dumaresq bb73cb8eec Fixed missing library 2014-08-24 18:19:01 -04:00
dumaresq 5acab98025 fixed hardcoded directory 2014-08-19 19:32:00 -04:00
dumaresq ed6a46e9c0 Added putioDownloader 2014-08-17 16:28:47 -04:00
186 changed files with 2853 additions and 10983 deletions
+9
View File
@@ -40,6 +40,8 @@ class WebHandler(BaseHandler):
return
try:
if route == 'robots.txt':
self.set_header('Content-Type', 'text/plain')
self.write(views[route]())
except:
log.error("Failed doing web request '%s': %s", (route, traceback.format_exc()))
@@ -60,6 +62,13 @@ def index():
addView('', index)
# Web view
def robots():
return 'User-agent: * \n' \
'Disallow: /'
addView('robots.txt', robots)
# API docs
def apiDocs():
routes = list(api.keys())
+26 -22
View File
@@ -7,6 +7,7 @@ import urllib
from couchpotato.core.helpers.request import getParams
from couchpotato.core.logger import CPLog
from tornado.ioloop import IOLoop
from tornado.web import RequestHandler, asynchronous
@@ -50,24 +51,22 @@ class NonBlockHandler(RequestHandler):
start, stop = api_nonblock[route]
self.stopper = stop
start(self.onNewMessage, last_id = self.get_argument('last_id', None))
start(self.sendData, last_id = self.get_argument('last_id', None))
def onNewMessage(self, response):
if self.request.connection.stream.closed():
self.on_connection_close()
return
def sendData(self, response):
if not self.request.connection.stream.closed():
try:
self.finish(response)
except:
log.debug('Failed doing nonblock request, probably already closed: %s', (traceback.format_exc()))
try: self.finish({'success': False, 'error': 'Failed returning results'})
except: pass
try:
self.finish(response)
except:
log.debug('Failed doing nonblock request, probably already closed: %s', (traceback.format_exc()))
try: self.finish({'success': False, 'error': 'Failed returning results'})
except: pass
def on_connection_close(self):
self.removeStopper()
def removeStopper(self):
if self.stopper:
self.stopper(self.onNewMessage)
self.stopper(self.sendData)
self.stopper = None
@@ -83,10 +82,11 @@ def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
# Blocking API handler
class ApiHandler(RequestHandler):
route = None
@asynchronous
def get(self, route, *args, **kwargs):
route = route.strip('/')
self.route = route = route.strip('/')
if not api.get(route):
self.write('API call doesn\'t seem to exist')
self.finish()
@@ -123,11 +123,15 @@ class ApiHandler(RequestHandler):
except:
log.error('Failed write error "%s": %s', (route, traceback.format_exc()))
api_locks[route].release()
self.unlock()
post = get
def taskFinished(self, result, route):
IOLoop.current().add_callback(self.sendData, result, route)
self.unlock()
def sendData(self, result, route):
if not self.request.connection.stream.closed():
try:
@@ -135,14 +139,12 @@ class ApiHandler(RequestHandler):
jsonp_callback = self.get_argument('callback_func', default = None)
if jsonp_callback:
self.write(str(jsonp_callback) + '(' + json.dumps(result) + ')')
self.set_header("Content-Type", "text/javascript")
self.finish()
self.set_header('Content-Type', 'text/javascript')
self.finish(str(jsonp_callback) + '(' + json.dumps(result) + ')')
elif isinstance(result, tuple) and result[0] == 'redirect':
self.redirect(result[1])
else:
self.write(result)
self.finish()
self.finish(result)
except UnicodeDecodeError:
log.error('Failed proper encode: %s', traceback.format_exc())
except:
@@ -150,7 +152,9 @@ class ApiHandler(RequestHandler):
try: self.finish({'success': False, 'error': 'Failed returning results'})
except: pass
api_locks[route].release()
def unlock(self):
try: api_locks[self.route].release()
except: pass
def addApiView(route, func, static = False, docs = None, **kwargs):
+36
View File
@@ -20,14 +20,31 @@ class Blackhole(DownloaderBase):
status_support = False
def download(self, data = None, media = None, filedata = None):
""" Send a torrent/nzb file to the downloader
:param data: dict returned from provider
Contains the release information
:param media: media dict with information
Used for creating the filename when possible
:param filedata: downloaded torrent/nzb filedata
The file gets downloaded in the searcher and send to this function
This is done to have failed checking before using the downloader, so the downloader
doesn't need to worry about that
:return: boolean
One faile returns false, but the downloaded should log his own errors
"""
if not media: media = {}
if not data: data = {}
directory = self.conf('directory')
# The folder needs to exist
if not directory or not os.path.isdir(directory):
log.error('No directory set for blackhole %s download.', data.get('protocol'))
else:
try:
# Filedata can be empty, which probably means it a magnet link
if not filedata or len(filedata) < 50:
try:
if data.get('protocol') == 'torrent_magnet':
@@ -36,13 +53,16 @@ class Blackhole(DownloaderBase):
except:
log.error('Failed download torrent via magnet url: %s', traceback.format_exc())
# If it's still empty, don't know what to do!
if not filedata or len(filedata) < 50:
log.error('No nzb/torrent available: %s', data.get('url'))
return False
# Create filename with imdb id and other nice stuff
file_name = self.createFileName(data, filedata, media)
full_path = os.path.join(directory, file_name)
# People want thinks nice and tidy, create a subdir
if self.conf('create_subdir'):
try:
new_path = os.path.splitext(full_path)[0]
@@ -53,6 +73,8 @@ class Blackhole(DownloaderBase):
log.error('Couldnt create sub dir, reverting to old one: %s', full_path)
try:
# Make sure the file doesn't exist yet, no need in overwriting it
if not os.path.isfile(full_path):
log.info('Downloading %s to %s.', (data.get('protocol'), full_path))
with open(full_path, 'wb') as f:
@@ -74,6 +96,10 @@ class Blackhole(DownloaderBase):
return False
def test(self):
""" Test and see if the directory is writable
:return: boolean
"""
directory = self.conf('directory')
if directory and os.path.isdir(directory):
@@ -88,6 +114,10 @@ class Blackhole(DownloaderBase):
return False
def getEnabledProtocol(self):
""" What protocols is this downloaded used for
:return: list with protocols
"""
if self.conf('use_for') == 'both':
return super(Blackhole, self).getEnabledProtocol()
elif self.conf('use_for') == 'torrent':
@@ -96,6 +126,12 @@ class Blackhole(DownloaderBase):
return ['nzb']
def isEnabled(self, manual = False, data = None):
""" Check if protocol is used (and enabled)
:param manual: The user has clicked to download a link through the webUI
:param data: dict returned from provider
Contains the release information
:return: boolean
"""
if not data: data = {}
for_protocol = ['both']
if data and 'torrent' in data.get('protocol'):
+29
View File
@@ -25,6 +25,11 @@ class Deluge(DownloaderBase):
drpc = None
def connect(self, reconnect = False):
""" Connect to the delugeRPC, re-use connection when already available
:param reconnect: force reconnect
:return: DelugeRPC instance
"""
# Load host from config and split out port.
host = cleanHost(self.conf('host'), protocol = False).split(':')
@@ -42,6 +47,20 @@ class Deluge(DownloaderBase):
return self.drpc
def download(self, data = None, media = None, filedata = None):
""" Send a torrent/nzb file to the downloader
:param data: dict returned from provider
Contains the release information
:param media: media dict with information
Used for creating the filename when possible
:param filedata: downloaded torrent/nzb filedata
The file gets downloaded in the searcher and send to this function
This is done to have failed checking before using the downloader, so the downloader
doesn't need to worry about that
:return: boolean
One faile returns false, but the downloaded should log his own errors
"""
if not media: media = {}
if not data: data = {}
@@ -96,11 +115,21 @@ class Deluge(DownloaderBase):
return self.downloadReturnId(remote_torrent)
def test(self):
""" Check if connection works
:return: bool
"""
if self.connect(True) and self.drpc.test():
return True
return False
def getAllDownloadStatus(self, ids):
""" Get status of all active downloads
:param ids: list of (mixed) downloader ids
Used to match the releases for this downloader as there could be
other downloaders active that it should ignore
:return: list of releases
"""
log.debug('Checking Deluge download status.')
+427
View File
@@ -0,0 +1,427 @@
from base64 import b16encode, b32decode, b64encode
from distutils.version import LooseVersion
from hashlib import sha1
import httplib
import json
import os
import re
import urllib2
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
from couchpotato.core.helpers.encoding import isInt, sp
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog
from bencode import bencode as benc, bdecode
log = CPLog(__name__)
autoload = 'Hadouken'
class Hadouken(DownloaderBase):
protocol = ['torrent', 'torrent_magnet']
hadouken_api = None
def connect(self):
# Load host from config and split out port.
host = cleanHost(self.conf('host'), protocol = False).split(':')
if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.')
return False
if not self.conf('api_key'):
log.error('Config properties are not filled in correctly, API key is missing.')
return False
self.hadouken_api = HadoukenAPI(host[0], port = host[1], api_key = self.conf('api_key'))
return True
def download(self, data = None, media = None, filedata = None):
""" Send a torrent/nzb file to the downloader
:param data: dict returned from provider
Contains the release information
:param media: media dict with information
Used for creating the filename when possible
:param filedata: downloaded torrent/nzb filedata
The file gets downloaded in the searcher and send to this function
This is done to have failed checking before using the downloader, so the downloader
doesn't need to worry about that
:return: boolean
One faile returns false, but the downloaded should log his own errors
"""
if not media: media = {}
if not data: data = {}
log.debug("Sending '%s' (%s) to Hadouken.", (data.get('name'), data.get('protocol')))
if not self.connect():
return False
torrent_params = {}
if self.conf('label'):
torrent_params['label'] = self.conf('label')
torrent_filename = self.createFileName(data, filedata, media)
if data.get('protocol') == 'torrent_magnet':
torrent_hash = re.findall('urn:btih:([\w]{32,40})', data.get('url'))[0].upper()
torrent_params['trackers'] = self.torrent_trackers
torrent_params['name'] = torrent_filename
else:
info = bdecode(filedata)['info']
torrent_hash = sha1(benc(info)).hexdigest().upper()
# Convert base 32 to hex
if len(torrent_hash) == 32:
torrent_hash = b16encode(b32decode(torrent_hash))
# Send request to Hadouken
if data.get('protocol') == 'torrent_magnet':
self.hadouken_api.add_magnet_link(data.get('url'), torrent_params)
else:
self.hadouken_api.add_file(filedata, torrent_params)
return self.downloadReturnId(torrent_hash)
def test(self):
""" Tests the given host:port and API key """
if not self.connect():
return False
version = self.hadouken_api.get_version()
if not version:
log.error('Could not get Hadouken version.')
return False
# The minimum required version of Hadouken is 4.5.6.
if LooseVersion(version) >= LooseVersion('4.5.6'):
return True
log.error('Hadouken v4.5.6 (or newer) required. Found v%s', version)
return False
def getAllDownloadStatus(self, ids):
""" Get status of all active downloads
:param ids: list of (mixed) downloader ids
Used to match the releases for this downloader as there could be
other downloaders active that it should ignore
:return: list of releases
"""
log.debug('Checking Hadouken download status.')
if not self.connect():
return []
release_downloads = ReleaseDownloadList(self)
queue = self.hadouken_api.get_by_hash_list(ids)
if not queue:
return []
for torrent in queue:
if torrent is None:
continue
torrent_filelist = self.hadouken_api.get_files_by_hash(torrent['InfoHash'])
torrent_files = []
save_path = torrent['SavePath']
# The 'Path' key for each file_item contains
# the full path to the single file relative to the
# torrents save path.
# For a single file torrent the result would be,
# - Save path: "C:\Downloads"
# - file_item['Path'] = "file1.iso"
# Resulting path: "C:\Downloads\file1.iso"
# For a multi file torrent the result would be,
# - Save path: "C:\Downloads"
# - file_item['Path'] = "dirname/file1.iso"
# Resulting path: "C:\Downloads\dirname/file1.iso"
for file_item in torrent_filelist:
torrent_files.append(sp(os.path.join(save_path, file_item['Path'])))
release_downloads.append({
'id': torrent['InfoHash'].upper(),
'name': torrent['Name'],
'status': self.get_torrent_status(torrent),
'seed_ratio': self.get_seed_ratio(torrent),
'original_status': torrent['State'],
'timeleft': -1,
'folder': sp(save_path if len(torrent_files == 1) else os.path.join(save_path, torrent['Name'])),
'files': torrent_files
})
return release_downloads
def get_seed_ratio(self, torrent):
""" Returns the seed ratio for a given torrent.
Keyword arguments:
torrent -- The torrent to calculate seed ratio for.
"""
up = torrent['TotalUploadedBytes']
down = torrent['TotalDownloadedBytes']
if up > 0 and down > 0:
return up / down
return 0
def get_torrent_status(self, torrent):
""" Returns the CouchPotato status for a given torrent.
Keyword arguments:
torrent -- The torrent to translate status for.
"""
if torrent['IsSeeding'] and torrent['IsFinished'] and torrent['Paused']:
return 'completed'
if torrent['IsSeeding']:
return 'seeding'
return 'busy'
def pause(self, release_download, pause = True):
""" Pauses or resumes the torrent specified by the ID field
in release_download.
Keyword arguments:
release_download -- The CouchPotato release_download to pause/resume.
pause -- Boolean indicating whether to pause or resume.
"""
if not self.connect():
return False
return self.hadouken_api.pause(release_download['id'], pause)
def removeFailed(self, release_download):
""" Removes a failed torrent and also remove the data associated with it.
Keyword arguments:
release_download -- The CouchPotato release_download to remove.
"""
log.info('%s failed downloading, deleting...', release_download['name'])
if not self.connect():
return False
return self.hadouken_api.remove(release_download['id'], remove_data = True)
def processComplete(self, release_download, delete_files = False):
""" Removes the completed torrent from Hadouken and optionally removes the data
associated with it.
Keyword arguments:
release_download -- The CouchPotato release_download to remove.
delete_files: Boolean indicating whether to remove the associated data.
"""
log.debug('Requesting Hadouken to remove the torrent %s%s.',
(release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
if not self.connect():
return False
return self.hadouken_api.remove(release_download['id'], remove_data = delete_files)
class HadoukenAPI(object):
def __init__(self, host = 'localhost', port = 7890, api_key = None):
self.url = 'http://' + str(host) + ':' + str(port)
self.api_key = api_key
self.requestId = 0;
self.opener = urllib2.build_opener()
self.opener.addheaders = [('User-agent', 'couchpotato-hadouken-client/1.0'), ('Accept', 'application/json')]
if not api_key:
log.error('API key missing.')
def add_file(self, filedata, torrent_params):
""" Add a file to Hadouken with the specified parameters.
Keyword arguments:
filedata -- The binary torrent data.
torrent_params -- Additional parameters for the file.
"""
data = {
'method': 'torrents.addFile',
'params': [b64encode(filedata), torrent_params]
}
return self._request(data)
def add_magnet_link(self, magnetLink, torrent_params):
""" Add a magnet link to Hadouken with the specified parameters.
Keyword arguments:
magnetLink -- The magnet link to send.
torrent_params -- Additional parameters for the magnet link.
"""
data = {
'method': 'torrents.addUrl',
'params': [magnetLink, torrent_params]
}
return self._request(data)
def get_by_hash_list(self, infoHashList):
""" Gets a list of torrents filtered by the given info hash list.
Keyword arguments:
infoHashList -- A list of info hashes.
"""
data = {
'method': 'torrents.getByInfoHashList',
'params': [infoHashList]
}
return self._request(data)
def get_files_by_hash(self, infoHash):
""" Gets a list of files for the torrent identified by the
given info hash.
Keyword arguments:
infoHash -- The info hash of the torrent to return files for.
"""
data = {
'method': 'torrents.getFiles',
'params': [infoHash]
}
return self._request(data)
def get_version(self):
""" Gets the version, commitish and build date of Hadouken. """
data = {
'method': 'core.getVersion',
'params': None
}
result = self._request(data)
if not result:
return False
return result['Version']
def pause(self, infoHash, pause):
""" Pauses/unpauses the torrent identified by the given info hash.
Keyword arguments:
infoHash -- The info hash of the torrent to operate on.
pause -- If true, pauses the torrent. Otherwise resumes.
"""
data = {
'method': 'torrents.pause',
'params': [infoHash]
}
if not pause:
data['method'] = 'torrents.resume'
return self._request(data)
def remove(self, infoHash, remove_data = False):
""" Removes the torrent identified by the given info hash and
optionally removes the data as well.
Keyword arguments:
infoHash -- The info hash of the torrent to remove.
remove_data -- If true, removes the data associated with the torrent.
"""
data = {
'method': 'torrents.remove',
'params': [infoHash, remove_data]
}
return self._request(data)
def _request(self, data):
self.requestId += 1
data['jsonrpc'] = '2.0'
data['id'] = self.requestId
request = urllib2.Request(self.url + '/jsonrpc', data = json.dumps(data))
request.add_header('Authorization', 'Token ' + self.api_key)
request.add_header('Content-Type', 'application/json')
try:
f = self.opener.open(request)
response = f.read()
f.close()
obj = json.loads(response)
if not 'error' in obj.keys():
return obj['result']
log.error('JSONRPC error, %s: %s', obj['error']['code'], obj['error']['message'])
except httplib.InvalidURL as err:
log.error('Invalid Hadouken host, check your config %s', err)
except urllib2.HTTPError as err:
if err.code == 401:
log.error('Invalid Hadouken API key, check your config')
else:
log.error('Hadouken HTTPError: %s', err)
except urllib2.URLError as err:
log.error('Unable to connect to Hadouken %s', err)
return False
config = [{
'name': 'hadouken',
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'hadouken',
'label': 'Hadouken',
'description': 'Use <a href="http://www.hdkn.net">Hadouken</a> (>= v4.5.6) to download torrents.',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
'radio_group': 'torrent'
},
{
'name': 'host',
'default': 'localhost:7890'
},
{
'name': 'api_key',
'label': 'API key',
'type': 'password'
},
{
'name': 'label',
'description': 'Label to add torrent as.'
}
]
}
]
}]
+28 -3
View File
@@ -23,6 +23,20 @@ class NZBGet(DownloaderBase):
rpc = 'xmlrpc'
def download(self, data = None, media = None, filedata = None):
""" Send a torrent/nzb file to the downloader
:param data: dict returned from provider
Contains the release information
:param media: media dict with information
Used for creating the filename when possible
:param filedata: downloaded torrent/nzb filedata
The file gets downloaded in the searcher and send to this function
This is done to have failed checking before using the downloader, so the downloader
doesn't need to worry about that
:return: boolean
One faile returns false, but the downloaded should log his own errors
"""
if not media: media = {}
if not data: data = {}
@@ -71,6 +85,10 @@ class NZBGet(DownloaderBase):
return False
def test(self):
""" Check if connection works
:return: bool
"""
rpc = self.getRPC()
try:
@@ -91,6 +109,13 @@ class NZBGet(DownloaderBase):
return True
def getAllDownloadStatus(self, ids):
""" Get status of all active downloads
:param ids: list of (mixed) downloader ids
Used to match the releases for this downloader as there could be
other downloaders active that it should ignore
:return: list of releases
"""
log.debug('Checking NZBGet download status.')
@@ -163,12 +188,12 @@ class NZBGet(DownloaderBase):
nzb_id = nzb['NZBID']
if nzb_id in ids:
log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (nzb['NZBFilename'] , nzb['ParStatus'], nzb['ScriptStatus'] , nzb['Log']))
log.debug('Found %s in NZBGet history. TotalStatus: %s, ParStatus: %s, ScriptStatus: %s, Log: %s', (nzb['NZBFilename'] , nzb['Status'], nzb['ParStatus'], nzb['ScriptStatus'] , nzb['Log']))
release_downloads.append({
'id': nzb_id,
'name': nzb['NZBFilename'],
'status': 'completed' if nzb['ParStatus'] in ['SUCCESS', 'NONE'] and nzb['ScriptStatus'] in ['SUCCESS', 'NONE'] else 'failed',
'original_status': nzb['ParStatus'] + ', ' + nzb['ScriptStatus'],
'status': 'completed' if 'SUCCESS' in nzb['Status'] else 'failed',
'original_status': nzb['Status'],
'timeleft': str(timedelta(seconds = 0)),
'folder': sp(nzb['DestDir'])
})
+25
View File
@@ -24,6 +24,20 @@ class NZBVortex(DownloaderBase):
session_id = None
def download(self, data = None, media = None, filedata = None):
""" Send a torrent/nzb file to the downloader
:param data: dict returned from provider
Contains the release information
:param media: media dict with information
Used for creating the filename when possible
:param filedata: downloaded torrent/nzb filedata
The file gets downloaded in the searcher and send to this function
This is done to have failed checking before using the downloader, so the downloader
doesn't need to worry about that
:return: boolean
One faile returns false, but the downloaded should log his own errors
"""
if not media: media = {}
if not data: data = {}
@@ -45,6 +59,10 @@ class NZBVortex(DownloaderBase):
return False
def test(self):
""" Check if connection works
:return: bool
"""
try:
login_result = self.login()
except:
@@ -53,6 +71,13 @@ class NZBVortex(DownloaderBase):
return login_result
def getAllDownloadStatus(self, ids):
""" Get status of all active downloads
:param ids: list of (mixed) downloader ids
Used to match the releases for this downloader as there could be
other downloaders active that it should ignore
:return: list of releases
"""
raw_statuses = self.call('nzb')
+18
View File
@@ -19,6 +19,20 @@ class Pneumatic(DownloaderBase):
status_support = False
def download(self, data = None, media = None, filedata = None):
""" Send a torrent/nzb file to the downloader
:param data: dict returned from provider
Contains the release information
:param media: media dict with information
Used for creating the filename when possible
:param filedata: downloaded torrent/nzb filedata
The file gets downloaded in the searcher and send to this function
This is done to have failed checking before using the downloader, so the downloader
doesn't need to worry about that
:return: boolean
One faile returns false, but the downloaded should log his own errors
"""
if not media: media = {}
if not data: data = {}
@@ -63,6 +77,10 @@ class Pneumatic(DownloaderBase):
return False
def test(self):
""" Check if connection works
:return: bool
"""
directory = self.conf('directory')
if directory and os.path.isdir(directory):
@@ -0,0 +1,68 @@
from .main import PutIO
def autoload():
return PutIO()
config = [{
'name': 'putio',
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'putio',
'label': 'put.io',
'description': 'This will start a torrent download on <a href="http://put.io">Put.io</a>.',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
'radio_group': 'torrent',
},
{
'name': 'oauth_token',
'label': 'oauth_token',
'description': 'This is the OAUTH_TOKEN from your putio API',
'advanced': True,
},
{
'name': 'folder',
'description': ('The folder on putio where you want the upload to go','Will find the first first folder that matches this name'),
'default': 0,
},
{
'name': 'callback_host',
'description': 'External reachable url to CP so put.io can do it\'s thing',
},
{
'name': 'download',
'description': 'Set this to have CouchPotato download the file from Put.io',
'type': 'bool',
'default': 0,
},
{
'name': 'delete_file',
'description': ('Set this to remove the file from putio after sucessful download','Does nothing if you don\'t select download'),
'type': 'bool',
'default': 0,
},
{
'name': 'download_dir',
'type': 'directory',
'label': 'Download Directory',
'description': 'The Directory to download files to, does nothing if you don\'t select download',
},
{
'name': 'manual',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
},
],
}
],
}]
+181
View File
@@ -0,0 +1,181 @@
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEventAsync
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
from pio import api as pio
import datetime
log = CPLog(__name__)
autoload = 'Putiodownload'
class PutIO(DownloaderBase):
protocol = ['torrent', 'torrent_magnet']
downloading_list = []
oauth_authenticate = 'https://api.couchpota.to/authorize/putio/'
def __init__(self):
addApiView('downloader.putio.getfrom', self.getFromPutio, docs = {
'desc': 'Allows you to download file from prom Put.io',
})
addApiView('downloader.putio.auth_url', self.getAuthorizationUrl)
addApiView('downloader.putio.credentials', self.getCredentials)
addEvent('putio.download', self.putioDownloader)
return super(PutIO, self).__init__()
# This is a recusive function to check for the folders
def recursionFolder(self, client, folder = 0, tfolder = ''):
files = client.File.list(folder)
for f in files:
if f.content_type == 'application/x-directory':
if f.name == tfolder:
return f.id
else:
result = self.recursionFolder(client, f.id, tfolder)
if result != 0:
return result
return 0
# This will check the root for the folder, and kick of recusively checking sub folder
def convertFolder(self, client, folder):
if folder == 0:
return 0
else:
return self.recursionFolder(client, 0, folder)
def download(self, data = None, media = None, filedata = None):
if not media: media = {}
if not data: data = {}
log.info('Sending "%s" to put.io', data.get('name'))
url = data.get('url')
client = pio.Client(self.conf('oauth_token'))
putioFolder = self.convertFolder(client, self.conf('folder'))
log.debug('putioFolder ID is %s', putioFolder)
# It might be possible to call getFromPutio from the renamer if we can then we don't need to do this.
# Note callback_host is NOT our address, it's the internet host that putio can call too
callbackurl = None
if self.conf('download'):
callbackurl = 'http://' + self.conf('callback_host') + '%sdownloader.putio.getfrom/' %Env.get('api_base'.strip('/'))
resp = client.Transfer.add_url(url, callback_url = callbackurl, parent_id = putioFolder)
log.debug('resp is %s', resp.id);
return self.downloadReturnId(resp.id)
def test(self):
try:
client = pio.Client(self.conf('oauth_token'))
if client.File.list():
return True
except:
log.info('Failed to get file listing, check OAUTH_TOKEN')
return False
def getAuthorizationUrl(self, host = None, **kwargs):
callback_url = cleanHost(host) + '%sdownloader.putio.credentials/' % (Env.get('api_base').lstrip('/'))
log.debug('callback_url is %s', callback_url)
target_url = self.oauth_authenticate + "?target=" + callback_url
log.debug('target_url is %s', target_url)
return {
'success': True,
'url': target_url,
}
def getCredentials(self, **kwargs):
try:
oauth_token = kwargs.get('oauth')
except:
return 'redirect', Env.get('web_base') + 'settings/downloaders/'
log.debug('oauth_token is: %s', oauth_token)
self.conf('oauth_token', value = oauth_token);
return 'redirect', Env.get('web_base') + 'settings/downloaders/'
def getAllDownloadStatus(self, ids):
log.debug('Checking putio download status.')
client = pio.Client(self.conf('oauth_token'))
transfers = client.Transfer.list()
log.debug(transfers);
release_downloads = ReleaseDownloadList(self)
for t in transfers:
if t.id in ids:
log.debug('downloading list is %s', self.downloading_list)
if t.status == "COMPLETED" and self.conf('download') == False :
status = 'completed'
# So check if we are trying to download something
elif t.status == "COMPLETED" and self.conf('download') == True:
# Assume we are done
status = 'completed'
if not self.downloading_list:
now = datetime.datetime.utcnow()
date_time = datetime.datetime.strptime(t.finished_at,"%Y-%m-%dT%H:%M:%S")
# We need to make sure a race condition didn't happen
if (now - date_time) < datetime.timedelta(minutes=5):
# 5 minutes haven't passed so we wait
status = 'busy'
else:
# If we have the file_id in the downloading_list mark it as busy
if str(t.file_id) in self.downloading_list:
status = 'busy'
else:
status = 'busy'
release_downloads.append({
'id' : t.id,
'name': t.name,
'status': status,
'timeleft': t.estimated_time,
})
return release_downloads
def putioDownloader(self, fid):
log.info('Put.io Real downloader called with file_id: %s',fid)
client = pio.Client(self.conf('oauth_token'))
log.debug('About to get file List')
putioFolder = self.convertFolder(client, self.conf('folder'))
log.debug('PutioFolderID is %s', putioFolder)
files = client.File.list(parent_id=putioFolder)
downloaddir = self.conf('download_dir')
for f in files:
if str(f.id) == str(fid):
client.File.download(f, dest = downloaddir, delete_after_download = self.conf('delete_file'))
# Once the download is complete we need to remove it from the running list.
self.downloading_list.remove(fid)
return True
def getFromPutio(self, **kwargs):
try:
file_id = str(kwargs.get('file_id'))
except:
return {
'success' : False,
}
log.info('Put.io Download has been called file_id is %s', file_id)
if file_id not in self.downloading_list:
self.downloading_list.append(file_id)
fireEventAsync('putio.download',fid = file_id)
return {
'success': True,
}
return {
'success': False,
}
@@ -0,0 +1,68 @@
var PutIODownloader = new Class({
initialize: function(){
var self = this;
App.addEvent('loadSettings', self.addRegisterButton.bind(self));
},
addRegisterButton: function(){
var self = this;
var setting_page = App.getPage('Settings');
setting_page.addEvent('create', function(){
var fieldset = setting_page.tabs.downloaders.groups.putio,
l = window.location;
var putio_set = 0;
fieldset.getElements('input[type=text]').each(function(el){
putio_set += +(el.get('value') != '');
});
new Element('.ctrlHolder').adopt(
// Unregister button
(putio_set > 0) ?
[
self.unregister = new Element('a.button.red', {
'text': 'Unregister "'+fieldset.getElement('input[name*=oauth_token]').get('value')+'"',
'events': {
'click': function(){
fieldset.getElements('input[name*=oauth_token]').set('value', '').fireEvent('change');
self.unregister.destroy();
self.unregister_or.destroy();
}
}
}),
self.unregister_or = new Element('span[text=or]')
]
: null,
// Register button
new Element('a.button', {
'text': putio_set > 0 ? 'Register a different account' : 'Register your put.io account',
'events': {
'click': function(){
Api.request('downloader.putio.auth_url', {
'data': {
'host': l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '')
},
'onComplete': function(json){
window.location = json.url;
}
});
}
}
})
).inject(fieldset.getElement('.test_button'), 'before');
})
}
});
window.addEvent('domready', function(){
new PutIODownloader();
});
@@ -41,12 +41,30 @@ class qBittorrent(DownloaderBase):
return self.qb
def test(self):
""" Check if connection works
:return: bool
"""
if self.connect():
return True
return False
def download(self, data = None, media = None, filedata = None):
""" Send a torrent/nzb file to the downloader
:param data: dict returned from provider
Contains the release information
:param media: media dict with information
Used for creating the filename when possible
:param filedata: downloaded torrent/nzb filedata
The file gets downloaded in the searcher and send to this function
This is done to have failed checking before using the downloader, so the downloader
doesn't need to worry about that
:return: boolean
One faile returns false, but the downloaded should log his own errors
"""
if not media: media = {}
if not data: data = {}
@@ -95,6 +113,14 @@ class qBittorrent(DownloaderBase):
return 'busy'
def getAllDownloadStatus(self, ids):
""" Get status of all active downloads
:param ids: list of (mixed) downloader ids
Used to match the releases for this downloader as there could be
other downloaders active that it should ignore
:return: list of releases
"""
log.debug('Checking qBittorrent download status.')
if not self.connect():
+26
View File
@@ -84,6 +84,10 @@ class rTorrent(DownloaderBase):
return self.rt
def test(self):
""" Check if connection works
:return: bool
"""
if self.connect(True):
return True
@@ -94,6 +98,20 @@ class rTorrent(DownloaderBase):
def download(self, data = None, media = None, filedata = None):
""" Send a torrent/nzb file to the downloader
:param data: dict returned from provider
Contains the release information
:param media: media dict with information
Used for creating the filename when possible
:param filedata: downloaded torrent/nzb filedata
The file gets downloaded in the searcher and send to this function
This is done to have failed checking before using the downloader, so the downloader
doesn't need to worry about that
:return: boolean
One faile returns false, but the downloaded should log his own errors
"""
if not media: media = {}
if not data: data = {}
@@ -161,6 +179,14 @@ class rTorrent(DownloaderBase):
return 'completed'
def getAllDownloadStatus(self, ids):
""" Get status of all active downloads
:param ids: list of (mixed) downloader ids
Used to match the releases for this downloader as there could be
other downloaders active that it should ignore
:return: list of releases
"""
log.debug('Checking rTorrent download status.')
if not self.connect():
+27
View File
@@ -21,6 +21,21 @@ class Sabnzbd(DownloaderBase):
protocol = ['nzb']
def download(self, data = None, media = None, filedata = None):
"""
Send a torrent/nzb file to the downloader
:param data: dict returned from provider
Contains the release information
:param media: media dict with information
Used for creating the filename when possible
:param filedata: downloaded torrent/nzb filedata
The file gets downloaded in the searcher and send to this function
This is done to have failed checking before using the downloader, so the downloader
doesn't need to worry about that
:return: boolean
One faile returns false, but the downloaded should log his own errors
"""
if not media: media = {}
if not data: data = {}
@@ -69,6 +84,11 @@ class Sabnzbd(DownloaderBase):
return False
def test(self):
""" Check if connection works
Return message if an old version of SAB is used
:return: bool
"""
try:
sab_data = self.call({
'mode': 'version',
@@ -89,6 +109,13 @@ class Sabnzbd(DownloaderBase):
return True
def getAllDownloadStatus(self, ids):
""" Get status of all active downloads
:param ids: list of (mixed) downloader ids
Used to match the releases for this downloader as there could be
other downloaders active that it should ignore
:return: list of releases
"""
log.debug('Checking SABnzbd download status.')
+20 -1
View File
@@ -19,6 +19,21 @@ class Synology(DownloaderBase):
status_support = False
def download(self, data = None, media = None, filedata = None):
"""
Send a torrent/nzb file to the downloader
:param data: dict returned from provider
Contains the release information
:param media: media dict with information
Used for creating the filename when possible
:param filedata: downloaded torrent/nzb filedata
The file gets downloaded in the searcher and send to this function
This is done to have failed checking before using the downloader, so the downloader
doesn't need to worry about that
:return: boolean
One faile returns false, but the downloaded should log his own errors
"""
if not media: media = {}
if not data: data = {}
@@ -50,6 +65,10 @@ class Synology(DownloaderBase):
return self.downloadReturnId('') if response else False
def test(self):
""" Check if connection works
:return: bool
"""
host = cleanHost(self.conf('host'), protocol = False).split(':')
try:
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
@@ -118,7 +137,7 @@ class SynologyRPC(object):
def _req(self, url, args, files = None):
response = {'success': False}
try:
req = requests.post(url, data = args, files = files)
req = requests.post(url, data = args, files = files, verify = False)
req.raise_for_status()
response = json.loads(req.text)
if response['success']:
@@ -34,6 +34,21 @@ class Transmission(DownloaderBase):
return self.trpc
def download(self, data = None, media = None, filedata = None):
"""
Send a torrent/nzb file to the downloader
:param data: dict returned from provider
Contains the release information
:param media: media dict with information
Used for creating the filename when possible
:param filedata: downloaded torrent/nzb filedata
The file gets downloaded in the searcher and send to this function
This is done to have failed checking before using the downloader, so the downloader
doesn't need to worry about that
:return: boolean
One faile returns false, but the downloaded should log his own errors
"""
if not media: media = {}
if not data: data = {}
@@ -88,11 +103,22 @@ class Transmission(DownloaderBase):
return self.downloadReturnId(data['hashString'])
def test(self):
""" Check if connection works
:return: bool
"""
if self.connect() and self.trpc.get_session():
return True
return False
def getAllDownloadStatus(self, ids):
""" Get status of all active downloads
:param ids: list of (mixed) downloader ids
Used to match the releases for this downloader as there could be
other downloaders active that it should ignore
:return: list of releases
"""
log.debug('Checking Transmission download status.')
@@ -121,6 +147,8 @@ class Transmission(DownloaderBase):
status = 'failed'
elif torrent['status'] == 0 and torrent['percentDone'] == 1:
status = 'completed'
elif torrent['status'] == 16 and torrent['percentDone'] == 1:
status = 'completed'
elif torrent['status'] in [5, 6]:
status = 'seeding'
+26
View File
@@ -51,6 +51,21 @@ class uTorrent(DownloaderBase):
return self.utorrent_api
def download(self, data = None, media = None, filedata = None):
"""
Send a torrent/nzb file to the downloader
:param data: dict returned from provider
Contains the release information
:param media: media dict with information
Used for creating the filename when possible
:param filedata: downloaded torrent/nzb filedata
The file gets downloaded in the searcher and send to this function
This is done to have failed checking before using the downloader, so the downloader
doesn't need to worry about that
:return: boolean
One faile returns false, but the downloaded should log his own errors
"""
if not media: media = {}
if not data: data = {}
@@ -120,6 +135,10 @@ class uTorrent(DownloaderBase):
return self.downloadReturnId(torrent_hash)
def test(self):
""" Check if connection works
:return: bool
"""
if self.connect():
build_version = self.utorrent_api.get_build()
if not build_version:
@@ -131,6 +150,13 @@ class uTorrent(DownloaderBase):
return False
def getAllDownloadStatus(self, ids):
""" Get status of all active downloads
:param ids: list of (mixed) downloader ids
Used to match the releases for this downloader as there could be
other downloaders active that it should ignore
:return: list of releases
"""
log.debug('Checking uTorrent download status.')
+8 -5
View File
@@ -37,15 +37,18 @@ def toUnicode(original, *args):
except:
try:
detected = detect(original)
if detected.get('encoding') == 'utf-8':
return original.decode('utf-8')
try:
if detected.get('confidence') > 0.8:
return original.decode(detected.get('encoding'))
except:
pass
return ek(original, *args)
except:
raise
except:
log.error('Unable to decode value "%s..." : %s ', (repr(original)[:20], traceback.format_exc()))
ascii_text = str(original).encode('string_escape')
return toUnicode(ascii_text)
return 'ERROR DECODING STRING'
def ss(original, *args):
@@ -92,7 +95,7 @@ def ek(original, *args):
if isinstance(original, (str, unicode)):
try:
from couchpotato.environment import Env
return original.decode(Env.get('encoding'))
return original.decode(Env.get('encoding'), 'ignore')
except UnicodeDecodeError:
raise
+11 -2
View File
@@ -1,9 +1,10 @@
import os
import traceback
from couchpotato import CPLog
from couchpotato import CPLog, md5
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import getExt
from couchpotato.core.plugins.base import Plugin
import six
@@ -92,7 +93,15 @@ class MediaBase(Plugin):
if not isinstance(image, (str, unicode)):
continue
if file_type not in existing_files or len(existing_files.get(file_type, [])) == 0:
# Check if it has top image
filename = '%s.%s' % (md5(image), getExt(image))
existing = existing_files.get(file_type, [])
has_latest = False
for x in existing:
if filename in x:
has_latest = True
if not has_latest or file_type not in existing_files or len(existing_files.get(file_type, [])) == 0:
file_path = fireEvent('file.download', url = image, single = True)
if file_path:
existing_files[file_type] = [toUnicode(file_path)]
+12 -27
View File
@@ -273,6 +273,10 @@ class MediaPlugin(MediaBase):
for x in filter_by:
media_ids = [n for n in media_ids if n in filter_by[x]]
total_count = len(media_ids)
if total_count == 0:
return 0, []
offset = 0
limit = -1
if limit_offset:
@@ -302,30 +306,11 @@ class MediaPlugin(MediaBase):
media_ids.remove(media_id)
if len(media_ids) == 0 or len(medias) == limit: break
# Sort media by type and return result
result = {}
# Create keys for media types we are listing
if types:
for media_type in types:
result['%ss' % media_type] = []
else:
for media_type in fireEvent('media.types', merge = True):
result['%ss' % media_type] = []
total_count = len(medias)
if total_count == 0:
return 0, result
for kind in medias:
result['%ss' % kind['type']].append(kind)
return total_count, result
return total_count, medias
def listView(self, **kwargs):
total_count, result = self.list(
total_movies, movies = self.list(
types = splitString(kwargs.get('type')),
status = splitString(kwargs.get('status')),
release_status = splitString(kwargs.get('release_status')),
@@ -336,12 +321,12 @@ class MediaPlugin(MediaBase):
search = kwargs.get('search')
)
results = result
results['success'] = True
results['empty'] = len(result) == 0
results['total'] = total_count
return results
return {
'success': True,
'empty': len(movies) == 0,
'total': total_movies,
'movies': movies,
}
def addSingleListView(self):
@@ -5,6 +5,11 @@ import time
import traceback
import xml.etree.ElementTree as XMLTree
try:
from xml.etree.ElementTree import ParseError as XmlParseError
except ImportError:
from xml.parsers.expat import ExpatError as XmlParseError
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.variable import tryFloat, mergeDicts, md5, \
@@ -94,6 +99,8 @@ class Provider(Plugin):
try:
data = XMLTree.fromstring(ss(data))
return self.getElements(data, item_path)
except XmlParseError:
log.error('Invalid XML returned, check "%s" manually for issues', url)
except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
@@ -68,8 +68,12 @@ class Base(NZBProvider, RSS):
if not date:
date = self.getTextElement(nzb, 'pubDate')
nzb_id = self.getTextElement(nzb, 'guid').split('/')[-1:].pop()
name = self.getTextElement(nzb, 'title')
detail_url = self.getTextElement(nzb, 'guid')
nzb_id = detail_url.split('/')[-1:].pop()
if '://' not in detail_url:
detail_url = (cleanHost(host['host']) + self.urls['detail']) % tryUrlencode(nzb_id)
if not name:
continue
@@ -103,7 +107,7 @@ class Base(NZBProvider, RSS):
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024,
'url': ((self.getUrl(host['host']) + self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host),
'detail_url': (cleanHost(host['host']) + self.urls['detail']) % tryUrlencode(nzb_id),
'detail_url': detail_url,
'content': self.getTextElement(nzb, 'description'),
'description': description,
'score': host['extra_score'],
@@ -183,7 +187,7 @@ class Base(NZBProvider, RSS):
return 'try_next'
try:
data = self.urlopen(url, show_error = False)
data = self.urlopen(url, show_error = False, headers = {'User-Agent': Env.getIdentifier()})
self.limits_reached[host] = False
return data
except HTTPError as e:
@@ -1,13 +1,9 @@
from urlparse import urlparse, parse_qs
import time
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.nzb.base import NZBProvider
from dateutil.parser import parse
log = CPLog(__name__)
@@ -16,27 +12,19 @@ log = CPLog(__name__)
class Base(NZBProvider, RSS):
urls = {
'search': 'https://rss.omgwtfnzbs.org/rss-search.php?%s',
'detail_url': 'https://omgwtfnzbs.org/details.php?id=%s',
'search': 'https://api.omgwtfnzbs.org/json/?%s',
}
http_time_between_calls = 1 # Seconds
cat_ids = [
([15], ['dvdrip']),
([15], ['dvdrip', 'scr', 'r5', 'tc', 'ts', 'cam']),
([15, 16], ['brrip']),
([16], ['720p', '1080p', 'bd50']),
([17], ['dvdr']),
]
cat_backup_id = 'movie'
def search(self, movie, quality):
if quality['identifier'] in fireEvent('quality.pre_releases', single = True):
return []
return super(Base, self).search(movie, quality)
def _searchOnTitle(self, title, movie, quality, results):
q = '%s %s' % (title, movie['info']['year'])
@@ -47,22 +35,20 @@ class Base(NZBProvider, RSS):
'api': self.conf('api_key', default = ''),
})
nzbs = self.getRSSData(self.urls['search'] % params)
nzbs = self.getJsonData(self.urls['search'] % params)
for nzb in nzbs:
if isinstance(nzbs, list):
for nzb in nzbs:
enclosure = self.getElement(nzb, 'enclosure').attrib
nzb_id = parse_qs(urlparse(self.getTextElement(nzb, 'link')).query).get('id')[0]
results.append({
'id': nzb_id,
'name': toUnicode(self.getTextElement(nzb, 'title')),
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, 'pubDate')).timetuple()))),
'size': tryInt(enclosure['length']) / 1024 / 1024,
'url': enclosure['url'],
'detail_url': self.urls['detail_url'] % nzb_id,
'description': self.getTextElement(nzb, 'description')
})
results.append({
'id': nzb.get('nzbid'),
'name': toUnicode(nzb.get('release')),
'age': self.calculateAge(tryInt(nzb.get('usenetage'))),
'size': tryInt(nzb.get('sizebytes')) / 1024 / 1024,
'url': nzb.get('getnzb'),
'detail_url': nzb.get('details'),
'description': nzb.get('weblink')
})
config = [{
@@ -13,11 +13,11 @@ log = CPLog(__name__)
class Base(TorrentProvider):
urls = {
'test': 'http://www.bit-hdtv.com/',
'login': 'http://www.bit-hdtv.com/takelogin.php',
'login_check': 'http://www.bit-hdtv.com/messages.php',
'detail': 'http://www.bit-hdtv.com/details.php?id=%s',
'search': 'http://www.bit-hdtv.com/torrents.php?',
'test': 'https://www.bit-hdtv.com/',
'login': 'https://www.bit-hdtv.com/takelogin.php',
'login_check': 'https://www.bit-hdtv.com/messages.php',
'detail': 'https://www.bit-hdtv.com/details.php?id=%s',
'search': 'https://www.bit-hdtv.com/torrents.php?',
}
# Searches for movies only - BiT-HDTV's subcategory and resolution search filters appear to be broken
@@ -93,7 +93,7 @@ config = [{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'BiT-HDTV',
'description': '<a href="http://bit-hdtv.com">BiT-HDTV</a>',
'description': '<a href="https://bit-hdtv.com">BiT-HDTV</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAABMklEQVR4AZ3Qu0ojcQCF8W9MJcQbJNgEEQUbQVIqWgnaWfkIvoCgggixEAmIhRtY2GV3w7KwU61B0EYIxmiw0YCik84ipaCuc0nmP5dcjIUgOjqDvxf4OAdf9mnMLcUJyPyGSCP+YRdC+Kp8iagJKhuS+InYRhTGgDbeV2uEMand4ZRxizjXHQEimxhraAnUr73BNqQxMiNeV2SwcjTLEVtb4Zl10mXutvOWm2otw5Sxz6TGTbdd6ncuYvVLXAXrvM+ruyBpy1S3JLGDfUQ1O6jn5vTsrJXvqSt4UNfj6vxTRPxBHER5QeSirhLGk/5rWN+ffB1XZuxjnDy1q87m7TS+xOGA+Iv4gfkbaw+nOMXHDHnITGEk0VfRFnn4Po4vNYm6RGukmggR0L08+l+e4HMeASo/i6AJUjLgAAAAAElFTkSuQmCC',
'options': [
@@ -0,0 +1,130 @@
import re
import traceback
from couchpotato.core.helpers.variable import tryInt, getIdentifier
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
log = CPLog(__name__)
class Base(TorrentProvider):
urls = {
'test': 'https://hdaccess.net/',
'detail': 'https://hdaccess.net/details.php?id=%s',
'search': 'https://hdaccess.net/searchapi.php?apikey=%s&username=%s&imdbid=%s&internal=%s',
'download': 'https://hdaccess.net/grab.php?torrent=%s&apikey=%s',
}
http_time_between_calls = 1 # Seconds
def _search(self, movie, quality, results):
data = self.getJsonData(self.urls['search'] % (self.conf('apikey'), self.conf('username'), getIdentifier(movie), self.conf('internal_only')))
if data:
try:
#for result in data[]:
for key, result in data.iteritems():
if tryInt(result['total_results']) == 0:
return
torrentscore = self.conf('extra_score')
releasegroup = result['releasegroup']
resolution = result['resolution']
encoding = result['encoding']
freeleech = tryInt(result['freeleech'])
seeders = tryInt(result['seeders'])
torrent_desc = '/ %s / %s / %s / %s seeders' % (releasegroup, resolution, encoding, seeders)
if freeleech > 0 and self.conf('prefer_internal'):
torrent_desc += '/ Internal'
torrentscore += 200
if seeders == 0:
torrentscore = 0
name = result['release_name']
year = tryInt(result['year'])
results.append({
'id': tryInt(result['torrentid']),
'name': re.sub('[^A-Za-z0-9\-_ \(\).]+', '', '%s (%s) %s' % (name, year, torrent_desc)),
'url': self.urls['download'] % (result['torrentid'], self.conf('apikey')),
'detail_url': self.urls['detail'] % result['torrentid'],
'size': tryInt(result['size']),
'seeders': tryInt(result['seeders']),
'leechers': tryInt(result['leechers']),
'age': tryInt(result['age']),
'score': torrentscore
})
except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
config = [{
'name': 'hdaccess',
'groups': [
{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'HDAccess',
'wizard': True,
'description': '<a href="https://hdaccess.net">HDAccess</a>',
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAADuUlEQVQ4yz3T209bdQAH8O/vnNNzWno5FIpAKZdSLi23gWMDtumWuSXOyzJj9M1kyIOPS1xiYuKe9GUPezZZnGIiMTqTxS1bdIuYkG2MWKBAKYVszOgKFkrbA+259HfO+fli/PwPHzI+Pg5CCEAI2VcUlEsl1tHdU7P5bGOkWChEaaUCwvHpmkD93POn6bwgCMQGAMYYYwyCruuQnE7SPzjIstvb8l+bm5fXkokJSmlQEkUQAIpSRH5vd0tyum7I/sA1Z5VH2ctmiGWZjHw4McE1NAZtQ9fD25kXt1VN7es7dNjuGRjiJFeVpWo6slsZPhF/Ys/PPeIs2056ff7zIOS5rpU5/viJEwwEnu3Mi18dojjw0aWP6amz57h9RSE/35zinq2nuGjvIQwOj7K2SKeZWkk0auXSSZ+/ZopSy+CbW1pQKpWu6Jr2/qVPPqWRjm6HWi6Tm999g3RyGbndLCqGgVBrO3F7fHykK0YX47NNtGLYlBq/c+H2iD+3k704dHQUDcFmQVXLyP6zhfTqCl45fQYjx17FemoJunoAk1bQFGoVhkdPwNC0ix2dMT+3llodM02rKdo7gN3dHAEhuH/vNgDg3Pl3cPaNt2GZJpYX5lBbFwClBukfGobL5WrayW6NccVCISY4HIQxYts2Q3J5CXOPHuLlo6NoCoXQ2hbG0JFRpJYWcVDIQ5ZlyL5qW5b9hNlWjKsYBgzDgKppMCoGHty7A0orOHbyNNweL+obGnDm9TdhWSYS8Vn4a2shOZ0QJRGSKIHjeGGtWNhjqqpyG+k04k8eozPai9ZwByavf4kfpyZxZGwMfYOHsbwQx34hB5dL4syKweRq/xpXHwzNapqWSSYWMDszzYqFPEaOn4KiKJiZfoCZ6d8Am+GtC++iXCpjaf4P9vefT8HzfKarp3eWRKMxCILwuWXSz977YIK2RTodDoGH1+OG1+tDlbsKkuiAJEngeWBjNUUnv7rucIiOLyzTvMKJTgnVtbVXLctK3L31g+NAUajL5bEptaDpOnTdgGkzVHl9drms0ju3fnJIkphoaQtfbQiFwAcCAY5wnCE5Xff3i8XX4o9nGksH+8zl9hAGZlWMCivkc9z0L3fZ999+LTCGZKi55YJTFHfye3sc6e/vB88LpK6+iWlqSS4WcpcNXZtwOp3B6mo/REmCSSkEgd+qq3vpRkt75Fp9Y1BZWZwnhq4zEovF/u/MATAti4U7umvyu9kR27aikihC9vvTnV2xufVUMu/2uIksy/9tZvgX49fLmAMx3bsAAAAASUVORK5CYII=',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
{
'name': 'username',
'default': '',
'description': 'Enter your site username.',
},
{
'name': 'apikey',
'default': '',
'label': 'API Key',
'description': 'Enter your site api key. This can be find on <a href="https://hdaccess.net/usercp.php?action=security">Profile Security</a>',
},
{
'name': 'seed_ratio',
'label': 'Seed ratio',
'type': 'float',
'default': 0,
'description': 'Will not be (re)moved until this seed ratio is met. HDAccess minimum is 1:1.',
},
{
'name': 'seed_time',
'label': 'Seed time',
'type': 'int',
'default': 0,
'description': 'Will not be (re)moved until this seed time (in hours) is met. HDAccess minimum is 48 hours.',
},
{
'name': 'prefer_internal',
'advanced': True,
'type': 'bool',
'default': 1,
'description': 'Favors internal releases over non-internal releases.',
},
{
'name': 'internal_only',
'advanced': True,
'label': 'Internal Only',
'type': 'bool',
'default': False,
'description': 'Only download releases marked as HDAccess internal',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
},
],
}]
@@ -29,6 +29,9 @@ class Base(TorrentProvider):
}
post_data.update(params)
if self.conf('internal_only'):
post_data.update({'origin': [1]})
try:
result = self.getJsonData(self.urls['api'], data = json.dumps(post_data))
@@ -110,6 +113,14 @@ config = [{
'default': 0,
'description': 'Starting score for each release found via this provider.',
},
{
'name': 'internal_only',
'advanced': True,
'label': 'Internal Only',
'type': 'bool',
'default': False,
'description': 'Only download releases marked as HDBits internal'
}
],
},
],
@@ -14,11 +14,11 @@ log = CPLog(__name__)
class Base(TorrentProvider):
urls = {
'test': 'https://www.iptorrents.com/',
'base_url': 'https://www.iptorrents.com',
'login': 'https://www.iptorrents.com/torrents/',
'login_check': 'https://www.iptorrents.com/inbox.php',
'search': 'https://www.iptorrents.com/torrents/?%s%%s&q=%s&qf=ti&p=%%d',
'test': 'https://iptorrents.eu/',
'base_url': 'https://iptorrents.eu',
'login': 'https://iptorrents.eu/torrents/',
'login_check': 'https://iptorrents.eu/inbox.php',
'search': 'https://iptorrents.eu/torrents/?%s%%s&q=%s&qf=ti&p=%%d',
}
http_time_between_calls = 1 # Seconds
@@ -120,7 +120,7 @@ config = [{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'IPTorrents',
'description': '<a href="http://www.iptorrents.com">IPTorrents</a>',
'description': '<a href="https://iptorrents.eu">IPTorrents</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABRklEQVR42qWQO0vDUBiG8zeKY3EqQUtNO7g0J6ZJ1+ifKIIFQXAqDYKCyaaYxM3udrZLHdRFhXrZ6liCW6mubfk874EESgqaeOCF7/Y8hEh41aq6yZi2nyZgBGya9XKtZs4No05pAkZV2YbEmyMMsoSxLQeC46wCTdPPY4HruPQyGIhF97qLWsS78Miydn4XdK46NJ9OsQAYBzMIMf8MQ9wtCnTdWCaIDx/u7uljOIQEe0hiIWPamSTLay3+RxOCSPI9+RJAo7Er9r2bnqjBFAqyK+VyK4f5/Cr5ni8OFKVCz49PFI5GdNvvU7ttE1M1zMU+8AMqFksEhrMnQsBDzqmDAwzx2ehRLwT7yyCI+vSC99c3mozH1NxrJgWWtR1BOECfEJSVCm6WCzJGCA7+IWhBsM4zywDPwEp4vCjx2DzBH2ODAfsDb33Ps6dQwJgAAAAASUVORK5CYII=',
'options': [
@@ -42,6 +42,7 @@ class Base(TorrentProvider):
link = result.find('td', attrs = {'class': 'ttr_name'}).find('a')
url = result.find('td', attrs = {'class': 'td_dl'}).find('a')
seeders = result.find('td', attrs = {'class': 'ttr_seeders'}).find('a')
leechers = result.find('td', attrs = {'class': 'ttr_leechers'}).find('a')
torrent_id = link['href'].replace('details?id=', '')
@@ -51,7 +52,7 @@ class Base(TorrentProvider):
'url': self.urls['download'] % url['href'],
'detail_url': self.urls['detail'] % torrent_id,
'size': self.parseSize(result.find('td', attrs = {'class': 'ttr_size'}).contents[0]),
'seeders': tryInt(result.find('td', attrs = {'class': 'ttr_seeders'}).find('a').string),
'seeders': tryInt(seeders.string) if seeders else 0,
'leechers': tryInt(leechers.string) if leechers else 0,
'get_more_info': self.getMoreInfo,
})
@@ -1,7 +1,7 @@
import traceback
from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
@@ -56,11 +56,12 @@ class Base(TorrentProvider):
full_id = link['href'].replace('details.php?id=', '')
torrent_id = full_id[:6]
name = toUnicode(link.get('title', link.contents[0]).encode('ISO-8859-1')).strip()
results.append({
'id': torrent_id,
'name': link.contents[0],
'url': self.urls['download'] % (torrent_id, link.contents[0]),
'name': name,
'url': self.urls['download'] % (torrent_id, name),
'detail_url': self.urls['detail'] % torrent_id,
'size': self.parseSize(cells[6].contents[0] + cells[6].contents[2]),
'seeders': tryInt(cells[8].find('span').contents[0]),
@@ -1,3 +1,4 @@
import re
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
@@ -8,12 +9,12 @@ log = CPLog(__name__)
class Base(TorrentProvider):
urls = {
'test': 'http://www.td.af/',
'login': 'http://www.td.af/torrents/',
'login_check': 'http://www.torrentday.com/userdetails.php',
'detail': 'http://www.td.af/details.php?id=%s',
'search': 'http://www.td.af/V3/API/API.php',
'download': 'http://www.td.af/download.php/%s/%s',
'test': 'https://torrentday.eu/',
'login': 'https://torrentday.eu/torrents/',
'login_check': 'https://torrentday.eu/userdetails.php',
'detail': 'https://torrentday.eu/details.php?id=%s',
'search': 'https://torrentday.eu/V3/API/API.php',
'download': 'https://torrentday.eu/download.php/%s/%s',
}
http_time_between_calls = 1 # Seconds
@@ -55,6 +56,10 @@ class Base(TorrentProvider):
}
def loginSuccess(self, output):
often = re.search('You tried too often, please wait .*</div>', output)
if often:
raise Exception(often.group(0)[:-6].strip())
return 'Password not correct' not in output
def loginCheckSuccess(self, output):
@@ -68,7 +73,7 @@ config = [{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'TorrentDay',
'description': '<a href="http://www.td.af/">TorrentDay</a>',
'description': '<a href="https://torrentday.eu/">TorrentDay</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC5ElEQVQ4y12TXUgUURTH//fO7Di7foeQJH6gEEEIZZllVohfSG/6UA+RSFAQQj74VA8+Bj30lmAlRVSEvZRfhNhaka5ZUG1paKaW39tq5O6Ou+PM3M4o6m6X+XPPzD3zm/+dcy574r515WfIW8CZBM4YAA5Gc/aQC3yd7oXYEONcsISE5dTDh91HS0t7FEWhBUAeN9ynV/d9qJAgE4AECURAcVsGlCCnly26LMA0IQwTa52dje3d3e3hcPi8qqrrMjcVYI3EHCQZlkFOHBwR2QHh2ASAAIJxWGAQEDxjePhs3527XjJwnb37OHBq0T+Tyyjh+9KnEzNJ7nouc1Q/3A3HGsOvnJy+PSUlj81w2Lny9WuJ6+3AmTjD4HOcrdR2dWXLRQePvyaSLfQOPMPC8mC9iHCsOxSyzJCelzdSXlNzD5ujpb25Wbfc/XXJemTXF4+nnCNq+AMLe50uFfEJTiw4GXSFtiHL0SnIq66+p0kSArqO+eH3RdsAv9+f5vW7L7GICq6rmM8XBCAXlBw90rOyxibn5yzfkg/L09M52/jxqdESaIrBXHYZZbB1GX8cEpySxKIB8S5XcOnvqpli1zuwmrTtoLjw5LOK/eeuWsE4JH5IRPaPZKiKigmPp+5pa+u1aEjIMhEgrRkmi9mgxGUhM7LNJSzOzsE3+cOeExovXOjdytE0LV4zqNZUtV0uZzAGoGkhDH/2YHZiErmv4uyWQnZZWc+hoqL3WzlTExN5hhA8IEwkZWZOxwB++30YG/9GkYCPvqAaHAW5uWPROW86OmqCprUR7z1yZDAGQNuCvkoB/baIKUBWMTYymv+gra3eJNvjXu+B562tFyXqTJ6YuHK8rKwvBmC3vR7cOCPQLWFz8LnfXWUrJo9U19BwMyUlJRjTSMJ2ENxUiGxq9KXQfwqYlnWstvbR5aamG9g0uzM8Q4OFt++3NNixQ2NgYmeN03FOTUv7XVpV9aKisvLl1vN/WVhNc/Fi1NEAAAAASUVORK5CYII=',
'options': [
@@ -0,0 +1,126 @@
import traceback
from bs4 import BeautifulSoup
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
import six
log = CPLog(__name__)
class Base(TorrentProvider):
urls = {
'test': 'https://www.torrentleech.org/',
'login': 'https://www.torrentleech.org/user/account/login/',
'login_check': 'https://torrentleech.org/user/messages',
'detail': 'https://www.torrentleech.org/torrent/%s',
'search': 'https://www.torrentleech.org/torrents/browse/index/query/%s/categories/%s',
'download': 'https://www.torrentleech.org%s',
}
http_time_between_calls = 1 # Seconds
cat_backup_id = None
def _searchOnTitle(self, title, media, quality, results):
url = self.urls['search'] % self.buildUrl(title, media, quality)
data = self.getHTMLData(url)
if data:
html = BeautifulSoup(data)
try:
result_table = html.find('table', attrs = {'id': 'torrenttable'})
if not result_table:
return
entries = result_table.find_all('tr')
for result in entries[1:]:
link = result.find('td', attrs = {'class': 'name'}).find('a')
url = result.find('td', attrs = {'class': 'quickdownload'}).find('a')
details = result.find('td', attrs = {'class': 'name'}).find('a')
results.append({
'id': link['href'].replace('/torrent/', ''),
'name': six.text_type(link.string),
'url': self.urls['download'] % url['href'],
'detail_url': self.urls['download'] % details['href'],
'size': self.parseSize(result.find_all('td')[4].string),
'seeders': tryInt(result.find('td', attrs = {'class': 'seeders'}).string),
'leechers': tryInt(result.find('td', attrs = {'class': 'leechers'}).string),
})
except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
def getLoginParams(self):
return {
'username': self.conf('username'),
'password': self.conf('password'),
'remember_me': 'on',
'login': 'submit',
}
def loginSuccess(self, output):
return '/user/account/logout' in output.lower() or 'welcome back' in output.lower()
loginCheckSuccess = loginSuccess
config = [{
'name': 'torrentleech',
'groups': [
{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'TorrentLeech',
'description': '<a href="http://torrentleech.org">TorrentLeech</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACHUlEQVR4AZVSO48SYRSdGTCBEMKzILLAWiybkKAGMZRUUJEoDZX7B9zsbuQPYEEjNLTQkYgJDwsoSaxspEBsCITXjjNAIKi8AkzceXgmbHQ1NJ5iMufmO9/9zrmXlCSJ+B8o75J8Pp/NZj0eTzweBy0Wi4PBYD6f12o1r9ebTCZx+22HcrnMsuxms7m6urTZ7LPZDMVYLBZ8ZV3yo8aq9Pq0wzCMTqe77dDv9y8uLyAWBH6xWOyL0K/56fcb+rrPgPZ6PZfLRe1fsl6vCUmGKIqoqNXqdDr9Dbjps9znUV0uTqdTjuPkDoVCIfcuJ4gizjMMm8u9vW+1nr04czqdK56c37CbKY9j2+1WEARZ0Gq1RFHAz2q1qlQqXxoN69HRcDjUarW8ZD6QUigUOnY8uKYH8N1sNkul9yiGw+F6vS4Rxn8EsodEIqHRaOSnq9T7ajQazWQycEIR1AEBYDabSZJyHDucJyegwWBQr9ebTCaKvHd4cCQANUU9evwQ1Ofz4YvUKUI43GE8HouSiFiNRhOowWBIpVLyHITJkuW3PwgAEf3pgIwxF5r+OplMEsk3CPT5szCMnY7EwUdhwUh/CXiej0Qi3idPz89fdrpdbsfBzH7S3Q9K5pP4c0sAKpVKoVAQGO1ut+t0OoFAQHkH2Da/3/+but3uarWK0ZMQoNdyucRutdttmqZxMTzY7XaYxsrgtUjEZrNhkSwWyy/0NCatZumrNQAAAABJRU5ErkJggg==',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
{
'name': 'username',
'default': '',
},
{
'name': 'password',
'default': '',
'type': 'password',
},
{
'name': 'seed_ratio',
'label': 'Seed ratio',
'type': 'float',
'default': 1,
'description': 'Will not be (re)moved until this seed ratio is met.',
},
{
'name': 'seed_time',
'label': 'Seed time',
'type': 'int',
'default': 40,
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 20,
'description': 'Starting score for each release found via this provider.',
}
],
},
],
}]
@@ -13,12 +13,12 @@ log = CPLog(__name__)
class Base(TorrentProvider):
urls = {
'test': 'http://torrentshack.eu/',
'login': 'http://torrentshack.eu/login.php',
'login_check': 'http://torrentshack.eu/inbox.php',
'detail': 'http://torrentshack.eu/torrent/%s',
'search': 'http://torrentshack.eu/torrents.php?action=advanced&searchstr=%s&scene=%s&filter_cat[%d]=1',
'download': 'http://torrentshack.eu/%s',
'test': 'https://torrentshack.me/',
'login': 'https://torrentshack.me/login.php',
'login_check': 'https://torrentshack.me/inbox.php',
'detail': 'https://torrentshack.me/torrent/%s',
'search': 'https://torrentshack.me/torrents.php?action=advanced&searchstr=%s&scene=%s&filter_cat[%d]=1',
'download': 'https://torrentshack.me/%s',
}
http_time_between_calls = 1 # Seconds
@@ -42,6 +42,7 @@ class Base(TorrentProvider):
link = result.find('span', attrs = {'class': 'torrent_name_link'}).parent
url = result.find('td', attrs = {'class': 'torrent_td'}).find('a')
size = result.find('td', attrs = {'class': 'size'}).contents[0].strip('\n ')
tds = result.find_all('td')
results.append({
@@ -49,7 +50,7 @@ class Base(TorrentProvider):
'name': six.text_type(link.span.string).translate({ord(six.u('\xad')): None}),
'url': self.urls['download'] % url['href'],
'detail_url': self.urls['download'] % link['href'],
'size': self.parseSize(result.find_all('td')[5].string),
'size': self.parseSize(size),
'seeders': tryInt(tds[len(tds)-2].string),
'leechers': tryInt(tds[len(tds)-1].string),
})
@@ -81,7 +82,7 @@ config = [{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'TorrentShack',
'description': '<a href="http://torrentshack.eu/">TorrentShack</a>',
'description': '<a href="https://torrentshack.me/">TorrentShack</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABmElEQVQoFQXBzY2cVRiE0afqvd84CQiAnxWWtyxsS6ThINBYg2Dc7mZBMEjE4mzs6e9WcY5+ePNuVFJJodQAoLo+SaWCy9rcV8cmjah3CI6iYu7oRU30kE5xxELRfamklY3k1NL19sSm7vPzP/ZdNZzKVDaY2sPZJBh9fv5ITrmG2+Vp4e1sPchVqTCQZJnVXi+/L4uuAJGly1+Pw8CprLbi8Om7tbT19/XRqJUk11JP9uHj9ulxhXbvJbI9qJvr5YkGXFG2IBT8tXczt+sfzDZCp3765f3t9tHEHGEDACma77+8o4oATKk+/PfW9YmHruRFjWoVSFsVsGu1YSKq6Oc37+n98unPZSRlY7vsKDqN+92X3yR9+PdXee3iJNKMStqdcZqoTJbUSi5JOkpfRlhSI0mSpEmCFKoU7FqSNOLAk54uGwCStMUCgLrVic62g7oDoFmmdI+P3S0pDe1xvDqb6XrZqbtzShWNoh9fv/XQHaDdM9OqrZi2M7M3UrB2vlkPS1IbdEBk7UiSoD6VlZ6aKWer4aH4f/AvKoHUTjuyAAAAAElFTkSuQmCC',
'options': [
@@ -22,12 +22,12 @@ class Base(TorrentMagnetProvider, RSS):
http_time_between_calls = 0
def _search(self, media, quality, results):
def _searchOnTitle(self, title, media, quality, results):
search_url = self.urls['verified_search'] if self.conf('verified_only') else self.urls['search']
# Create search parameters
search_params = self.buildUrl(media)
search_params = self.buildUrl(title, media, quality)
smin = quality.get('size_min')
smax = quality.get('size_max')
@@ -2,28 +2,25 @@ import traceback
from couchpotato.core.helpers.variable import tryInt, getIdentifier
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
log = CPLog(__name__)
class Base(TorrentMagnetProvider):
class Base(TorrentProvider):
urls = {
'test': '%s/api',
'search': '%s/api/list.json?keywords=%s&quality=%s',
'detail': '%s/api/movie.json?id=%s'
'test': '%s/api/v2',
'search': '%s/api/v2/list_movies.json?limit=50&query_term=%s'
}
http_time_between_calls = 1 # seconds
proxy_list = [
'http://yify.unlocktorrent.com',
'http://yify-torrents.com.come.in',
'http://yts.re',
'http://yts.im'
'http://yify-torrents.im',
'https://yts.re',
'https://yts.wf',
'https://yts.im',
]
def search(self, movie, quality):
@@ -39,28 +36,31 @@ class Base(TorrentMagnetProvider):
if not domain:
return
search_url = self.urls['search'] % (domain, getIdentifier(movie), quality['identifier'])
search_url = self.urls['search'] % (domain, getIdentifier(movie))
data = self.getJsonData(search_url)
data = self.getJsonData(search_url) or {}
data = data.get('data')
if data and data.get('MovieList'):
if isinstance(data, dict) and data.get('movies'):
try:
for result in data.get('MovieList'):
for result in data.get('movies'):
if result['Quality'] and result['Quality'] not in result['MovieTitle']:
title = result['MovieTitle'] + ' BrRip ' + result['Quality']
else:
title = result['MovieTitle'] + ' BrRip'
for release in result.get('torrents', []):
results.append({
'id': result['MovieID'],
'name': title,
'url': result['TorrentMagnetUrl'],
'detail_url': self.urls['detail'] % (domain, result['MovieID']),
'size': self.parseSize(result['Size']),
'seeders': tryInt(result['TorrentSeeds']),
'leechers': tryInt(result['TorrentPeers']),
})
if release['quality'] and release['quality'] not in result['title_long']:
title = result['title_long'] + ' BRRip ' + release['quality']
else:
title = result['title_long'] + ' BRRip'
results.append({
'id': release['hash'],
'name': title,
'url': release['url'],
'detail_url': result['url'],
'size': self.parseSize(release['size']),
'seeders': tryInt(release['seeds']),
'leechers': tryInt(release['peers']),
})
except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
+1 -1
View File
@@ -65,7 +65,7 @@ class MovieBase(MovieTypeBase):
return False
elif not params.get('info'):
try:
is_movie = fireEvent('movie.is_movie', identifier = params.get('identifier'), single = True)
is_movie = fireEvent('movie.is_movie', identifier = params.get('identifier'), adding = True, single = True)
if not is_movie:
msg = 'Can\'t add movie, seems to be a TV show.'
log.error(msg)
@@ -696,7 +696,7 @@ MA.Readd = new Class({
if(movie_done || snatched && snatched > 0)
self.el = new Element('a.readd', {
'title': 'Readd the movie and mark all previous snatched/downloaded as ignored',
'title': 'Re-add the movie and mark all previous snatched/downloaded as ignored',
'events': {
'click': self.doReadd.bind(self)
}
@@ -44,11 +44,12 @@ var Charts = new Class({
if( Cookie.read('suggestions_charts_menu_selected') === 'charts'){
self.show();
self.fireEvent.delay(0, self, 'created');
}
else
self.el.hide();
self.fireEvent.delay(0, self, 'created');
},
fill: function(json){
@@ -0,0 +1,89 @@
import re
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import tryInt, splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.media.movie.providers.automation.base import Automation
log = CPLog(__name__)
autoload = 'CrowdAI'
class CrowdAI(Automation, RSS):
interval = 1800
def getIMDBids(self):
movies = []
urls = dict(zip(splitString(self.conf('automation_urls')), [tryInt(x) for x in splitString(self.conf('automation_urls_use'))]))
for url in urls:
if not urls[url]:
continue
rss_movies = self.getRSSData(url)
for movie in rss_movies:
description = self.getTextElement(movie, 'description')
grabs = 0
for item in movie:
if item.attrib.get('name') == 'grabs':
grabs = item.attrib.get('value')
break
if int(grabs) > tryInt(self.conf('number_grabs')):
title = re.match(r'.*Title: .a href.*/">(.*) \(\d{4}\).*', description).group(1)
log.info2('%s grabs for movie: %s, enqueue...', (grabs, title))
year = re.match(r'.*Year: (\d{4}).*', description).group(1)
imdb = self.search(title, year)
if imdb and self.isMinimalMovie(imdb):
movies.append(imdb['imdb'])
return movies
config = [{
'name': 'crowdai',
'groups': [
{
'tab': 'automation',
'list': 'automation_providers',
'name': 'crowdai_automation',
'label': 'CrowdAI',
'description': 'Imports from any newznab powered NZB providers RSS feed depending on the number of grabs per movie. Go to your newznab site and find the RSS section. Then copy the copy paste the link under "Movies > x264 feed" here.',
'options': [
{
'name': 'automation_enabled',
'default': False,
'type': 'enabler',
},
{
'name': 'automation_urls_use',
'label': 'Use',
'default': '1',
},
{
'name': 'automation_urls',
'label': 'url',
'type': 'combined',
'combine': ['automation_urls_use', 'automation_urls'],
'default': 'http://YOUR_PROVIDER/rss?t=THE_MOVIE_CATEGORY&i=YOUR_USER_ID&r=YOUR_API_KEY&res=2&rls=2&num=100',
},
{
'name': 'number_grabs',
'default': '500',
'label': 'Grab threshold',
'description': 'Number of grabs required',
},
],
},
],
}]
@@ -48,11 +48,12 @@ class Letterboxd(Automation):
soup = BeautifulSoup(self.getHTMLData(self.url % username))
for movie in soup.find_all('a', attrs = {'class': 'frame'}):
match = removeEmpty(self.pattern.split(movie['title']))
for movie in soup.find_all('li', attrs = {'class': 'poster-container'}):
img = movie.find('img', movie)
title = img.get('alt')
movies.append({
'title': match[0],
'year': match[1]
'title': title
})
return movies
@@ -39,15 +39,14 @@ class Rottentomatoes(Automation, RSS):
if result:
log.info2('Something smells...')
rating = tryInt(self.getTextElement(movie, rating_tag))
name = result.group(0)
print rating, tryInt(self.conf('tomatometer_percent'))
if rating < tryInt(self.conf('tomatometer_percent')):
log.info2('%s seems to be rotten...', name)
else:
log.info2('Found %s fresh enough movies, enqueuing: %s', (rating, name))
log.info2('Found %s with fresh rating %s', (name, rating))
year = datetime.datetime.now().strftime("%Y")
imdb = self.search(name, year)
@@ -69,12 +69,15 @@ class CouchPotatoApi(MovieProvider):
name_enc = base64.b64encode(ss(name))
return self.getJsonData(self.urls['validate'] % name_enc, headers = self.getRequestHeaders())
def isMovie(self, identifier = None):
def isMovie(self, identifier = None, adding = False):
if not identifier:
return
data = self.getJsonData(self.urls['is_movie'] % identifier, headers = self.getRequestHeaders())
url = self.urls['is_movie'] % identifier
url += '?adding=1' if adding else ''
data = self.getJsonData(url, headers = self.getRequestHeaders())
if data:
return data.get('is_movie', True)
@@ -2,6 +2,7 @@ import json
import re
import traceback
from couchpotato import Env
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import tryInt, tryFloat, splitString
@@ -17,8 +18,8 @@ autoload = 'OMDBAPI'
class OMDBAPI(MovieProvider):
urls = {
'search': 'http://www.omdbapi.com/?%s',
'info': 'http://www.omdbapi.com/?i=%s',
'search': 'http://www.omdbapi.com/?type=movie&%s',
'info': 'http://www.omdbapi.com/?type=movie&i=%s',
}
http_time_between_calls = 0
@@ -38,7 +39,8 @@ class OMDBAPI(MovieProvider):
}
cache_key = 'omdbapi.cache.%s' % q
cached = self.getCache(cache_key, self.urls['search'] % tryUrlencode({'t': name_year.get('name'), 'y': name_year.get('year', '')}), timeout = 3)
url = self.urls['search'] % tryUrlencode({'t': name_year.get('name'), 'y': name_year.get('year', '')})
cached = self.getCache(cache_key, url, timeout = 3, headers = {'User-Agent': Env.getIdentifier()})
if cached:
result = self.parseMovie(cached)
@@ -56,7 +58,7 @@ class OMDBAPI(MovieProvider):
return {}
cache_key = 'omdbapi.cache.%s' % identifier
cached = self.getCache(cache_key, self.urls['info'] % identifier, timeout = 3)
cached = self.getCache(cache_key, self.urls['info'] % identifier, timeout = 3, headers = {'User-Agent': Env.getIdentifier()})
if cached:
result = self.parseMovie(cached)
@@ -11,7 +11,7 @@ autoload = 'Bitsoup'
class Bitsoup(MovieProvider, Base):
cat_ids = [
([17], ['3d']),
([41], ['720p', '1080p']),
([80], ['720p', '1080p']),
([20], ['dvdr']),
([19], ['brrip', 'dvdrip']),
]
@@ -0,0 +1,11 @@
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.hdaccess import Base
from couchpotato.core.media.movie.providers.base import MovieProvider
log = CPLog(__name__)
autoload = 'HDAccess'
class HDAccess(MovieProvider, Base):
pass
@@ -13,7 +13,7 @@ class IPTorrents(MovieProvider, Base):
([87], ['3d']),
([48], ['720p', '1080p', 'bd50']),
([72], ['cam', 'ts', 'tc', 'r5', 'scr']),
([7,48], ['dvdrip', 'brrip']),
([7, 48, 20], ['dvdrip', 'brrip']),
([6], ['dvdr']),
]
@@ -0,0 +1,27 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.torrentleech import Base
from couchpotato.core.media.movie.providers.base import MovieProvider
log = CPLog(__name__)
autoload = 'TorrentLeech'
class TorrentLeech(MovieProvider, Base):
cat_ids = [
([13], ['720p', '1080p', 'bd50']),
([8], ['cam']),
([9], ['ts', 'tc']),
([10], ['r5', 'scr']),
([11], ['dvdrip']),
([13, 14], ['brrip']),
([12], ['dvdr']),
]
def buildUrl(self, title, media, quality):
return (
tryUrlencode(title.replace(':', '')),
','.join([str(x) for x in self.getCatId(quality)])
)
@@ -22,8 +22,8 @@ class TorrentShack(MovieProvider, Base):
# Movies-SD Pack - 983 (not included)
cat_ids = [
([970], ['bd50']),
([300], ['720p', '1080p']),
([970, 320], ['bd50']),
([300, 320], ['720p', '1080p']),
([350], ['dvdr']),
([400], ['brrip', 'dvdrip']),
]
@@ -1,6 +1,5 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.media._base.providers.torrent.torrentz import Base
from couchpotato.core.media.movie.providers.base import MovieProvider
@@ -11,5 +10,5 @@ autoload = 'Torrentz'
class Torrentz(MovieProvider, Base):
def buildUrl(self, media):
return tryUrlencode('"%s"' % fireEvent('library.query', media, single = True))
def buildUrl(self, title, media, quality):
return tryUrlencode('"%s %s"' % (title, media['info']['year']))
@@ -12,7 +12,7 @@ autoload = 'RottenTomatoes'
class RottenTomatoes(UserscriptBase):
includes = ['*://www.rottentomatoes.com/m/*/']
includes = ['*://www.rottentomatoes.com/m/*']
excludes = ['*://www.rottentomatoes.com/m/*/*/']
version = 2
+3 -2
View File
@@ -394,8 +394,9 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
log.info('Trying next release for: %s', getTitle(media))
self.single(media, manual = manual, force_download = force_download)
return True
return True
return False
except:
log.error('Failed searching for next release: %s', traceback.format_exc())
return False
@@ -51,8 +51,8 @@ var SuggestList = new Class({
self.show();
else
self.hide();
self.fireEvent('created');
self.fireEvent.delay(0, self, 'created');
},
-11
View File
@@ -1,11 +0,0 @@
from couchpotato.core.media import MediaBase
class ShowTypeBase(MediaBase):
_type = 'show'
def getType(self):
if hasattr(self, 'type') and self.type != self._type:
return '%s.%s' % (self._type, self.type)
return self._type
@@ -1,4 +0,0 @@
from .main import ShowBase
def autoload():
return ShowBase()
@@ -1,109 +0,0 @@
from couchpotato import get_db
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.logger import CPLog
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.media import MediaBase
log = CPLog(__name__)
autoload = 'Episode'
class Episode(MediaBase):
def __init__(self):
addEvent('show.episode.add', self.add)
addEvent('show.episode.update', self.update)
addEvent('show.episode.update_extras', self.updateExtras)
def add(self, parent_id, info = None, update_after = True, status = None):
if not info: info = {}
identifiers = info.pop('identifiers', None)
if not identifiers:
log.warning('Unable to add episode, missing identifiers (info provider mismatch?)')
return
# Add Season
episode_info = {
'_t': 'media',
'type': 'show.episode',
'identifiers': identifiers,
'status': status if status else 'active',
'parent_id': parent_id,
'info': info, # Returned dict by providers
}
# Check if season already exists
existing_episode = fireEvent('media.with_identifiers', identifiers, with_doc = True, single = True)
db = get_db()
if existing_episode:
s = existing_episode['doc']
s.update(episode_info)
episode = db.update(s)
else:
episode = db.insert(episode_info)
# Update library info
if update_after is not False:
handle = fireEventAsync if update_after is 'async' else fireEvent
handle('show.episode.update_extras', episode, info, store = True, single = True)
return episode
def update(self, media_id = None, identifiers = None, info = None):
if not info: info = {}
if self.shuttingDown():
return
db = get_db()
episode = db.get('id', media_id)
# Get new info
if not info:
season = db.get('id', episode['parent_id'])
show = db.get('id', season['parent_id'])
info = fireEvent(
'episode.info', show.get('identifiers'), {
'season_identifiers': season.get('identifiers'),
'season_number': season.get('info', {}).get('number'),
'episode_identifiers': episode.get('identifiers'),
'episode_number': episode.get('info', {}).get('number'),
'absolute_number': episode.get('info', {}).get('absolute_number')
},
merge = True
)
info['season_number'] = season.get('info', {}).get('number')
identifiers = info.pop('identifiers', None) or identifiers
# Update/create media
episode['identifiers'].update(identifiers)
episode.update({'info': info})
self.updateExtras(episode, info)
db.update(episode)
return episode
def updateExtras(self, episode, info, store=False):
db = get_db()
# Get images
image_urls = info.get('images', [])
existing_files = episode.get('files', {})
self.getPoster(image_urls, existing_files)
if store:
db.update(episode)
-291
View File
@@ -1,291 +0,0 @@
import time
import traceback
from couchpotato import get_db
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
from couchpotato.core.helpers.variable import getTitle, find
from couchpotato.core.logger import CPLog
from couchpotato.core.media import MediaBase
log = CPLog(__name__)
class ShowBase(MediaBase):
_type = 'show'
def __init__(self):
super(ShowBase, self).__init__()
self.initType()
addApiView('show.add', self.addView, docs = {
'desc': 'Add new show to the wanted list',
'params': {
'identifier': {'desc': 'IMDB id of the show your want to add.'},
'profile_id': {'desc': 'ID of quality profile you want the add the show in. If empty will use the default profile.'},
'category_id': {'desc': 'ID of category you want the add the show in.'},
'title': {'desc': 'Title of the show to use for search and renaming'},
}
})
addEvent('show.add', self.add)
addEvent('show.update', self.update)
addEvent('show.update_extras', self.updateExtras)
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 = None, force_readd = True, search_after = True, update_after = True, notify_after = True, status = None):
if not params: params = {}
# Identifiers
if not params.get('identifiers'):
msg = 'Can\'t add show without at least 1 identifier.'
log.error(msg)
fireEvent('notify.frontend', type = 'show.no_identifier', message = msg)
return False
info = params.get('info')
if not info or (info and len(info.get('titles', [])) == 0):
info = fireEvent('show.info', merge = True, identifiers = params.get('identifiers'))
# Add Show
try:
m, added = self.create(info, params, force_readd, search_after, update_after)
result = fireEvent('media.get', m['_id'], single = True)
if added and notify_after:
if params.get('title'):
message = 'Successfully added "%s" to your wanted list.' % params.get('title', '')
else:
title = getTitle(m)
if title:
message = 'Successfully added "%s" to your wanted list.' % title
else:
message = 'Successfully added to your wanted list.'
fireEvent('notify.frontend', type = 'show.added', data = result, message = message)
return result
except:
log.error('Failed adding media: %s', traceback.format_exc())
def create(self, info, params = None, force_readd = True, search_after = True, update_after = True, notify_after = True, status = None):
# Set default title
def_title = self.getDefaultTitle(info)
# Default profile and category
default_profile = {}
if not params.get('profile_id'):
default_profile = fireEvent('profile.default', single = True)
cat_id = params.get('category_id')
media = {
'_t': 'media',
'type': 'show',
'title': def_title,
'identifiers': info.get('identifiers'),
'status': status if status else 'active',
'profile_id': params.get('profile_id', default_profile.get('_id')),
'category_id': cat_id if cat_id is not None and len(cat_id) > 0 and cat_id != '-1' else None
}
identifiers = info.pop('identifiers', {})
seasons = info.pop('seasons', {})
# Update media with info
self.updateInfo(media, info)
existing_show = fireEvent('media.with_identifiers', params.get('identifiers'), with_doc = True)
db = get_db()
if existing_show:
s = existing_show['doc']
s.update(media)
show = db.update(s)
else:
show = db.insert(media)
# Update dict to be usable
show.update(media)
added = True
do_search = False
search_after = search_after and self.conf('search_on_add', section = 'showsearcher')
onComplete = None
if existing_show:
if search_after:
onComplete = self.createOnComplete(show['_id'])
search_after = False
elif force_readd:
# Clean snatched history
for release in fireEvent('release.for_media', show['_id'], single = True):
if release.get('status') in ['downloaded', 'snatched', 'done']:
if params.get('ignore_previous', False):
release['status'] = 'ignored'
db.update(release)
else:
fireEvent('release.delete', release['_id'], single = True)
show['profile_id'] = params.get('profile_id', default_profile.get('id'))
show['category_id'] = media.get('category_id')
show['last_edit'] = int(time.time())
do_search = True
db.update(show)
else:
params.pop('info', None)
log.debug('Show already exists, not updating: %s', params)
added = False
# Create episodes
self.createEpisodes(show, seasons)
# Trigger update info
if added and update_after:
# Do full update to get images etc
fireEventAsync('show.update_extras', show.copy(), info, store = True, on_complete = onComplete)
# Remove releases
for rel in fireEvent('release.for_media', show['_id'], single = True):
if rel['status'] is 'available':
db.delete(rel)
if do_search and search_after:
onComplete = self.createOnComplete(show['_id'])
onComplete()
return show, added
def createEpisodes(self, m, seasons_info):
# Add Seasons
for season_nr in seasons_info:
season_info = seasons_info[season_nr]
episodes = season_info.get('episodes', {})
season = fireEvent('show.season.add', m.get('_id'), season_info, update_after = False, single = True)
# Add Episodes
for episode_nr in episodes:
episode_info = episodes[episode_nr]
episode_info['season_number'] = season_nr
fireEvent('show.episode.add', season.get('_id'), episode_info, update_after = False, single = True)
def update(self, media_id = None, media = None, identifiers = None, info = None):
"""
Update movie information inside media['doc']['info']
@param media_id: document id
@param identifiers: identifiers from multiple providers
{
'thetvdb': 123,
'imdb': 'tt123123',
..
}
@param extended: update with extended info (parses more info, actors, images from some info providers)
@return: dict, with media
"""
if not info: info = {}
if not identifiers: identifiers = {}
db = get_db()
if self.shuttingDown():
return
if media is None and media_id:
media = db.get('id', media_id)
else:
log.error('missing "media" and "media_id" parameters, unable to update')
return
if not info:
info = fireEvent('show.info', identifiers = media.get('identifiers'), merge = True)
try:
identifiers = info.pop('identifiers', {})
seasons = info.pop('seasons', {})
self.updateInfo(media, info)
self.updateEpisodes(media, seasons)
self.updateExtras(media, info)
db.update(media)
return media
except:
log.error('Failed update media: %s', traceback.format_exc())
return {}
def updateInfo(self, media, info):
db = get_db()
# Remove season info for later use (save separately)
info.pop('in_wanted', None)
info.pop('in_library', None)
if not info or len(info) == 0:
log.error('Could not update, no show info to work with: %s', media.get('identifier'))
return False
# Update basic info
media['info'] = info
def updateEpisodes(self, media, seasons):
# Fetch current season/episode tree
show_tree = fireEvent('library.tree', media_id = media['_id'], single = True)
# Update seasons
for season_num in seasons:
season_info = seasons[season_num]
episodes = season_info.get('episodes', {})
# Find season that matches number
season = find(lambda s: s.get('info', {}).get('number', 0) == season_num, show_tree.get('seasons', []))
if not season:
log.warning('Unable to find season "%s"', season_num)
continue
# Update season
fireEvent('show.season.update', season['_id'], info = season_info, single = True)
# Update episodes
for episode_num in episodes:
episode_info = episodes[episode_num]
episode_info['season_number'] = season_num
# Find episode that matches number
episode = find(lambda s: s.get('info', {}).get('number', 0) == episode_num, season.get('episodes', []))
if not episode:
log.debug('Creating new episode %s in season %s', (episode_num, season_num))
fireEvent('show.episode.add', season.get('_id'), episode_info, update_after = False, single = True)
continue
fireEvent('show.episode.update', episode['_id'], info = episode_info, single = True)
def updateExtras(self, media, info, store=False):
db = get_db()
# Update image file
image_urls = info.get('images', [])
self.getPoster(media, image_urls)
if store:
db.update(media)
@@ -1,94 +0,0 @@
from couchpotato import get_db
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.logger import CPLog
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.media import MediaBase
log = CPLog(__name__)
autoload = 'Season'
class Season(MediaBase):
def __init__(self):
addEvent('show.season.add', self.add)
addEvent('show.season.update', self.update)
addEvent('show.season.update_extras', self.updateExtras)
def add(self, parent_id, info = None, update_after = True, status = None):
if not info: info = {}
identifiers = info.pop('identifiers', None)
info.pop('episodes', None)
# Add Season
season_info = {
'_t': 'media',
'type': 'show.season',
'identifiers': identifiers,
'status': status if status else 'active',
'parent_id': parent_id,
'info': info, # Returned dict by providers
}
# Check if season already exists
existing_season = fireEvent('media.with_identifiers', identifiers, with_doc = True, single = True)
db = get_db()
if existing_season:
s = existing_season['doc']
s.update(season_info)
season = db.update(s)
else:
season = db.insert(season_info)
# Update library info
if update_after is not False:
handle = fireEventAsync if update_after is 'async' else fireEvent
handle('show.season.update_extras', season, info, store = True, single = True)
return season
def update(self, media_id = None, identifiers = None, info = None):
if not info: info = {}
if self.shuttingDown():
return
db = get_db()
season = db.get('id', media_id)
show = db.get('id', season['parent_id'])
# Get new info
if not info:
info = fireEvent('season.info', show.get('identifiers'), {
'season_number': season.get('info', {}).get('number', 0)
}, merge = True)
identifiers = info.pop('identifiers', None) or identifiers
info.pop('episodes', None)
# Update/create media
season['identifiers'].update(identifiers)
season.update({'info': info})
self.updateExtras(season, info)
db.update(season)
return season
def updateExtras(self, season, info, store=False):
db = get_db()
# Get images
image_urls = info.get('images', [])
existing_files = season.get('files', {})
self.getPoster(image_urls, existing_files)
if store:
db.update(season)
@@ -1,28 +0,0 @@
Page.Shows = new Class({
Extends: PageBase,
name: 'shows',
title: 'Gimmy gimmy gimmy!',
folder_browser: null,
indexAction: function(){
var self = this;
if(!self.wanted){
// Wanted movies
self.wanted = new ShowList({
'identifier': 'wanted',
'status': 'active',
'type': 'show',
'actions': [MA.IMDB, MA.Trailer, MA.Release, MA.Edit, MA.Refresh, MA.Readd, MA.Delete],
'add_new': true,
'on_empty_element': App.createUserscriptButtons().addClass('empty_wanted')
});
$(self.wanted).inject(self.el);
}
}
});
@@ -1,474 +0,0 @@
var EpisodeAction = new Class({
Implements: [Options],
class_name: 'item-action icon2',
initialize: function(episode, options){
var self = this;
self.setOptions(options);
self.show = episode.show;
self.episode = episode;
self.create();
if(self.el)
self.el.addClass(self.class_name)
},
create: function(){},
disable: function(){
if(this.el)
this.el.addClass('disable')
},
enable: function(){
if(this.el)
this.el.removeClass('disable')
},
getTitle: function(){
var self = this;
try {
return self.show.getTitle();
}
catch(e){
try {
return self.show.original_title ? self.show.original_title : self.show.titles[0];
}
catch(e){
return 'Unknown';
}
}
},
get: function(key){
var self = this;
try {
return self.show.get(key)
}
catch(e){
return self.show[key]
}
},
createMask: function(){
var self = this;
self.mask = new Element('div.mask', {
'styles': {
'z-index': '1'
}
}).inject(self.show, 'top').fade('hide');
},
toElement: function(){
return this.el || null
}
});
var EA = {};
EA.IMDB = new Class({
Extends: EpisodeAction,
id: null,
create: function(){
var self = this;
self.id = self.show.getIdentifier ? self.show.getIdentifier() : self.get('imdb');
self.el = new Element('a.imdb', {
'title': 'Go to the IMDB page of ' + self.getTitle(),
'href': 'http://www.imdb.com/title/'+self.id+'/',
'target': '_blank'
});
if(!self.id) self.disable();
}
});
EA.Release = new Class({
Extends: EpisodeAction,
create: function(){
var self = this;
self.el = new Element('a.releases.download', {
'title': 'Show the releases that are available for ' + self.getTitle(),
'events': {
'click': self.toggle.bind(self)
}
});
self.options = new Element('div.episode-options').inject(self.episode.el);
if(!self.episode.data.releases || self.episode.data.releases.length == 0)
self.el.hide();
else
self.showHelper();
App.on('show.searcher.ended', function(notification){
if(self.show.data._id != notification.data._id) return;
self.releases = null;
if(self.options_container){
self.options_container.destroy();
self.options_container = null;
}
});
},
toggle: function(e){
var self = this;
if(self.options && self.options.hasClass('expanded')) {
self.close();
} else {
self.open();
}
},
open: function(e){
var self = this;
if(e)
(e).preventDefault();
self.createReleases();
},
close: function(e) {
var self = this;
if(e)
(e).preventDefault();
self.options.setStyle('height', 0)
.removeClass('expanded');
},
createReleases: function(){
var self = this;
if(!self.releases_table){
self.options.adopt(
self.releases_table = new Element('div.releases.table')
);
// Header
new Element('div.item.head').adopt(
new Element('span.name', {'text': 'Release name'}),
new Element('span.status', {'text': 'Status'}),
new Element('span.quality', {'text': 'Quality'}),
new Element('span.size', {'text': 'Size'}),
new Element('span.age', {'text': 'Age'}),
new Element('span.score', {'text': 'Score'}),
new Element('span.provider', {'text': 'Provider'})
).inject(self.releases_table);
if(self.episode.data.releases)
self.episode.data.releases.each(function(release){
var quality = Quality.getQuality(release.quality) || {},
info = release.info || {},
provider = self.get(release, 'provider') + (info['provider_extra'] ? self.get(release, 'provider_extra') : '');
var release_name = self.get(release, 'name');
if(release.files && release.files.length > 0){
try {
var movie_file = release.files.filter(function(file){
var type = File.Type.get(file.type_id);
return type && type.identifier == 'movie'
}).pick();
release_name = movie_file.path.split(Api.getOption('path_sep')).getLast();
}
catch(e){}
}
// Create release
release['el'] = new Element('div', {
'class': 'item '+release.status,
'id': 'release_'+release._id
}).adopt(
new Element('span.name', {'text': release_name, 'title': release_name}),
new Element('span.status', {'text': release.status, 'class': 'status '+release.status}),
new Element('span.quality', {'text': quality.label + (release.is_3d ? ' 3D' : '') || 'n/a'}),
new Element('span.size', {'text': info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}),
new Element('span.age', {'text': self.get(release, 'age')}),
new Element('span.score', {'text': self.get(release, 'score')}),
new Element('span.provider', { 'text': provider, 'title': provider }),
info['detail_url'] ? new Element('a.info.icon2', {
'href': info['detail_url'],
'target': '_blank'
}) : new Element('a'),
new Element('a.download.icon2', {
'events': {
'click': function(e){
(e).preventDefault();
if(!this.hasClass('completed'))
self.download(release);
}
}
}),
new Element('a.delete.icon2', {
'events': {
'click': function(e){
(e).preventDefault();
self.ignore(release);
}
}
})
).inject(self.releases_table);
if(release.status == 'ignored' || release.status == 'failed' || release.status == 'snatched'){
if(!self.last_release || (self.last_release && self.last_release.status != 'snatched' && release.status == 'snatched'))
self.last_release = release;
}
else if(!self.next_release && release.status == 'available'){
self.next_release = release;
}
var update_handle = function(notification) {
if(notification.data._id != release._id) return;
var q = self.show.quality.getElement('.q_' + release.quality),
new_status = notification.data.status;
release.el.set('class', 'item ' + new_status);
var status_el = release.el.getElement('.release_status');
status_el.set('class', 'release_status ' + new_status);
status_el.set('text', new_status);
if(!q && (new_status == 'snatched' || new_status == 'seeding' || new_status == 'done'))
q = self.addQuality(release.quality_id);
if(q && !q.hasClass(new_status)) {
q.removeClass(release.status).addClass(new_status);
q.set('title', q.get('title').replace(release.status, new_status));
}
};
App.on('release.update_status', update_handle);
});
if(self.last_release)
self.releases_table.getElements('#release_'+self.last_release._id).addClass('last_release');
if(self.next_release)
self.releases_table.getElements('#release_'+self.next_release._id).addClass('next_release');
if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status) === false)){
self.trynext_container = new Element('div.buttons.try_container').inject(self.releases_table, 'top');
var nr = self.next_release,
lr = self.last_release;
self.trynext_container.adopt(
new Element('span.or', {
'text': 'If anything went wrong, download'
}),
lr ? new Element('a.button.orange', {
'text': 'the same release again',
'events': {
'click': function(){
self.download(lr);
}
}
}) : null,
nr && lr ? new Element('span.or', {
'text': ','
}) : null,
nr ? [new Element('a.button.green', {
'text': lr ? 'another release' : 'the best release',
'events': {
'click': function(){
self.download(nr);
}
}
}),
new Element('span.or', {
'text': 'or pick one below'
})] : null
)
}
self.last_release = null;
self.next_release = null;
self.episode.el.addEvent('outerClick', function(){
self.close();
});
}
self.options.setStyle('height', self.releases_table.getSize().y)
.addClass('expanded');
},
showHelper: function(e){
var self = this;
if(e)
(e).preventDefault();
var has_available = false,
has_snatched = false;
if(self.episode.data.releases)
self.episode.data.releases.each(function(release){
if(has_available && has_snatched) return;
if(['snatched', 'downloaded', 'seeding'].contains(release.status))
has_snatched = true;
if(['available'].contains(release.status))
has_available = true;
});
if(has_available || has_snatched){
self.trynext_container = new Element('div.buttons.trynext').inject(self.show.info_container);
self.trynext_container.adopt(
has_available ? [new Element('a.icon2.readd', {
'text': has_snatched ? 'Download another release' : 'Download the best release',
'events': {
'click': self.tryNextRelease.bind(self)
}
}),
new Element('a.icon2.download', {
'text': 'pick one yourself',
'events': {
'click': function(){
self.show.quality.fireEvent('click');
}
}
})] : null,
new Element('a.icon2.completed', {
'text': 'mark this movie done',
'events': {
'click': self.markMovieDone.bind(self)
}
})
)
}
},
get: function(release, type){
return (release.info && release.info[type] !== undefined) ? release.info[type] : 'n/a'
},
download: function(release){
var self = this;
var release_el = self.releases_table.getElement('#release_'+release._id),
icon = release_el.getElement('.download.icon2');
if(icon)
icon.addClass('icon spinner').removeClass('download');
Api.request('release.manual_download', {
'data': {
'id': release._id
},
'onComplete': function(json){
if(icon)
icon.removeClass('icon spinner');
if(json.success){
if(icon)
icon.addClass('completed');
release_el.getElement('.release_status').set('text', 'snatched');
}
else
if(icon)
icon.addClass('attention').set('title', 'Something went wrong when downloading, please check logs.');
}
});
},
ignore: function(release){
Api.request('release.ignore', {
'data': {
'id': release._id
}
})
},
markMovieDone: function(){
var self = this;
Api.request('media.delete', {
'data': {
'id': self.show.get('_id'),
'delete_from': 'wanted'
},
'onComplete': function(){
var movie = $(self.show);
movie.set('tween', {
'duration': 300,
'onComplete': function(){
self.show.destroy()
}
});
movie.tween('height', 0);
}
});
},
tryNextRelease: function(){
var self = this;
Api.request('movie.searcher.try_next', {
'data': {
'media_id': self.show.get('_id')
}
});
}
});
EA.Refresh = new Class({
Extends: EpisodeAction,
create: function(){
var self = this;
self.el = new Element('a.refresh', {
'title': 'Refresh the movie info and do a forced search',
'events': {
'click': self.doRefresh.bind(self)
}
});
},
doRefresh: function(e){
var self = this;
(e).preventDefault();
Api.request('media.refresh', {
'data': {
'id': self.episode.get('_id')
}
});
}
});
@@ -1,128 +0,0 @@
var Episode = new Class({
Extends: BlockBase,
action: {},
initialize: function(show, options, data){
var self = this;
self.setOptions(options);
self.show = show;
self.options = options;
self.data = data;
self.profile = self.show.profile;
self.el = new Element('div.item.episode').adopt(
self.detail = new Element('div.item.data')
);
self.create();
},
create: function(){
var self = this;
self.detail.set('id', 'episode_'+self.data._id);
self.detail.adopt(
new Element('span.episode', {'text': (self.data.info.number || 0)}),
new Element('span.name', {'text': self.getTitle()}),
new Element('span.firstaired', {'text': self.data.info.firstaired}),
self.quality = new Element('span.quality', {
'events': {
'click': function(e){
var releases = self.detail.getElement('.item-actions .releases');
if(releases.isVisible())
releases.fireEvent('click', [e])
}
}
}),
self.actions = new Element('div.item-actions')
);
// Add profile
if(self.profile.data) {
self.profile.getTypes().each(function(type){
var q = self.addQuality(type.get('quality'), type.get('3d'));
if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){
q.addClass('finish');
q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.')
}
});
}
// Add releases
self.updateReleases();
Object.each(self.options.actions, function(action, key){
self.action[key.toLowerCase()] = action = new self.options.actions[key](self);
if(action.el)
self.actions.adopt(action)
});
},
updateReleases: function(){
var self = this;
if(!self.data.releases || self.data.releases.length == 0) return;
self.data.releases.each(function(release){
var q = self.quality.getElement('.q_'+ release.quality+(release.is_3d ? '.is_3d' : ':not(.is_3d)')),
status = release.status;
if(!q && (status == 'snatched' || status == 'seeding' || status == 'done'))
q = self.addQuality(release.quality, release.is_3d || false);
if (q && !q.hasClass(status)){
q.addClass(status);
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status)
}
});
},
addQuality: function(quality, is_3d){
var self = this,
q = Quality.getQuality(quality);
return new Element('span', {
'text': q.label + (is_3d ? ' 3D' : ''),
'class': 'q_'+q.identifier + (is_3d ? ' is_3d' : ''),
'title': ''
}).inject(self.quality);
},
getTitle: function(){
var self = this;
var title = '';
if(self.data.info.titles && self.data.info.titles.length > 0) {
title = self.data.info.titles[0];
} else {
title = 'Episode ' + self.data.info.number;
}
return title;
},
getIdentifier: function(){
var self = this;
try {
return self.get('identifiers').imdb;
}
catch (e){ }
return self.get('imdb');
},
get: function(attr){
return this.data[attr] || this.data.info[attr]
}
});
@@ -1,636 +0,0 @@
var ShowList = new Class({
Implements: [Events, Options],
options: {
navigation: true,
limit: 50,
load_more: true,
loader: true,
menu: [],
add_new: false,
force_view: false
},
movies: [],
movies_added: {},
total_movies: 0,
letters: {},
filter: null,
initialize: function(options){
var self = this;
self.setOptions(options);
self.offset = 0;
self.filter = self.options.filter || {
'starts_with': null,
'search': null
};
self.el = new Element('div.shows').adopt(
self.title = self.options.title ? new Element('h2', {
'text': self.options.title,
'styles': {'display': 'none'}
}) : null,
self.description = self.options.description ? new Element('div.description', {
'html': self.options.description,
'styles': {'display': 'none'}
}) : null,
self.movie_list = new Element('div.list'),
self.load_more = self.options.load_more ? new Element('a.load_more', {
'events': {
'click': self.loadMore.bind(self)
}
}) : null
);
if($(window).getSize().x <= 480 && !self.options.force_view)
self.changeView('list');
else
self.changeView(self.getSavedView() || self.options.view || 'details');
self.getMovies();
App.on('movie.added', self.movieAdded.bind(self));
App.on('movie.deleted', self.movieDeleted.bind(self))
},
movieDeleted: function(notification){
var self = this;
if(self.movies_added[notification.data._id]){
self.movies.each(function(movie){
if(movie.get('_id') == notification.data._id){
movie.destroy();
delete self.movies_added[notification.data._id];
self.setCounter(self.counter_count-1);
self.total_movies--;
}
})
}
self.checkIfEmpty();
},
movieAdded: function(notification){
var self = this;
self.fireEvent('movieAdded', notification);
if(self.options.add_new && !self.movies_added[notification.data._id] && notification.data.status == self.options.status){
window.scroll(0,0);
self.createShow(notification.data, 'top');
self.setCounter(self.counter_count+1);
self.checkIfEmpty();
}
},
create: function(){
var self = this;
// Create the alphabet nav
if(self.options.navigation)
self.createNavigation();
if(self.options.load_more)
self.scrollspy = new ScrollSpy({
min: function(){
var c = self.load_more.getCoordinates();
return c.top - window.document.getSize().y - 300
},
onEnter: self.loadMore.bind(self)
});
self.created = true;
},
addMovies: function(movies, total){
var self = this;
if(!self.created) self.create();
// do scrollspy
if(movies.length < self.options.limit && self.scrollspy){
self.load_more.hide();
self.scrollspy.stop();
}
Object.each(movies, function(movie){
self.createShow(movie);
});
self.total_movies += total;
self.setCounter(total);
},
setCounter: function(count){
var self = this;
if(!self.navigation_counter) return;
self.counter_count = count;
self.navigation_counter.set('text', (count || 0) + ' shows');
if (self.empty_message) {
self.empty_message.destroy();
self.empty_message = null;
}
if(self.total_movies && count == 0 && !self.empty_message){
var message = (self.filter.search ? 'for "'+self.filter.search+'"' : '') +
(self.filter.starts_with ? ' in <strong>'+self.filter.starts_with+'</strong>' : '');
self.empty_message = new Element('.message', {
'html': 'No shows found ' + message + '.<br/>'
}).grab(
new Element('a', {
'text': 'Reset filter',
'events': {
'click': function(){
self.filter = {
'starts_with': null,
'search': null
};
self.navigation_search_input.set('value', '');
self.reset();
self.activateLetter();
self.getMovies(true);
self.last_search_value = '';
}
}
})
).inject(self.movie_list);
}
},
createShow: function(show, inject_at){
var self = this;
var m = new Show(self, {
'actions': self.options.actions,
'view': self.current_view,
'onSelect': self.calculateSelected.bind(self)
}, show);
$(m).inject(self.movie_list, inject_at || 'bottom');
m.fireEvent('injected');
self.movies.include(m);
self.movies_added[show._id] = true;
},
createNavigation: function(){
var self = this;
var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
self.el.addClass('with_navigation');
self.navigation = new Element('div.alph_nav').adopt(
self.mass_edit_form = new Element('div.mass_edit_form').adopt(
new Element('span.select').adopt(
self.mass_edit_select = new Element('input[type=checkbox].inlay', {
'events': {
'change': self.massEditToggleAll.bind(self)
}
}),
self.mass_edit_selected = new Element('span.count', {'text': 0}),
self.mass_edit_selected_label = new Element('span', {'text': 'selected'})
),
new Element('div.quality').adopt(
self.mass_edit_quality = new Element('select'),
new Element('a.button.orange', {
'text': 'Change quality',
'events': {
'click': self.changeQualitySelected.bind(self)
}
})
),
new Element('div.delete').adopt(
new Element('span[text=or]'),
new Element('a.button.red', {
'text': 'Delete',
'events': {
'click': self.deleteSelected.bind(self)
}
})
),
new Element('div.refresh').adopt(
new Element('span[text=or]'),
new Element('a.button.green', {
'text': 'Refresh',
'events': {
'click': self.refreshSelected.bind(self)
}
})
)
),
new Element('div.menus').adopt(
self.navigation_counter = new Element('span.counter[title=Total]'),
self.filter_menu = new Block.Menu(self, {
'class': 'filter'
}),
self.navigation_actions = new Element('ul.actions', {
'events': {
'click:relay(li)': function(e, el){
var a = 'active';
self.navigation_actions.getElements('.'+a).removeClass(a);
self.changeView(el.get('data-view'));
this.addClass(a);
el.inject(el.getParent(), 'top');
el.getSiblings().hide();
setTimeout(function(){
el.getSiblings().setStyle('display', null);
}, 100)
}
}
}),
self.navigation_menu = new Block.Menu(self, {
'class': 'extra'
})
)
).inject(self.el, 'top');
// Mass edit
self.mass_edit_select_class = new Form.Check(self.mass_edit_select);
Quality.getActiveProfiles().each(function(profile){
new Element('option', {
'value': profile.get('_id'),
'text': profile.get('label')
}).inject(self.mass_edit_quality)
});
self.filter_menu.addLink(
self.navigation_search_input = new Element('input', {
'title': 'Search through ' + self.options.identifier,
'placeholder': 'Search through ' + self.options.identifier,
'events': {
'keyup': self.search.bind(self),
'change': self.search.bind(self)
}
})
).addClass('search');
var available_chars;
self.filter_menu.addEvent('open', function(){
self.navigation_search_input.focus();
// Get available chars and highlight
if(!available_chars && (self.navigation.isDisplayed() || self.navigation.isVisible()))
Api.request('media.available_chars', {
'data': Object.merge({
'type': 'show',
'status': self.options.status
}, self.filter),
'onSuccess': function(json){
available_chars = json.chars;
available_chars.each(function(c){
self.letters[c.capitalize()].addClass('available')
})
}
});
});
self.filter_menu.addLink(
self.navigation_alpha = new Element('ul.numbers', {
'events': {
'click:relay(li.available)': function(e, el){
self.activateLetter(el.get('data-letter'));
self.getMovies(true)
}
}
})
);
// Actions
['mass_edit', 'details', 'list'].each(function(view){
var current = self.current_view == view;
new Element('li', {
'class': 'icon2 ' + view + (current ? ' active ' : ''),
'data-view': view
}).inject(self.navigation_actions, current ? 'top' : 'bottom');
});
// All
self.letters['all'] = new Element('li.letter_all.available.active', {
'text': 'ALL'
}).inject(self.navigation_alpha);
// Chars
chars.split('').each(function(c){
self.letters[c] = new Element('li', {
'text': c,
'class': 'letter_'+c,
'data-letter': c
}).inject(self.navigation_alpha);
});
// Add menu or hide
if (self.options.menu.length > 0)
self.options.menu.each(function(menu_item){
self.navigation_menu.addLink(menu_item);
});
else
self.navigation_menu.hide();
},
calculateSelected: function(){
var self = this;
var selected = 0,
movies = self.movies.length;
self.movies.each(function(movie){
selected += movie.isSelected() ? 1 : 0
});
var indeterminate = selected > 0 && selected < movies,
checked = selected == movies && selected > 0;
self.mass_edit_select.set('indeterminate', indeterminate);
self.mass_edit_select_class[checked ? 'check' : 'uncheck']();
self.mass_edit_select_class.element[indeterminate ? 'addClass' : 'removeClass']('indeterminate');
self.mass_edit_selected.set('text', selected);
},
deleteSelected: function(){
var self = this,
ids = self.getSelectedMovies(),
help_msg = self.identifier == 'wanted' ? 'If you do, you won\'t be able to watch them, as they won\'t get downloaded!' : 'Your files will be safe, this will only delete the reference from the CouchPotato manage list';
var qObj = new Question('Are you sure you want to delete '+ids.length+' movie'+ (ids.length != 1 ? 's' : '') +'?', help_msg, [{
'text': 'Yes, delete '+(ids.length != 1 ? 'them' : 'it'),
'class': 'delete',
'events': {
'click': function(e){
(e).preventDefault();
this.set('text', 'Deleting..');
Api.request('media.delete', {
'method': 'post',
'data': {
'id': ids.join(','),
'delete_from': self.options.identifier
},
'onSuccess': function(){
qObj.close();
var erase_movies = [];
self.movies.each(function(movie){
if (movie.isSelected()){
$(movie).destroy();
erase_movies.include(movie);
}
});
erase_movies.each(function(movie){
self.movies.erase(movie);
movie.destroy();
self.setCounter(self.counter_count-1);
self.total_movies--;
});
self.calculateSelected();
}
});
}
}
}, {
'text': 'Cancel',
'cancel': true
}]);
},
changeQualitySelected: function(){
var self = this;
var ids = self.getSelectedMovies();
Api.request('movie.edit', {
'method': 'post',
'data': {
'id': ids.join(','),
'profile_id': self.mass_edit_quality.get('value')
},
'onSuccess': self.search.bind(self)
});
},
refreshSelected: function(){
var self = this;
var ids = self.getSelectedMovies();
Api.request('media.refresh', {
'method': 'post',
'data': {
'id': ids.join(',')
}
});
},
getSelectedMovies: function(){
var self = this;
var ids = [];
self.movies.each(function(movie){
if (movie.isSelected())
ids.include(movie.get('_id'))
});
return ids
},
massEditToggleAll: function(){
var self = this;
var select = self.mass_edit_select.get('checked');
self.movies.each(function(movie){
movie.select(select)
});
self.calculateSelected()
},
reset: function(){
var self = this;
self.movies = [];
if(self.mass_edit_select)
self.calculateSelected();
if(self.navigation_alpha)
self.navigation_alpha.getElements('.active').removeClass('active');
self.offset = 0;
if(self.scrollspy){
//self.load_more.show();
self.scrollspy.start();
}
},
activateLetter: function(letter){
var self = this;
self.reset();
self.letters[letter || 'all'].addClass('active');
self.filter.starts_with = letter;
},
changeView: function(new_view){
var self = this;
self.el
.removeClass(self.current_view+'_list')
.addClass(new_view+'_list');
self.current_view = new_view;
Cookie.write(self.options.identifier+'_view2', new_view, {duration: 1000});
},
getSavedView: function(){
var self = this;
return Cookie.read(self.options.identifier+'_view2');
},
search: function(){
var self = this;
if(self.search_timer) clearTimeout(self.search_timer);
self.search_timer = (function(){
var search_value = self.navigation_search_input.get('value');
if (search_value == self.last_search_value) return;
self.reset();
self.activateLetter();
self.filter.search = search_value;
self.getMovies(true);
self.last_search_value = search_value;
}).delay(250);
},
update: function(){
var self = this;
self.reset();
self.getMovies(true);
},
getMovies: function(reset){
var self = this;
if(self.scrollspy){
self.scrollspy.stop();
self.load_more.set('text', 'loading...');
}
if(self.movies.length == 0 && self.options.loader){
self.loader_first = new Element('div.loading').adopt(
new Element('div.message', {'text': self.options.title ? 'Loading \'' + self.options.title + '\'' : 'Loading...'})
).inject(self.el, 'top');
createSpinner(self.loader_first, {
radius: 4,
length: 4,
width: 1
});
self.el.setStyle('min-height', 93);
}
Api.request(self.options.api_call || 'media.list', {
'data': Object.merge({
'type': self.options.type || 'movie',
'status': self.options.status,
'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null
}, self.filter),
'onSuccess': function(json){
if(reset)
self.movie_list.empty();
if(self.loader_first){
var lf = self.loader_first;
self.loader_first.addClass('hide');
self.loader_first = null;
setTimeout(function(){
lf.destroy();
}, 20000);
self.el.setStyle('min-height', null);
}
self.store(json.shows);
self.addMovies(json.shows, json.total || json.shows.length);
if(self.scrollspy) {
self.load_more.set('text', 'load more movies');
self.scrollspy.start();
}
self.checkIfEmpty();
self.fireEvent('loaded');
}
});
},
loadMore: function(){
var self = this;
if(self.offset >= self.options.limit)
self.getMovies()
},
store: function(movies){
var self = this;
self.offset += movies.length;
},
checkIfEmpty: function(){
var self = this;
var is_empty = self.movies.length == 0 && (self.total_movies == 0 || self.total_movies === undefined);
if(self.title)
self.title[is_empty ? 'hide' : 'show']();
if(self.description)
self.description.setStyle('display', [is_empty ? 'none' : '']);
if(is_empty && self.options.on_empty_element){
self.options.on_empty_element.inject(self.loader_first || self.title || self.movie_list, 'after');
if(self.navigation)
self.navigation.hide();
self.empty_element = self.options.on_empty_element;
}
else if(self.empty_element){
self.empty_element.destroy();
if(self.navigation)
self.navigation.show();
}
},
toElement: function(){
return this.el;
}
});
@@ -1,230 +0,0 @@
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': {
'identifiers': self.info.identifiers,
'type': self.info.type,
'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.success ? '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.get('_id'),
'text': profile.get('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
}
});
@@ -1,127 +0,0 @@
var Season = new Class({
Extends: BlockBase,
action: {},
initialize: function(show, options, data){
var self = this;
self.setOptions(options);
self.show = show;
self.options = options;
self.data = data;
self.profile = self.show.profile;
self.el = new Element('div.item.season').adopt(
self.detail = new Element('div.item.data')
);
self.create();
},
create: function(){
var self = this;
self.detail.set('id', 'season_'+self.data._id);
self.detail.adopt(
new Element('span.name', {'text': self.getTitle()}),
self.quality = new Element('span.quality', {
'events': {
'click': function(e){
var releases = self.detail.getElement('.item-actions .releases');
if(releases.isVisible())
releases.fireEvent('click', [e])
}
}
}),
self.actions = new Element('div.item-actions')
);
// Add profile
if(self.profile.data) {
self.profile.getTypes().each(function(type){
var q = self.addQuality(type.get('quality'), type.get('3d'));
if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){
q.addClass('finish');
q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.')
}
});
}
// Add releases
self.updateReleases();
Object.each(self.options.actions, function(action, key){
self.action[key.toLowerCase()] = action = new self.options.actions[key](self);
if(action.el)
self.actions.adopt(action)
});
},
updateReleases: function(){
var self = this;
if(!self.data.releases || self.data.releases.length == 0) return;
self.data.releases.each(function(release){
var q = self.quality.getElement('.q_'+ release.quality+(release.is_3d ? '.is_3d' : ':not(.is_3d)')),
status = release.status;
if(!q && (status == 'snatched' || status == 'seeding' || status == 'done'))
q = self.addQuality(release.quality, release.is_3d || false);
if (q && !q.hasClass(status)){
q.addClass(status);
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status)
}
});
},
addQuality: function(quality, is_3d){
var self = this,
q = Quality.getQuality(quality);
return new Element('span', {
'text': q.label + (is_3d ? ' 3D' : ''),
'class': 'q_'+q.identifier + (is_3d ? ' is_3d' : ''),
'title': ''
}).inject(self.quality);
},
getTitle: function(){
var self = this;
var title = '';
if(self.data.info.number) {
title = 'Season ' + self.data.info.number;
} else {
// Season 0 / Specials
title = 'Specials';
}
return title;
},
getIdentifier: function(){
var self = this;
try {
return self.get('identifiers').imdb;
}
catch (e){ }
return self.get('imdb');
},
get: function(attr){
return this.data[attr] || this.data.info[attr]
}
});
File diff suppressed because it is too large Load Diff
@@ -1,92 +0,0 @@
var Episodes = new Class({
initialize: function(show, options) {
var self = this;
self.show = show;
self.options = options;
},
open: function(){
var self = this;
if(!self.container){
self.container = new Element('div.options').grab(
self.episodes_container = new Element('div.episodes.table')
);
self.container.inject(self.show, 'top');
Api.request('library.tree', {
'data': {
'media_id': self.show.data._id
},
'onComplete': function(json){
self.data = json.result;
self.createEpisodes();
}
});
}
self.show.slide('in', self.container, true);
},
createEpisodes: function() {
var self = this;
self.data.seasons.sort(self.sortSeasons);
self.data.seasons.each(function(season) {
self.createSeason(season);
season.episodes.sort(self.sortEpisodes);
season.episodes.each(function(episode) {
self.createEpisode(episode);
});
});
},
createSeason: function(season) {
var self = this,
s = new Season(self.show, self.options, season);
$(s).inject(self.episodes_container);
},
createEpisode: function(episode){
var self = this,
e = new Episode(self.show, self.options, episode);
$(e).inject(self.episodes_container);
},
sortSeasons: function(a, b) {
// Move "Specials" to the bottom of the list
if(!a.info.number) {
return 1;
}
if(!b.info.number) {
return -1;
}
// Order seasons descending
if(a.info.number < b.info.number)
return -1;
if(a.info.number > b.info.number)
return 1;
return 0;
},
sortEpisodes: function(a, b) {
// Order episodes descending
if(a.info.number < b.info.number)
return -1;
if(a.info.number > b.info.number)
return 1;
return 0;
}
});
@@ -1,370 +0,0 @@
var Show = new Class({
Extends: BlockBase,
action: {},
initialize: function(list, options, data){
var self = this;
self.data = data;
self.view = options.view || 'details';
self.list = list;
self.el = new Element('div.show');
self.episodes = new Episodes(self, {
'actions': [EA.IMDB, EA.Release, EA.Refresh]
});
self.profile = Quality.getProfile(data.profile_id) || {};
self.category = CategoryList.getCategory(data.category_id) || {};
self.parent(self, options);
self.addEvents();
},
addEvents: function(){
var self = this;
self.global_events = {};
// Do refresh with new data
self.global_events['movie.update'] = function(notification){
if(self.data._id != notification.data._id) return;
self.busy(false);
self.removeView();
self.update.delay(2000, self, notification);
};
App.on('movie.update', self.global_events['movie.update']);
// Add spinner on load / search
['media.busy', 'movie.searcher.started'].each(function(listener){
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)))
self.busy(true);
};
App.on(listener, self.global_events[listener]);
});
// Remove spinner
self.global_events['movie.searcher.ended'] = function(notification){
if(notification.data && self.data._id == notification.data._id)
self.busy(false)
};
App.on('movie.searcher.ended', self.global_events['movie.searcher.ended']);
// Reload when releases have updated
self.global_events['release.update_status'] = function(notification){
var data = notification.data;
if(data && self.data._id == data.movie_id){
if(!self.data.releases)
self.data.releases = [];
self.data.releases.push({'quality': data.quality, 'status': data.status});
self.updateReleases();
}
};
App.on('release.update_status', self.global_events['release.update_status']);
},
destroy: function(){
var self = this;
self.el.destroy();
delete self.list.movies_added[self.get('id')];
self.list.movies.erase(self);
self.list.checkIfEmpty();
// Remove events
Object.each(self.global_events, function(handle, listener){
App.off(listener, handle);
});
},
busy: function(set_busy, timeout){
var self = this;
if(!set_busy){
setTimeout(function(){
if(self.spinner){
self.mask.fade('out');
setTimeout(function(){
if(self.mask)
self.mask.destroy();
if(self.spinner)
self.spinner.el.destroy();
self.spinner = null;
self.mask = null;
}, timeout || 400);
}
}, timeout || 1000)
}
else if(!self.spinner) {
self.createMask();
self.spinner = createSpinner(self.mask);
self.mask.fade('in');
}
},
createMask: function(){
var self = this;
self.mask = new Element('div.mask', {
'styles': {
'z-index': 4
}
}).inject(self.el, 'top').fade('hide');
},
update: function(notification){
var self = this;
self.data = notification.data;
self.el.empty();
self.removeView();
self.profile = Quality.getProfile(self.data.profile_id) || {};
self.category = CategoryList.getCategory(self.data.category_id) || {};
self.create();
self.busy(false);
},
create: function(){
var self = this;
self.el.addClass('status_'+self.get('status'));
var eta = null,
eta_date = null,
now = Math.round(+new Date()/1000);
if(self.data.info.release_date)
[self.data.info.release_date.dvd, self.data.info.release_date.theater].each(function(timestamp){
if (timestamp > 0 && (eta == null || Math.abs(timestamp - now) < Math.abs(eta - now)))
eta = timestamp;
});
if(eta){
eta_date = new Date(eta * 1000);
eta_date = eta_date.toLocaleString('en-us', { month: "long" }) + ' ' + eta_date.getFullYear();
}
self.el.adopt(
self.select_checkbox = new Element('input[type=checkbox].inlay', {
'events': {
'change': function(){
self.fireEvent('select')
}
}
}),
self.thumbnail = (self.data.files && self.data.files.image_poster) ? new Element('img', {
'class': 'type_image poster',
'src': Api.createUrl('file.cache') + self.data.files.image_poster[0].split(Api.getOption('path_sep')).pop()
}): null,
self.data_container = new Element('div.data.inlay.light').adopt(
self.info_container = new Element('div.info').adopt(
new Element('div.title').adopt(
self.title = new Element('a', {
'events': {
'click': function(e){
self.episodes.open();
}
},
'text': self.getTitle() || 'n/a'
}),
self.year = new Element('div.year', {
'text': self.data.info.year || 'n/a'
})
),
self.description = new Element('div.description.tiny_scroll', {
'text': self.data.info.plot
}),
self.eta = eta_date && (now+8035200 > eta) ? new Element('div.eta', {
'text': eta_date,
'title': 'ETA'
}) : null,
self.quality = new Element('div.quality', {
'events': {
'click': function(e){
var releases = self.el.getElement('.actions .releases');
if(releases.isVisible())
releases.fireEvent('click', [e])
}
}
})
),
self.actions = new Element('div.actions')
)
);
if(!self.thumbnail)
self.el.addClass('no_thumbnail');
//self.changeView(self.view);
self.select_checkbox_class = new Form.Check(self.select_checkbox);
// Add profile
if(self.profile.data)
self.profile.getTypes().each(function(type){
var q = self.addQuality(type.get('quality'), type.get('3d'));
if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){
q.addClass('finish');
q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.')
}
});
// Add releases
self.updateReleases();
Object.each(self.options.actions, function(action, key){
self.action[key.toLowerCase()] = action = new self.options.actions[key](self);
if(action.el)
self.actions.adopt(action)
});
},
updateReleases: function(){
var self = this;
if(!self.data.releases || self.data.releases.length == 0) return;
self.data.releases.each(function(release){
var q = self.quality.getElement('.q_'+ release.quality+(release.is_3d ? '.is_3d' : ':not(.is_3d)')),
status = release.status;
if(!q && (status == 'snatched' || status == 'seeding' || status == 'done'))
q = self.addQuality(release.quality, release.is_3d || false);
if (q && !q.hasClass(status)){
q.addClass(status);
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status)
}
});
},
addQuality: function(quality, is_3d){
var self = this;
var q = Quality.getQuality(quality);
return new Element('span', {
'text': q.label + (is_3d ? ' 3D' : ''),
'class': 'q_'+q.identifier + (is_3d ? ' is_3d' : ''),
'title': ''
}).inject(self.quality);
},
getTitle: function(){
var self = this;
if(self.data.title)
return self.getUnprefixedTitle(self.data.title);
else if(self.data.info.titles.length > 0)
return self.getUnprefixedTitle(self.data.info.titles[0]);
return 'Unknown movie'
},
getUnprefixedTitle: function(t){
if(t.substr(0, 4).toLowerCase() == 'the ')
t = t.substr(4) + ', The';
else if(t.substr(0, 3).toLowerCase() == 'an ')
t = t.substr(3) + ', An';
else if(t.substr(0, 2).toLowerCase() == 'a ')
t = t.substr(2) + ', A';
return t;
},
slide: function(direction, el, expand){
var self = this;
if(direction == 'in'){
self.temp_view = self.view;
self.changeView('details');
self.el.addEvent('outerClick', function(){
self.removeView();
self.slide('out')
});
el.show();
if(expand === true) {
self.el.addClass('expanded');
self.el.getElements('.table').addClass('expanded');
}
self.data_container.addClass('hide_right');
}
else {
self.el.removeEvents('outerClick');
setTimeout(function(){
if(self.el)
{
self.el.getElements('> :not(.data):not(.poster):not(.movie_container)').hide();
self.el.getElements('.table').removeClass('expanded');
}
}, 600);
self.el.removeClass('expanded');
self.data_container.removeClass('hide_right');
}
},
changeView: function(new_view){
var self = this;
if(self.el)
self.el
.removeClass(self.view+'_view')
.addClass(new_view+'_view');
self.view = new_view;
},
removeView: function(){
var self = this;
self.el.removeClass(self.view+'_view')
},
getIdentifier: function(){
var self = this;
try {
return self.get('identifiers').imdb;
}
catch (e){ }
return self.get('imdb');
},
get: function(attr){
return this.data[attr] || this.data.info[attr]
},
select: function(bool){
var self = this;
self.select_checkbox_class[bool ? 'check' : 'uncheck']()
},
isSelected: function(){
return this.select_checkbox.get('checked');
},
toElement: function(){
return this.el;
}
});
@@ -1,71 +0,0 @@
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.library.base import LibraryBase
log = CPLog(__name__)
autoload = 'EpisodeLibraryPlugin'
class EpisodeLibraryPlugin(LibraryBase):
def __init__(self):
addEvent('library.query', self.query)
addEvent('library.identifier', self.identifier)
def query(self, media, first = True, condense = True, include_identifier = True, **kwargs):
if media.get('type') != 'show.episode':
return
related = fireEvent('library.related', media, single = True)
# Get season titles
titles = fireEvent(
'library.query', related['season'],
first = False,
include_identifier = include_identifier,
condense = condense,
single = True
)
# Add episode identifier to titles
if include_identifier:
identifier = fireEvent('library.identifier', media, single = True)
if 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, media):
if media.get('type') != 'show.episode':
return
identifier = {
'season': None,
'episode': None
}
# TODO identifier mapping
# scene_map = media['info'].get('map_episode', {}).get('scene')
# if scene_map:
# # Use scene mappings if they are available
# identifier['season'] = scene_map.get('season_nr')
# identifier['episode'] = scene_map.get('episode_nr')
# else:
# Fallback to normal season/episode numbers
identifier['season'] = media['info'].get('season_number')
identifier['episode'] = media['info'].get('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
@@ -1,52 +0,0 @@
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.library.base import LibraryBase
log = CPLog(__name__)
autoload = 'SeasonLibraryPlugin'
class SeasonLibraryPlugin(LibraryBase):
def __init__(self):
addEvent('library.query', self.query)
addEvent('library.identifier', self.identifier)
def query(self, media, first = True, condense = True, include_identifier = True, **kwargs):
if media.get('type') != 'show.season':
return
related = fireEvent('library.related', media, single = True)
# Get show titles
titles = fireEvent(
'library.query', related['show'],
first = False,
condense = condense,
single = True
)
# TODO map_names
# Add season identifier to titles
if include_identifier:
identifier = fireEvent('library.identifier', media, single = True)
if 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, media):
if media.get('type') != 'show.season':
return
return {
'season': tryInt(media['info']['number'], None)
}
@@ -1,38 +0,0 @@
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import simplifyString
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.library.base import LibraryBase
from qcond import QueryCondenser
log = CPLog(__name__)
autoload = 'ShowLibraryPlugin'
class ShowLibraryPlugin(LibraryBase):
query_condenser = QueryCondenser()
def __init__(self):
addEvent('library.query', self.query)
def query(self, media, first = True, condense = True, include_identifier = True, **kwargs):
if media.get('type') != 'show':
return
titles = media['info']['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 = [simplifyString(title) for title in titles]
if first:
return titles[0] if titles else None
return titles
@@ -1,7 +0,0 @@
from .main import ShowMatcher
def autoload():
return ShowMatcher()
config = []
@@ -1,72 +0,0 @@
from couchpotato import fireEvent, CPLog, tryInt
from couchpotato.core.event import addEvent
from couchpotato.core.media._base.matcher.base import MatcherBase
log = CPLog(__name__)
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
@@ -1,30 +0,0 @@
from couchpotato import fireEvent, CPLog
from couchpotato.core.media.show.matcher.base import Base
log = CPLog(__name__)
class Episode(Base):
type = 'show.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, 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
@@ -1,9 +0,0 @@
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.matcher.episode import Episode
from couchpotato.core.media.show.matcher.season import Season
class ShowMatcher(MultiProvider):
def getTypes(self):
return [Season, Episode]
@@ -1,27 +0,0 @@
from couchpotato import fireEvent, CPLog
from couchpotato.core.media.show.matcher.base import Base
log = CPLog(__name__)
class Season(Base):
type = 'show.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, single = True)
if identifier != required:
log.info2('Wrong: required identifier (%s) does not match release identifier (%s)', (required, identifier))
return False
return True
@@ -1,13 +0,0 @@
from couchpotato.core.media._base.providers.info.base import BaseInfoProvider
class ShowProvider(BaseInfoProvider):
type = 'show'
class SeasonProvider(BaseInfoProvider):
type = 'show.season'
class EpisodeProvider(BaseInfoProvider):
type = 'show.episode'
@@ -1,372 +0,0 @@
from datetime import datetime
import os
import traceback
from couchpotato import Env
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
from couchpotato.core.helpers.variable import splitString, tryInt, tryFloat
from couchpotato.core.logger import CPLog
from couchpotato.core.media.show.providers.base import ShowProvider
from tvdb_api import tvdb_exceptions
from tvdb_api.tvdb_api import Tvdb, Show
log = CPLog(__name__)
autoload = 'TheTVDb'
class TheTVDb(ShowProvider):
# TODO: Consider grabbing zips to put less strain on tvdb
# TODO: Unicode stuff (check)
# TODO: Notigy frontend on error (tvdb down at monent)
# TODO: Expose apikey in setting so it can be changed by user
def __init__(self):
addEvent('show.info', self.getShowInfo, priority = 1)
addEvent('season.info', self.getSeasonInfo, priority = 1)
addEvent('episode.info', self.getEpisodeInfo, priority = 1)
self.tvdb_api_parms = {
'apikey': self.conf('api_key'),
'banners': True,
'language': 'en',
'cache': os.path.join(Env.get('cache_dir'), 'thetvdb_api'),
}
self._setup()
def _setup(self):
self.tvdb = Tvdb(**self.tvdb_api_parms)
self.valid_languages = self.tvdb.config['valid_languages']
def getShow(self, identifier = None):
show = None
try:
log.debug('Getting show: %s', identifier)
show = self.tvdb[int(identifier)]
except (tvdb_exceptions.tvdb_error, IOError), e:
log.error('Failed to getShowInfo for show id "%s": %s', (identifier, traceback.format_exc()))
return None
return show
def getShowInfo(self, identifiers = None):
"""
@param identifiers: dict with identifiers per provider
@return: Full show info including season and episode info
"""
if not identifiers or not identifiers.get('thetvdb'):
return None
identifier = tryInt(identifiers.get('thetvdb'))
cache_key = 'thetvdb.cache.show.%s' % identifier
result = None #self.getCache(cache_key)
if result:
return result
show = self.getShow(identifier = identifier)
if show:
result = self._parseShow(show)
self.setCache(cache_key, result)
return result or {}
def getSeasonInfo(self, identifiers = None, params = {}):
"""Either return a list of all seasons or a single season by number.
identifier is the show 'id'
"""
if not identifiers or not identifiers.get('thetvdb'):
return None
season_number = params.get('season_number', None)
identifier = tryInt(identifiers.get('thetvdb'))
cache_key = 'thetvdb.cache.%s.%s' % (identifier, season_number)
log.debug('Getting SeasonInfo: %s', cache_key)
result = self.getCache(cache_key) or {}
if result:
return result
try:
show = self.tvdb[int(identifier)]
except (tvdb_exceptions.tvdb_error, IOError), e:
log.error('Failed parsing TheTVDB SeasonInfo for "%s" id "%s": %s', (show, identifier, traceback.format_exc()))
return False
result = []
for number, season in show.items():
if season_number is not None and number == season_number:
result = self._parseSeason(show, number, season)
self.setCache(cache_key, result)
return result
else:
result.append(self._parseSeason(show, number, season))
self.setCache(cache_key, result)
return result
def getEpisodeInfo(self, identifier = None, params = {}):
"""Either return a list of all episodes or a single episode.
If episode_identifer contains an episode number to search for
"""
season_number = self.getIdentifier(params.get('season_number', None))
episode_identifier = self.getIdentifier(params.get('episode_identifiers', None))
identifier = self.getIdentifier(identifier)
if not identifier and season_number is None:
return False
# season_identifier must contain the 'show id : season number' since there is no tvdb id
# for season and we need a reference to both the show id and season number
if not identifier and season_number:
try:
identifier, season_number = season_number.split(':')
season_number = int(season_number)
except: return None
cache_key = 'thetvdb.cache.%s.%s.%s' % (identifier, episode_identifier, season_number)
log.debug('Getting EpisodeInfo: %s', cache_key)
result = self.getCache(cache_key) or {}
if result:
return result
try:
show = self.tvdb[int(identifier)]
except (tvdb_exceptions.tvdb_error, IOError), e:
log.error('Failed parsing TheTVDB EpisodeInfo for "%s" id "%s": %s', (show, identifier, traceback.format_exc()))
return False
result = []
for number, season in show.items():
if season_number is not None and number != season_number:
continue
for episode in season.values():
if episode_identifier is not None and episode['id'] == toUnicode(episode_identifier):
result = self._parseEpisode(episode)
self.setCache(cache_key, result)
return result
else:
result.append(self._parseEpisode(episode))
self.setCache(cache_key, result)
return result
def getIdentifier(self, value):
if type(value) is dict:
return value.get('thetvdb')
return value
def _parseShow(self, show):
#
# NOTE: show object only allows direct access via
# show['id'], not show.get('id')
#
def get(name):
return show.get(name) if not hasattr(show, 'search') else show[name]
## Images
poster = get('poster')
backdrop = get('fanart')
genres = splitString(get('genre'), '|')
if get('firstaired') is not None:
try: year = datetime.strptime(get('firstaired'), '%Y-%m-%d').year
except: year = None
else:
year = None
show_data = {
'identifiers': {
'thetvdb': tryInt(get('id')),
'imdb': get('imdb_id'),
'zap2it': get('zap2it_id'),
},
'type': 'show',
'titles': [get('seriesname')],
'images': {
'poster': [poster] if poster else [],
'backdrop': [backdrop] if backdrop else [],
'poster_original': [],
'backdrop_original': [],
},
'year': year,
'genres': genres,
'network': get('network'),
'plot': get('overview'),
'networkid': get('networkid'),
'air_day': (get('airs_dayofweek') or '').lower(),
'air_time': self.parseTime(get('airs_time')),
'firstaired': get('firstaired'),
'runtime': tryInt(get('runtime')),
'contentrating': get('contentrating'),
'rating': {},
'actors': splitString(get('actors'), '|'),
'status': get('status'),
'language': get('language'),
}
if tryFloat(get('rating')):
show_data['rating']['thetvdb'] = [tryFloat(get('rating')), tryInt(get('ratingcount'))],
show_data = dict((k, v) for k, v in show_data.iteritems() if v)
# Only load season info when available
if type(show) == Show:
# Parse season and episode data
show_data['seasons'] = {}
for season_nr in show:
season = self._parseSeason(show, season_nr, show[season_nr])
season['episodes'] = {}
for episode_nr in show[season_nr]:
season['episodes'][episode_nr] = self._parseEpisode(show[season_nr][episode_nr])
show_data['seasons'][season_nr] = season
# Add alternative titles
# try:
# raw = self.tvdb.search(show['seriesname'])
# if raw:
# for show_info in raw:
# print show_info
# if show_info['id'] == show_data['id'] and show_info.get('aliasnames', None):
# for alt_name in show_info['aliasnames'].split('|'):
# show_data['titles'].append(toUnicode(alt_name))
# except (tvdb_exceptions.tvdb_error, IOError), e:
# log.error('Failed searching TheTVDB for "%s": %s', (show['seriesname'], traceback.format_exc()))
return show_data
def _parseSeason(self, show, number, season):
"""
contains no data
"""
poster = []
try:
temp_poster = {}
for id, data in show.data['_banners']['season']['season'].items():
if data.get('season') == str(number) and data.get('language') == self.tvdb_api_parms['language']:
temp_poster[tryFloat(data.get('rating')) * tryInt(data.get('ratingcount'))] = data.get('_bannerpath')
#break
poster.append(temp_poster[sorted(temp_poster, reverse = True)[0]])
except:
pass
season_data = {
'identifiers': {
'thetvdb': show['id'] if show.get('id') else show[number][1]['seasonid']
},
'number': tryInt(number),
'images': {
'poster': poster,
},
}
season_data = dict((k, v) for k, v in season_data.iteritems() if v)
return season_data
def _parseEpisode(self, episode):
"""
('episodenumber', u'1'),
('thumb_added', None),
('rating', u'7.7'),
('overview',
u'Experienced waitress Max Black meets her new co-worker, former rich-girl Caroline Channing, and puts her skills to the test at an old but re-emerging Brooklyn diner. Despite her initial distaste for Caroline, Max eventually softens and the two team up for a new business venture.'),
('dvd_episodenumber', None),
('dvd_discid', None),
('combined_episodenumber', u'1'),
('epimgflag', u'7'),
('id', u'4099506'),
('seasonid', u'465948'),
('thumb_height', u'225'),
('tms_export', u'1374789754'),
('seasonnumber', u'1'),
('writer', u'|Michael Patrick King|Whitney Cummings|'),
('lastupdated', u'1371420338'),
('filename', u'http://thetvdb.com/banners/episodes/248741/4099506.jpg'),
('absolute_number', u'1'),
('ratingcount', u'102'),
('combined_season', u'1'),
('thumb_width', u'400'),
('imdb_id', u'tt1980319'),
('director', u'James Burrows'),
('dvd_chapter', None),
('dvd_season', None),
('gueststars',
u'|Brooke Lyons|Noah Mills|Shoshana Bush|Cale Hartmann|Adam Korson|Alex Enriquez|Matt Cook|Bill Parks|Eugene Shaw|Sergey Brusilovsky|Greg Lewis|Cocoa Brown|Nick Jameson|'),
('seriesid', u'248741'),
('language', u'en'),
('productioncode', u'296793'),
('firstaired', u'2011-09-19'),
('episodename', u'Pilot')]
"""
def get(name, default = None):
return episode.get(name, default)
poster = get('filename', [])
episode_data = {
'number': tryInt(get('episodenumber')),
'absolute_number': tryInt(get('absolute_number')),
'identifiers': {
'thetvdb': tryInt(episode['id'])
},
'type': 'episode',
'titles': [get('episodename')] if get('episodename') else [],
'images': {
'poster': [poster] if poster else [],
},
'released': get('firstaired'),
'plot': get('overview'),
'firstaired': get('firstaired'),
'language': get('language'),
}
if get('imdb_id'):
episode_data['identifiers']['imdb'] = get('imdb_id')
episode_data = dict((k, v) for k, v in episode_data.iteritems() if v)
return episode_data
def parseTime(self, time):
return time
def isDisabled(self):
if self.conf('api_key') == '':
log.error('No API key provided.')
return True
else:
return False
config = [{
'name': 'thetvdb',
'groups': [
{
'tab': 'providers',
'name': 'tmdb',
'label': 'TheTVDB',
'hidden': True,
'description': 'Used for all calls to TheTVDB.',
'options': [
{
'name': 'api_key',
'default': '7966C02F860586D2',
'label': 'Api Key',
},
],
},
],
}]
@@ -1,86 +0,0 @@
import urllib
from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.media.show.providers.base import ShowProvider
log = CPLog(__name__)
autoload = 'Trakt'
class Trakt(ShowProvider):
api_key = 'c043de5ada9d180028c10229d2a3ea5b'
base_url = 'http://api.trakt.tv/%%s.json/%s' % api_key
def __init__(self):
addEvent('info.search', self.search, priority = 1)
addEvent('show.search', self.search, priority = 1)
def search(self, q, limit = 12):
if self.isDisabled():
return False
# Check for cached result
cache_key = 'trakt.cache.search.%s.%s' % (q, limit)
results = self.getCache(cache_key) or []
if results:
return results
# Search
log.debug('Searching for show: "%s"', q)
response = self._request('search/shows', query=q, limit=limit)
if not response:
return []
# Parse search results
for show in response:
results.append(self._parseShow(show))
log.info('Found: %s', [result['titles'][0] + ' (' + str(result.get('year', 0)) + ')' for result in results])
self.setCache(cache_key, results)
return results
def _request(self, action, **kwargs):
url = self.base_url % action
if kwargs:
url += '?' + urllib.urlencode(kwargs)
return self.getJsonData(url)
def _parseShow(self, show):
# Images
images = show.get('images', {})
poster = images.get('poster')
backdrop = images.get('backdrop')
# Rating
rating = show.get('ratings', {}).get('percentage')
# Build show dict
show_data = {
'identifiers': {
'thetvdb': show.get('tvdb_id'),
'imdb': show.get('imdb_id'),
'tvrage': show.get('tvrage_id'),
},
'type': 'show',
'titles': [show.get('title')],
'images': {
'poster': [poster] if poster else [],
'backdrop': [backdrop] if backdrop else [],
'poster_original': [],
'backdrop_original': [],
},
'year': show.get('year'),
'rating': {
'trakt': float(rating) / 10
},
}
return dict((k, v) for k, v in show_data.iteritems() if v)
@@ -1,216 +0,0 @@
from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.media.show.providers.base import ShowProvider
log = CPLog(__name__)
autoload = 'Xem'
class Xem(ShowProvider):
'''
Mapping Information
===================
Single
------
You will need the id / identifier of the show e.g. tvdb-id for American Dad! is 73141
the origin is the name of the site/entity the episode, season (and/or absolute) numbers are based on
http://thexem.de/map/single?id=&origin=&episode=&season=&absolute=
episode, season and absolute are all optional but it wont work if you don't provide either episode and season OR absolute in
addition you can provide destination as the name of the wished destination, if not provided it will output all available
When a destination has two or more addresses another entry will be added as _ ... for now the second address gets the index "2"
(the first index is omitted) and so on
http://thexem.de/map/single?id=7529&origin=anidb&season=1&episode=2&destination=trakt
{
"result":"success",
"data":{
"trakt": {"season":1,"episode":3,"absolute":3},
"trakt_2":{"season":1,"episode":4,"absolute":4}
},
"message":"single mapping for 7529 on anidb."
}
All
---
Basically same as "single" just a little easier
The origin address is added into the output too!!
http://thexem.de/map/all?id=7529&origin=anidb
All Names
---------
Get all names xem has to offer
non optional params: origin(an entity string like 'tvdb')
optional params: season, language
- season: a season number or a list like: 1,3,5 or a compare operator like ne,gt,ge,lt,le,eq and a season number. default would
return all
- language: a language string like 'us' or 'jp' default is all
- defaultNames: 1(yes) or 0(no) should the default names be added to the list ? default is 0(no)
http://thexem.de/map/allNames?origin=tvdb&season=le1
{
"result": "success",
"data": {
"248812": ["Dont Trust the Bitch in Apartment 23", "Don't Trust the Bitch in Apartment 23"],
"257571": ["Nazo no Kanojo X"],
"257875": ["Lupin III - Mine Fujiko to Iu Onna", "Lupin III Fujiko to Iu Onna", "Lupin the Third - Mine Fujiko to Iu Onna"]
},
"message": ""
}
'''
def __init__(self):
addEvent('show.info', self.getShowInfo, priority = 5)
addEvent('episode.info', self.getEpisodeInfo, priority = 5)
self.config = {}
self.config['base_url'] = "http://thexem.de"
self.config['url_single'] = u"%(base_url)s/map/single?" % self.config
self.config['url_all'] = u"%(base_url)s/map/all?" % self.config
self.config['url_names'] = u"%(base_url)s/map/names?" % self.config
self.config['url_all_names'] = u"%(base_url)s/map/allNames?" % self.config
def getShowInfo(self, identifiers = None):
if self.isDisabled():
return {}
identifier = identifiers.get('thetvdb')
if not identifier:
return {}
cache_key = 'xem.cache.%s' % identifier
log.debug('Getting showInfo: %s', cache_key)
result = self.getCache(cache_key) or {}
if result:
return result
result['seasons'] = {}
# Create season/episode and absolute mappings
url = self.config['url_all'] + "id=%s&origin=tvdb" % tryUrlencode(identifier)
response = self.getJsonData(url)
if response and response.get('result') == 'success':
data = response.get('data', None)
self.parseMaps(result, data)
# Create name alias mappings
url = self.config['url_names'] + "id=%s&origin=tvdb" % tryUrlencode(identifier)
response = self.getJsonData(url)
if response and response.get('result') == 'success':
data = response.get('data', None)
self.parseNames(result, data)
self.setCache(cache_key, result)
return result
def getEpisodeInfo(self, identifiers = None, params = {}):
episode_num = params.get('episode_number', None)
if episode_num is None:
return False
season_num = params.get('season_number', None)
if season_num is None:
return False
result = self.getShowInfo(identifiers)
if not result:
return False
# Find season
if season_num not in result['seasons']:
return False
season = result['seasons'][season_num]
# Find episode
if episode_num not in season['episodes']:
return False
return season['episodes'][episode_num]
def parseMaps(self, result, data, master = 'tvdb'):
'''parses xem map and returns a custom formatted dict map
To retreive map for scene:
if 'scene' in map['map_episode'][1][1]:
print map['map_episode'][1][1]['scene']['season']
'''
if not isinstance(data, list):
return
for episode_map in data:
origin = episode_map.pop(master, None)
if origin is None:
continue # No master origin to map to
o_season = origin['season']
o_episode = origin['episode']
# Create season info
if o_season not in result['seasons']:
result['seasons'][o_season] = {}
season = result['seasons'][o_season]
if 'episodes' not in season:
season['episodes'] = {}
# Create episode info
if o_episode not in season['episodes']:
season['episodes'][o_episode] = {}
episode = season['episodes'][o_episode]
episode['episode_map'] = episode_map
def parseNames(self, result, data):
result['title_map'] = data.pop('all', None)
for season, title_map in data.items():
season = int(season)
# Create season info
if season not in result['seasons']:
result['seasons'][season] = {}
season = result['seasons'][season]
season['title_map'] = title_map
def isDisabled(self):
if __name__ == '__main__':
return False
if self.conf('enabled'):
return False
else:
return True
config = [{
'name': 'xem',
'groups': [
{
'tab': 'providers',
'name': 'xem',
'label': 'TheXem',
'hidden': True,
'description': 'Used for all calls to TheXem.',
'options': [
{
'name': 'enabled',
'default': True,
'label': 'Enabled',
},
],
},
],
}]
@@ -1,51 +0,0 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media._base.providers.nzb.binsearch import Base
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.environment import Env
log = CPLog(__name__)
autoload = 'BinSearch'
class BinSearch(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
def buildUrl(self, media, quality):
query = tryUrlencode({
'q': fireEvent('media.search_query', media, single = True),
'm': 'n',
'max': 400,
'adv_age': Env.setting('retention', 'nzb'),
'adv_sort': 'date',
'adv_col': 'on',
'adv_nfo': 'on',
'minsize': quality.get('size_min'),
'maxsize': quality.get('size_max'),
})
return query
class Episode(EpisodeProvider, Base):
def buildUrl(self, media, quality):
query = tryUrlencode({
'q': fireEvent('media.search_query', media, single = True),
'm': 'n',
'max': 400,
'adv_age': Env.setting('retention', 'nzb'),
'adv_sort': 'date',
'adv_col': 'on',
'adv_nfo': 'on',
'minsize': quality.get('size_min'),
'maxsize': quality.get('size_max'),
})
return query
@@ -1,49 +0,0 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.event import fireEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media._base.providers.nzb.newznab import Base
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
log = CPLog(__name__)
autoload = 'Newznab'
class Newznab(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
def buildUrl(self, media, host):
related = fireEvent('library.related', media, single = True)
identifier = fireEvent('library.identifier', media, single = True)
query = tryUrlencode({
't': 'tvsearch',
'apikey': host['api_key'],
'q': related['show']['title'],
'season': identifier['season'],
'extended': 1
})
return query
class Episode(EpisodeProvider, Base):
def buildUrl(self, media, host):
related = fireEvent('library.related', media, single = True)
identifier = fireEvent('library.identifier', media, single = True)
query = tryUrlencode({
't': 'tvsearch',
'apikey': host['api_key'],
'q': related['show']['title'],
'season': identifier['season'],
'ep': identifier['episode'],
'extended': 1
})
return query
@@ -1,52 +0,0 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.core.media._base.providers.nzb.nzbclub import Base
log = CPLog(__name__)
autoload = 'NZBClub'
class NZBClub(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
def buildUrl(self, media):
q = tryUrlencode({
'q': fireEvent('media.search_query', media, single = True),
})
query = tryUrlencode({
'ig': 1,
'rpp': 200,
'st': 5,
'sp': 1,
'ns': 1,
})
return '%s&%s' % (q, query)
class Episode(EpisodeProvider, Base):
def buildUrl(self, media):
q = tryUrlencode({
'q': fireEvent('media.search_query', media, single = True),
})
query = tryUrlencode({
'ig': 1,
'rpp': 200,
'st': 5,
'sp': 1,
'ns': 1,
})
return '%s&%s' % (q, query)
@@ -1,51 +0,0 @@
from couchpotato import Env
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.core.media._base.providers.nzb.nzbindex import Base
log = CPLog(__name__)
autoload = 'NzbIndex'
class NzbIndex(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
def buildUrl(self, media, quality):
query = tryUrlencode({
'q': fireEvent('media.search_query', media, single = True),
'age': Env.setting('retention', 'nzb'),
'sort': 'agedesc',
'minsize': quality.get('size_min'),
'maxsize': quality.get('size_max'),
'rating': 1,
'max': 250,
'more': 1,
'complete': 1,
})
return query
class Episode(EpisodeProvider, Base):
def buildUrl(self, media, quality):
query = tryUrlencode({
'q': fireEvent('media.search_query', media, single = True),
'age': Env.setting('retention', 'nzb'),
'sort': 'agedesc',
'minsize': quality.get('size_min'),
'maxsize': quality.get('size_max'),
'rating': 1,
'max': 250,
'more': 1,
'complete': 1,
})
return query
@@ -1,36 +0,0 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.core.media._base.providers.torrent.bithdtv import Base
log = CPLog(__name__)
autoload = 'BiTHDTV'
class BiTHDTV(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
def buildUrl(self, media):
query = tryUrlencode({
'search': fireEvent('media.search_query', media, single = True),
'cat': 12 # Season cat
})
return query
class Episode(EpisodeProvider, Base):
def buildUrl(self, media):
query = tryUrlencode({
'search': fireEvent('media.search_query', media, single = True),
'cat': 10 # Episode cat
})
return query
@@ -1,41 +0,0 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.core.media._base.providers.torrent.bitsoup import Base
log = CPLog(__name__)
autoload = 'Bitsoup'
class Bitsoup(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
# For season bundles, bitsoup currently only has one category
def buildUrl(self, media, quality):
query = tryUrlencode({
'search': fireEvent('media.search_query', media, single = True),
'cat': 45 # TV-Packs Category
})
return query
class Episode(EpisodeProvider, Base):
cat_ids = [
([42], ['hdtv_720p', 'webdl_720p', 'webdl_1080p', 'bdrip_1080p', 'bdrip_720p', 'brrip_1080p', 'brrip_720p']),
([49], ['hdtv_sd', 'webdl_480p'])
]
cat_backup_id = 0
def buildUrl(self, media, quality):
query = tryUrlencode({
'search': fireEvent('media.search_query', media, single = True),
'cat': self.getCatId(quality['identifier'])[0],
})
return query
@@ -1,37 +0,0 @@
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.core.media._base.providers.torrent.iptorrents import Base
log = CPLog(__name__)
autoload = 'IPTorrents'
class IPTorrents(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
# TODO come back to this later, a better quality system needs to be created
cat_ids = [
([65], [
'bluray_1080p', 'bluray_720p',
'bdrip_1080p', 'bdrip_720p',
'brrip_1080p', 'brrip_720p',
'webdl_1080p', 'webdl_720p', 'webdl_480p',
'hdtv_720p', 'hdtv_sd'
]),
]
class Episode(EpisodeProvider, Base):
# TODO come back to this later, a better quality system needs to be created
cat_ids = [
([5], ['hdtv_720p', 'webdl_720p', 'webdl_1080p']),
([4, 78, 79], ['hdtv_sd'])
]
@@ -1,27 +0,0 @@
from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.core.media._base.providers.torrent.publichd import Base
log = CPLog(__name__)
autoload = 'PublicHD'
class PublicHD(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
def buildUrl(self, media):
return fireEvent('media.search_query', media, single = True)
class Episode(EpisodeProvider, Base):
def buildUrl(self, media):
return fireEvent('media.search_query', media, single = True)
@@ -1,60 +0,0 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.event import fireEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.core.media._base.providers.torrent.sceneaccess import Base
log = CPLog(__name__)
autoload = 'SceneAccess'
class SceneAccess(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
cat_ids = [
([26], ['hdtv_sd', 'hdtv_720p', 'webdl_720p', 'webdl_1080p']),
]
def buildUrl(self, media, quality):
url = self.urls['archive'] % (
self.getCatId(quality['identifier'])[0],
self.getCatId(quality['identifier'])[0]
)
arguments = tryUrlencode({
'search': fireEvent('media.search_query', media, single = True),
'method': 3,
})
query = "%s&%s" % (url, arguments)
return query
class Episode(EpisodeProvider, Base):
cat_ids = [
([27], ['hdtv_720p', 'webdl_720p', 'webdl_1080p']),
([17, 11], ['hdtv_sd'])
]
def buildUrl(self, media, quality):
url = self.urls['search'] % (
self.getCatId(quality['identifier'])[0],
self.getCatId(quality['identifier'])[0]
)
arguments = tryUrlencode({
'search': fireEvent('media.search_query', media, single = True),
'method': 3,
})
query = "%s&%s" % (url, arguments)
return query
@@ -1,46 +0,0 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.core.media._base.providers.torrent.thepiratebay import Base
log = CPLog(__name__)
autoload = 'ThePirateBay'
class ThePirateBay(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
cat_ids = [
([208], ['hdtv_720p', 'webdl_720p', 'webdl_1080p']),
([205], ['hdtv_sd'])
]
def buildUrl(self, media, page, cats):
return (
tryUrlencode('"%s"' % fireEvent('media.search_query', media, single = True)),
page,
','.join(str(x) for x in cats)
)
class Episode(EpisodeProvider, Base):
cat_ids = [
([208], ['hdtv_720p', 'webdl_720p', 'webdl_1080p']),
([205], ['hdtv_sd'])
]
def buildUrl(self, media, page, cats):
return (
tryUrlencode('"%s"' % fireEvent('media.search_query', media, single = True)),
page,
','.join(str(x) for x in cats)
)
@@ -1,34 +0,0 @@
from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.core.media._base.providers.torrent.torrentday import Base
log = CPLog(__name__)
autoload = 'TorrentDay'
class TorrentDay(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
cat_ids = [
([14], ['hdtv_sd', 'hdtv_720p', 'webdl_720p', 'webdl_1080p']),
]
def buildUrl(self, media):
return fireEvent('media.search_query', media, single = True)
class Episode(EpisodeProvider, Base):
cat_ids = [
([7], ['hdtv_720p', 'webdl_720p', 'webdl_1080p']),
([2], [24], [26], ['hdtv_sd'])
]
def buildUrl(self, media):
return fireEvent('media.search_query', media, single = True)
@@ -1,42 +0,0 @@
from couchpotato import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.core.media._base.providers.torrent.torrentleech import Base
log = CPLog(__name__)
autoload = 'TorrentLeech'
class TorrentLeech(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
cat_ids = [
([27], ['hdtv_sd', 'hdtv_720p', 'webdl_720p', 'webdl_1080p']),
]
def buildUrl(self, media, quality):
return (
tryUrlencode(fireEvent('media.search_query', media, single = True)),
self.getCatId(quality['identifier'])[0]
)
class Episode(EpisodeProvider, Base):
cat_ids = [
([32], ['hdtv_720p', 'webdl_720p', 'webdl_1080p']),
([26], ['hdtv_sd'])
]
def buildUrl(self, media, quality):
return (
tryUrlencode(fireEvent('media.search_query', media, single = True)),
self.getCatId(quality['identifier'])[0]
)
@@ -1,38 +0,0 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.core.media._base.providers.torrent.torrentpotato import Base
log = CPLog(__name__)
autoload = 'TorrentPotato'
class TorrentPotato(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
def buildUrl(self, media, host):
arguments = tryUrlencode({
'user': host['name'],
'passkey': host['pass_key'],
'search': fireEvent('media.search_query', media, single = True)
})
return '%s?%s' % (host['host'], arguments)
class Episode(EpisodeProvider, Base):
def buildUrl(self, media, host):
arguments = tryUrlencode({
'user': host['name'],
'passkey': host['pass_key'],
'search': fireEvent('media.search_query', media, single = True)
})
return '%s?%s' % (host['host'], arguments)
@@ -1,52 +0,0 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.core.media._base.providers.torrent.torrentshack import Base
log = CPLog(__name__)
autoload = 'TorrentShack'
class TorrentShack(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
# TorrentShack tv season search categories
# TV-SD Pack - 980
# TV-HD Pack - 981
# Full Blu-ray - 970
cat_ids = [
([980], ['hdtv_sd']),
([981], ['hdtv_720p', 'webdl_720p', 'webdl_1080p', 'bdrip_1080p', 'bdrip_720p', 'brrip_1080p', 'brrip_720p']),
([970], ['bluray_1080p', 'bluray_720p']),
]
cat_backup_id = 980
def buildUrl(self, media, quality):
query = (tryUrlencode(fireEvent('media.search_query', media, single = True)),
self.getCatId(quality['identifier'])[0],
self.getSceneOnly())
return query
class Episode(EpisodeProvider, Base):
# TorrentShack tv episode search categories
# TV/x264-HD - 600
# TV/x264-SD - 620
# TV/DVDrip - 700
cat_ids = [
([600], ['hdtv_720p', 'webdl_720p', 'webdl_1080p', 'bdrip_1080p', 'bdrip_720p', 'brrip_1080p', 'brrip_720p']),
([620], ['hdtv_sd'])
]
cat_backup_id = 620
def buildUrl(self, media, quality):
query = (tryUrlencode(fireEvent('media.search_query', media, single = True)),
self.getCatId(quality['identifier'])[0],
self.getSceneOnly())
return query
@@ -1,152 +0,0 @@
from couchpotato import fireEvent, get_db, Env
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEventAsync
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.searcher.base import SearcherBase
from couchpotato.core.media._base.searcher.main import SearchSetupError
from couchpotato.core.media.show import ShowTypeBase
log = CPLog(__name__)
autoload = 'EpisodeSearcher'
class EpisodeSearcher(SearcherBase, ShowTypeBase):
type = 'episode'
in_progress = False
def __init__(self):
super(EpisodeSearcher, self).__init__()
addEvent('%s.searcher.all' % self.getType(), self.searchAll)
addEvent('%s.searcher.single' % self.getType(), self.single)
addEvent('searcher.correct_release', self.correctRelease)
addApiView('%s.searcher.full_search' % self.getType(), self.searchAllView, docs = {
'desc': 'Starts a full search for all wanted shows',
})
addApiView('%s.searcher.single' % self.getType(), self.singleView)
def searchAllView(self, **kwargs):
fireEventAsync('%s.searcher.all' % self.getType(), manual = True)
return {
'success': not self.in_progress
}
def searchAll(self, manual = False):
pass
def singleView(self, media_id, **kwargs):
db = get_db()
media = db.get('id', media_id)
return {
'result': fireEvent('%s.searcher.single' % self.getType(), media, single = True)
}
def single(self, media, profile = None, quality_order = None, search_protocols = None, manual = False):
db = get_db()
related = fireEvent('library.related', media, single = True)
# TODO search_protocols, profile, quality_order can be moved to a base method
# Find out search type
try:
if not search_protocols:
search_protocols = fireEvent('searcher.protocols', single = True)
except SearchSetupError:
return
if not profile and related['show']['profile_id']:
profile = db.get('id', related['show']['profile_id'])
if not quality_order:
quality_order = fireEvent('quality.order', single = True)
# TODO: check episode status
# 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'])
# return
ret = False
has_better_quality = None
found_releases = []
too_early_to_search = []
releases = fireEvent('release.for_media', media['_id'], single = True)
query = fireEvent('library.query', media, condense = False, single = True)
index = 0
for q_identifier in profile.get('qualities'):
quality_custom = {
'quality': q_identifier,
'finish': profile['finish'][index],
'wait_for': profile['wait_for'][index],
'3d': profile['3d'][index] if profile.get('3d') else False
}
has_better_quality = 0
# See if better quality is available
for release in releases:
if quality_order.index(release['quality']) <= quality_order.index(q_identifier) and release['status'] not in ['available', 'ignored', 'failed']:
has_better_quality += 1
# Don't search for quality lower then already available.
if has_better_quality is 0:
log.info('Searching for %s in %s', (query, q_identifier))
quality = fireEvent('quality.single', identifier = q_identifier, single = True)
quality['custom'] = quality_custom
results = fireEvent('searcher.search', search_protocols, media, quality, single = True)
if len(results) == 0:
log.debug('Nothing found for %s in %s', (query, q_identifier))
# Add them to this movie releases list
found_releases += fireEvent('release.create_from_search', results, media, quality, single = True)
# Try find a valid result and download it
if fireEvent('release.try_download_result', results, media, quality, single = True):
ret = True
# Remove releases that aren't found anymore
for release in releases:
if release.get('status') == 'available' 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', (q_identifier, query))
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, query))
def correctRelease(self, release = None, media = None, quality = None, **kwargs):
if media.get('type') != 'show.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
@@ -1,172 +0,0 @@
from couchpotato import get_db, Env
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.searcher.base import SearcherBase
from couchpotato.core.media.movie.searcher import SearchSetupError
from couchpotato.core.media.show import ShowTypeBase
log = CPLog(__name__)
autoload = 'SeasonSearcher'
class SeasonSearcher(SearcherBase, ShowTypeBase):
type = 'season'
in_progress = False
def __init__(self):
super(SeasonSearcher, self).__init__()
addEvent('%s.searcher.all' % self.getType(), self.searchAll)
addEvent('%s.searcher.single' % self.getType(), self.single)
addEvent('searcher.correct_release', self.correctRelease)
addApiView('%s.searcher.full_search' % self.getType(), self.searchAllView, docs = {
'desc': 'Starts a full search for all wanted seasons',
})
def searchAllView(self, **kwargs):
fireEventAsync('%s.searcher.all' % self.getType(), manual = True)
return {
'success': not self.in_progress
}
def searchAll(self, manual = False):
pass
def single(self, media, profile = None, quality_order = None, search_protocols = None, manual = False):
db = get_db()
related = fireEvent('library.related', media, single = True)
# TODO search_protocols, profile, quality_order can be moved to a base method
# Find out search type
try:
if not search_protocols:
search_protocols = fireEvent('searcher.protocols', single = True)
except SearchSetupError:
return
if not profile and related['show']['profile_id']:
profile = db.get('id', related['show']['profile_id'])
if not quality_order:
quality_order = fireEvent('quality.order', single = True)
# Find 'active' episodes
episodes = related['episodes']
episodes_active = []
for episode in episodes:
if episode.get('status') != 'active':
continue
episodes_active.append(episode)
if len(episodes_active) == len(episodes):
# All episodes are 'active', try and search for full season
if self.search(media, profile, quality_order, search_protocols):
# Success, end season search
return True
else:
log.info('Unable to find season pack, searching for individual episodes...')
# Search for each episode individually
for episode in episodes_active:
fireEvent('show.episode.searcher.single', episode, profile, quality_order, search_protocols, manual)
# TODO (testing) only grab one episode
return True
return True
def search(self, media, profile, quality_order, search_protocols):
# TODO: check episode status
# 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'])
# return
ret = False
has_better_quality = None
found_releases = []
too_early_to_search = []
releases = fireEvent('release.for_media', media['_id'], single = True)
query = fireEvent('library.query', media, condense = False, single = True)
index = 0
for q_identifier in profile.get('qualities'):
quality_custom = {
'quality': q_identifier,
'finish': profile['finish'][index],
'wait_for': profile['wait_for'][index],
'3d': profile['3d'][index] if profile.get('3d') else False
}
has_better_quality = 0
# See if better quality is available
for release in releases:
if quality_order.index(release['quality']) <= quality_order.index(q_identifier) and release['status'] not in ['available', 'ignored', 'failed']:
has_better_quality += 1
# Don't search for quality lower then already available.
if has_better_quality is 0:
log.info('Searching for %s in %s', (query, q_identifier))
quality = fireEvent('quality.single', identifier = q_identifier, single = True)
quality['custom'] = quality_custom
results = fireEvent('searcher.search', search_protocols, media, quality, single = True)
if len(results) == 0:
log.debug('Nothing found for %s in %s', (query, q_identifier))
# Add them to this movie releases list
found_releases += fireEvent('release.create_from_search', results, media, quality, single = True)
# Try find a valid result and download it
if fireEvent('release.try_download_result', results, media, quality, single = True):
ret = True
# Remove releases that aren't found anymore
for release in releases:
if release.get('status') == 'available' 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', (q_identifier, query))
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, query))
return len(found_releases) > 0
def correctRelease(self, release = None, media = None, quality = None, **kwargs):
if media.get('type') != 'show.season':
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
@@ -1,88 +0,0 @@
from couchpotato import get_db
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
from couchpotato.core.helpers.variable import getTitle
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.searcher.base import SearcherBase
from couchpotato.core.media._base.searcher.main import SearchSetupError
from couchpotato.core.media.show import ShowTypeBase
log = CPLog(__name__)
autoload = 'ShowSearcher'
class ShowSearcher(SearcherBase, ShowTypeBase):
type = 'show'
in_progress = False
def __init__(self):
super(ShowSearcher, self).__init__()
addEvent('%s.searcher.all' % self.getType(), self.searchAll)
addEvent('%s.searcher.single' % self.getType(), self.single)
addEvent('searcher.get_search_title', self.getSearchTitle)
addApiView('%s.searcher.full_search' % self.getType(), self.searchAllView, docs = {
'desc': 'Starts a full search for all wanted episodes',
})
def searchAllView(self, **kwargs):
fireEventAsync('%s.searcher.all' % self.getType(), manual = True)
return {
'success': not self.in_progress
}
def searchAll(self, manual = False):
pass
def single(self, media, search_protocols = None, manual = False):
# Find out search type
try:
if not search_protocols:
search_protocols = fireEvent('searcher.protocols', single = True)
except SearchSetupError:
return
if not media['profile_id'] or media['status'] == 'done':
log.debug('Show doesn\'t have a profile or already done, assuming in manage tab.')
return
show_title = fireEvent('media.search_query', media, condense = False, single = True)
fireEvent('notify.frontend', type = 'show.searcher.started.%s' % media['_id'], data = True, message = 'Searching for "%s"' % show_title)
show_tree = fireEvent('library.tree', media, single = True)
db = get_db()
profile = db.get('id', media['profile_id'])
quality_order = fireEvent('quality.order', single = True)
for season in show_tree.get('seasons', []):
if not season.get('info'):
continue
# Skip specials (and seasons missing 'number') for now
# TODO: set status for specials to skipped by default
if not season['info'].get('number'):
continue
# Check if full season can be downloaded
fireEvent('show.season.searcher.single', season, profile, quality_order, search_protocols, manual)
# TODO (testing) only snatch one season
return
fireEvent('notify.frontend', type = 'show.searcher.ended.%s' % media['_id'], data = True)
def getSearchTitle(self, media):
if media.get('type') != 'show':
related = fireEvent('library.related', media, single = True)
show = related['show']
else:
show = media
return getTitle(show)
+13 -13
View File
@@ -14,6 +14,7 @@ from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from .index import NotificationIndex, NotificationUnreadIndex
from couchpotato.environment import Env
from tornado.ioloop import IOLoop
log = CPLog(__name__)
@@ -110,11 +111,11 @@ class CoreNotifier(Notification):
if limit_offset:
splt = splitString(limit_offset)
limit = splt[0]
offset = 0 if len(splt) is 1 else splt[1]
results = db.get_many('notification', limit = limit, offset = offset, with_doc = True)
limit = tryInt(splt[0])
offset = tryInt(0 if len(splt) is 1 else splt[1])
results = db.all('notification', limit = limit, offset = offset, with_doc = True)
else:
results = db.get_many('notification', limit = 200, with_doc = True)
results = db.all('notification', limit = 200, with_doc = True)
notifications = []
for n in results:
@@ -148,16 +149,15 @@ class CoreNotifier(Notification):
def notify(self, message = '', data = None, listener = None):
if not data: data = {}
n = {
'_t': 'notification',
'time': int(time.time()),
}
try:
db = get_db()
data['notification_type'] = listener if listener else 'unknown'
n = {
'_t': 'notification',
'time': int(time.time()),
'message': toUnicode(message)
}
n['message'] = toUnicode(message)
if data.get('sticky'):
n['sticky'] = True
@@ -170,7 +170,7 @@ class CoreNotifier(Notification):
return True
except:
log.error('Failed notify: %s', traceback.format_exc())
log.error('Failed notify "%s": %s', (n, traceback.format_exc()))
def frontend(self, type = 'notification', data = None, message = None):
if not data: data = {}
@@ -190,7 +190,7 @@ class CoreNotifier(Notification):
while len(self.listeners) > 0 and not self.shuttingDown():
try:
listener, last_id = self.listeners.pop()
listener({
IOLoop.current().add_callback(listener, {
'success': True,
'result': [notification],
})

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