Compare commits

...

283 Commits

Author SHA1 Message Date
Ruud
821f68909d One up 2013-05-05 21:19:10 +02:00
Ruud
2b8dfed475 Merge branch 'refs/heads/master' into desktop
Conflicts:
	version.py
2013-05-05 20:31:28 +02:00
Ruud
0a749ce913 Merge branch 'refs/heads/develop' 2013-05-05 20:24:40 +02:00
Ruud
e6db505cf7 Restart notification request every 2 minutes 2013-05-05 20:15:23 +02:00
Ruud
9e8d6aaaa1 Delay notification start more on mobile 2013-05-05 20:14:54 +02:00
Ruud
e814b551b4 Delay fade loader after refreshing movie 2013-05-05 17:41:16 +02:00
Ruud
080da48223 Only attach imdb url when available 2013-05-05 17:28:02 +02:00
Faryn
897330e646 Include IMDb link to movie in Pushover notifications 2013-05-05 16:54:11 +02:00
Ruud
c4c7b5b1a9 Settings styling issues 2013-05-05 14:09:11 +02:00
Ruud
b90861bc63 Show if movie is in library again on search 2013-05-05 13:35:49 +02:00
Ruud
6d1297a85f Don't show double message when refreshing movie 2013-05-05 13:31:26 +02:00
Ruud
dfd2c33657 Extend files, not append 2013-05-05 10:15:19 +02:00
Ruud
f5af551325 Extend files, not append 2013-05-05 10:14:10 +02:00
Ruud
7aad27c3d2 Last message check 0 after first message 2013-05-03 23:05:17 +02:00
Ruud
60ff3b08d4 Last message check 0 after first message 2013-05-03 23:04:52 +02:00
Ruud
7a5588d5de Merge branch 'refs/heads/develop' 2013-05-03 22:51:35 +02:00
Ruud
56b6fbbe7f Backtotop button over log pagination 2013-05-03 22:51:06 +02:00
Ruud
46c408befb SImplify thumbslist 2013-05-03 22:29:33 +02:00
Ruud
6f808fc25a Only fire hide-scrollbar function on smaller resolutions 2013-05-03 22:27:25 +02:00
Ruud
4cba44fbb1 API notifications 2013-05-03 14:07:23 +02:00
Ruud
91c45bad71 Move source url to api 2013-05-02 15:13:57 +02:00
Ruud
a30caefc04 Cleanup wizard 2013-05-02 12:38:28 +02:00
Ruud
eb20fda878 Position checkbox 2013-05-02 12:37:54 +02:00
Ruud
4a5aa02e6c Remove userscript detection 2013-05-02 11:47:32 +02:00
Ruud
25b37ad915 Criticker userscript support 2013-05-02 11:32:26 +02:00
Ruud
bbcceb982a Reverse before merging dicts 2013-05-01 23:46:35 +02:00
Ruud
c41f5eb84d Give userscript window a proper height 2013-05-01 23:32:17 +02:00
Ruud
89dc9e90b2 Logo border outside header 2013-05-01 19:02:01 +02:00
Ruud
39b1dedf12 Updater message at the bottom 2013-05-01 18:30:52 +02:00
Ruud
be28820fb2 List and dropdown styling issues 2013-05-01 17:48:59 +02:00
Ruud
0654c8cf07 More header margin on mobile 2013-04-30 22:32:13 +02:00
Ruud
2f5cb81029 Optimize initial requests 2013-04-30 22:28:15 +02:00
Ruud
067d6e8514 Put link and symlink in helpers 2013-04-30 19:32:11 +02:00
mano3m
42e19e1e2b Replace linktastic with simple ctypes
Linktastic calls the command line interpreter to do linking. This
solution calls the windows API directly. This is faster and cleaner, but
most important of all: it doesn't cause a command window to popup every
time a link is made. This popup asks window focus and thus interrupts a
movie you are watching!!

Hopefully this also works on non-windows systems. I am unable to test
this so please let me know :)
2013-04-30 19:24:28 +02:00
Ruud
6b846b91b4 Thumblist title and action positioning 2013-04-30 19:18:59 +02:00
Ruud
5838a41813 Newznab, not enough api_keys. fix #1677 2013-04-30 18:20:33 +02:00
Ruud
3cd5513c0c Point source updater to new url 2013-04-30 18:11:44 +02:00
Ruud
bfdc8d1053 Don't remove version file 2013-04-30 18:04:58 +02:00
Ruud
30ec8216e1 Minify on backend 2013-04-30 13:17:03 +02:00
Ruud
12c3fc6ce3 Don't reverse result order 2013-04-30 11:13:47 +02:00
Ruud
7b3a1409d5 Force thumbnail view on home 2013-04-30 10:32:22 +02:00
Ruud
924bed06cb Rewrite font css 2013-04-30 00:29:25 +02:00
Ruud
8b0aa7a6b3 Initial mobile styling 2013-04-30 00:24:56 +02:00
Ruud
367c385fff Lowercase variables 2013-04-27 11:12:15 +02:00
Ruud
840efb1571 Merge branch 'develop' of git://github.com/clinton-hall/CouchPotatoServer into clinton-hall-develop 2013-04-27 11:09:30 +02:00
Ruud
9ba19d27a6 Combine status calls 2013-04-27 11:04:19 +02:00
Ruud
1d603e1ec2 Simplify event handling 2013-04-27 10:48:47 +02:00
Ruud
7818b43045 Release files add in bulk 2013-04-27 10:08:03 +02:00
Ruud
3936100000 Cache status calls 2013-04-27 09:42:29 +02:00
Clinton Hall
1a846b04ee Fix minor errors. Fix #1666 2013-04-27 11:26:52 +09:30
Ruud
384a355a53 Check release type by info 2013-04-26 23:48:54 +02:00
Ruud
58ad5c3938 Merge branch 'develop_symlink' of git://github.com/mano3m/CouchPotatoServer into mano3m-develop_symlink 2013-04-26 19:59:47 +02:00
mano3m
6ee68d1418 Fix getDownloadInfo 2013-04-26 19:37:47 +02:00
Ruud
6e45c14ac5 Don't download html files as trailers. fixes #1658 2013-04-26 18:49:39 +02:00
Ruud
e786c9c79a Use sets for ignored words. fixes #1657 2013-04-26 18:11:18 +02:00
Ruud
518ac16814 Use lowercase variable 2013-04-26 16:44:02 +02:00
Ruud
1d07eafa83 Merge branch 'dev-nzbget' of git://github.com/clinton-hall/CouchPotatoServer into clinton-hall-dev-nzbget 2013-04-26 16:42:24 +02:00
Ruud
1600b6d0ea NZBGet set advanced and description 2013-04-26 16:36:42 +02:00
Ruud
6de3a7246e Merge branch 'develop_nzbgetusername' of git://github.com/mano3m/CouchPotatoServer into mano3m-develop_nzbgetusername 2013-04-26 16:29:36 +02:00
Ruud
cbd29df52a Update to_go even if movie isn't found in manage. 2013-04-26 16:27:36 +02:00
clinton-hall
92998bafc8 write unique id to nzbget params
My mistake. Fixed now.

Yeah... sorry ;)
This does work for check_snatched... Marks as busy, or failed etc.

keep consistent release table format

fix check_snatched

correctly parse the NZBGet Parameters and Pass status.downloader

remove downloader and fix id

My mistake. Fixed now.

Yeah... sorry ;)
This does work for check_snatched... Marks as busy, or failed etc.

keep consistent release table format

fix check_snatched

correctly parse the NZBGet Parameters and Pass status.downloader

remove downloader and fix id
2013-04-26 16:03:20 +09:30
mano3m
1022753213 Add username to nzbget downloader
For raspberry pi a different username than normal is required. Fixes
#1652
2013-04-24 22:19:34 +02:00
mano3m
b85942989d Standardise failed status
apply the failed status in case of a manual re-add and automatic try
next release
2013-04-21 23:46:21 +02:00
Clinton Hall
f2f43a2231 Wont delete the "from" (sub)folders, "movie" folder or "destination" folder at this time. These should be deleted after the renaming... at this stage we should only be deleting any "older releases" 2013-04-20 11:42:53 +09:30
mano3m
2979a8edec Cleanup 2013-04-19 21:28:53 +02:00
Ruud
0e90739786 Update to Tornado 3.0 2013-04-19 14:49:27 +02:00
mano3m
185a530b59 only link for torrents not nzbs 2013-04-17 21:19:50 +02:00
Ruud
4f6b31d14a Add login check to torrentleech. closes #1635 2013-04-16 21:15:38 +02:00
Ruud
f1dde5c925 Merge branch 'refs/heads/develop' 2013-04-14 11:09:32 +02:00
Ruud
64afa3701a Add headers for IPTorrents. fix #1558
Thanks @got3nks
2013-04-14 11:08:50 +02:00
Ruud
cb0b6614c6 move_symlink proper function 2013-04-12 21:14:45 +02:00
Kyle Klein
177063d39c Add File Action Move & Sym link 2013-04-12 21:10:30 +02:00
Ruud
be595aba91 Loading in movie lists 2013-04-12 20:57:00 +02:00
Ruud
66d9d853af NZB ids not persistent in new sessions 2013-04-08 21:44:36 +02:00
Ruud
95a68af795 Debug logs 2013-04-08 21:40:17 +02:00
Ruud
c1937ea71f linktastic clenaup 2013-04-08 21:10:46 +02:00
Ruud
a7bd8c822a Simplify nonblocking requests 2013-04-08 20:55:42 +02:00
Ruud
0eff4f0096 Merge branch 'master' of github.com:RuudBurger/CouchPotatoServer 2013-04-05 23:59:56 +02:00
Ruud
4d7fa08805 Merge branch 'refs/heads/develop' 2013-04-05 23:57:54 +02:00
Ruud
5fd4312ff8 Use simpler file.cache 2013-04-05 23:50:28 +02:00
Ruud
a600430be4 file_action cleanup
tag ignored/failed/renamed with custom file
2013-04-05 21:56:33 +02:00
Ruud
f77b598899 Merge branch 'links' of git://github.com/jkaberg/CouchPotatoServer into jkaberg-links 2013-04-05 18:00:17 +02:00
Ruud
ac045539d1 Don't move or delete anything in status check 2013-04-05 17:51:45 +02:00
Ruud
5b0fa9054b Put renaming started lower 2013-04-05 17:51:19 +02:00
Joel Kåberg
3c2a00b17b add copy and move to file actions 2013-04-05 14:21:15 +02:00
Joel Kåberg
47ddf31f76 ruud was bulling me ;) 2013-04-05 14:07:10 +02:00
Joel Kåberg
57ae06e139 initial link support 2013-04-05 13:48:09 +02:00
Ruud
7f4373e000 Traceback import missing 2013-04-05 12:08:45 +02:00
mano3m
63609bb52c Fix Transmission 2013-04-05 00:09:23 +02:00
Ruud
f0af184262 Merge branch 'refs/heads/develop' 2013-04-02 11:32:20 +02:00
Ruud
72cc3576d3 Use @mano3m code to check download_info 2013-04-01 22:51:34 +02:00
Ruud
3fe7d2ea15 Download id cleanup 2013-04-01 21:18:29 +02:00
Ruud
8eed54f1f7 Merge branch 'develop_dwnlodid_complete' of git://github.com/mano3m/CouchPotatoServer into mano3m-develop_dwnlodid_complete
Conflicts:
	couchpotato/core/downloaders/transmission/__init__.py
	couchpotato/core/downloaders/transmission/main.py
2013-04-01 20:46:36 +02:00
Ruud
c7ee8a0635 Don't use object in correctmovie event 2013-04-01 20:11:49 +02:00
Ruud
33a6a7d3a0 CP Provider API Identifier 2013-04-01 17:31:04 +02:00
mano3m
2851781a72 Move around items between scanner and renamer 2013-04-01 13:03:05 +02:00
mano3m
45b9919f67 Added some debugging info 2013-04-01 10:16:30 +02:00
Ruud
207e846ae6 Check crons after saving settings. fix #1556 & #1557 2013-04-01 00:06:15 +02:00
Ruud
a83c276aa2 Schedule start normal 2013-03-31 23:50:47 +02:00
Ruud
4cdb99a383 Add dvdscreen to screener quality. fix #1555 2013-03-31 22:42:27 +02:00
mano3m
8fe60a893c Update of uTorrent 2013-03-31 18:53:19 +02:00
Ruud
0c44c48628 Notification test failed. closes #1561
Thanks @FredrikWendt
2013-03-31 11:12:37 +02:00
mano3m
6a18e546ca Add and make use of renamer.scanfolder in downloaders
This is the next step in closing the loop between the downloaders and CPS. The download_id and folder from the downloader are used to find the downloaded files and start the renamer. This is done by adding an additional API call: renamer.scanfolder.

I tested this for SabNZBd only (!) and everything works as expected.

I also added transmission with thanks @manusfreedom for setting this up in f1cf0d91da. @manusfreedom, please check if this works as expected. Note that transmission now has a feature which is not in the other torrent providers: it waits until the seed ratio is met and then removes the torrent. I opened a topic in the forum to discuss how we want to deal with torrents: https://couchpota.to/forum/thread-1704.html
2013-03-31 10:49:40 +02:00
Ruud
4cedccb178 Move over html template 2013-03-29 12:39:15 +01:00
Ruud
eab9a735a9 Show real transmission error. 2013-03-29 12:39:06 +01:00
Ruud
1df05cf344 Don't use directory when it's empty. fix #1448 2013-03-28 21:51:42 +01:00
Ruud
843ff0eabc Add some default Newznab providers 2013-03-26 22:02:43 +01:00
Ruud
5a23be2224 Merge branch 'refs/heads/develop' 2013-03-26 21:42:25 +01:00
Ruud
45c8817c62 Always show release helper. fix #1549 2013-03-26 21:42:07 +01:00
Ruud
7f87b255f9 Merge branch 'refs/heads/develop' 2013-03-26 21:10:27 +01:00
Ruud
665c84c6de Ignore done_status on re-add. fix #1547 2013-03-25 23:00:12 +01:00
Ruud
b91a077c91 Easier "get next best release" buttons 2013-03-25 22:54:22 +01:00
Ruud
59b924efe7 Tweak button styling 2013-03-25 22:53:48 +01:00
Ruud
730718a396 Link to country codes for subtitles 2013-03-25 12:24:54 +01:00
Ruud
3c0edc0d6a Clean out more words before search.
Thanks to @jkaberg
2013-03-25 12:09:32 +01:00
Ruud
7c234ab7e9 Simplify notification providers 2013-03-25 11:01:31 +01:00
Ruud
b82319cb54 Use existing Trakt auth settings and other cleanup 2013-03-25 10:48:59 +01:00
Ruud
6685495400 Merge branch 'trakt_notify' of git://github.com/EchelonFour/CouchPotatoServer into EchelonFour-trakt_notify 2013-03-25 09:59:51 +01:00
Ruud
b216589e88 Use normal ID, not extended. fix #1543 2013-03-25 09:58:39 +01:00
Ruud
744aa153f6 Merge branch 'develop_720p1080p' of git://github.com/mano3m/CouchPotatoServer into mano3m-develop_720p1080p 2013-03-25 09:11:23 +01:00
Ruud
67612fce98 Use label on combined setting 2013-03-24 20:27:38 +01:00
Ruud
72ba1a173c Make letterboxd multi-watchlist 2013-03-24 20:18:49 +01:00
Ruud
989e217775 Merge branch 'letterboxd-importer' of git://github.com/himynameisjonas/CouchPotatoServer into himynameisjonas-letterboxd-importer 2013-03-24 19:57:15 +01:00
Ruud
b0d556c8eb Try and find release status by ID instead of name. closes #1511
Thanks to @mano3m
2013-03-24 19:54:11 +01:00
Jonas Forsberg
1a54d8fad9 Letterboxd wishlist importer 2013-03-23 11:49:40 +01:00
mano3m
f9ace29cab Fix the names of quality identifiers
720P should be 720p, 1080P should be 1080p. Ref
http://en.wikipedia.org/wiki/720p and http://en.wikipedia.org/wiki/1080p

Note that this update only changes anything for new databases. For as
far as I can see the change for existing databases is minimal.
2013-03-23 00:40:38 +01:00
Ruud
a97570027d Prevent null in boolean column. fix #1374 2013-03-22 22:31:49 +01:00
Ruud
de36faa0a7 DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS 2013-03-22 21:59:28 +01:00
Ruud
19641bd897 Give the scanner some rest when to many threads 2013-03-20 22:47:14 +01:00
Ruud
2c64641a1b Prepend lists when merging event objects 2013-03-20 22:45:31 +01:00
Ruud
5ac1118db3 Merge branch 'refs/heads/develop' 2013-03-20 20:32:57 +01:00
Ruud
717b88b5fe Force pushalot image refresh 2013-03-20 20:30:34 +01:00
Ruud
158a7fc311 Optimize PNGs 2013-03-20 19:47:48 +01:00
Ruud
2c46279617 Merge branch 'refs/heads/develop' 2013-03-20 19:37:15 +01:00
Ruud
b843d5f13b General notification icons 2013-03-20 19:35:11 +01:00
Ruud
4aff3f0495 Add score per provider. closes #1512 2013-03-20 08:50:43 +01:00
Ruud
4406f133b9 CAPITALIZE MOTHAFAAACKAAH! 2013-03-19 23:28:51 +01:00
Ruud
572dfd529e Shorten automation description 2013-03-19 23:26:55 +01:00
mano3m
2cb6ddfe9a Add automation genre checking
With this commit you can set requirements to the genres of movie
automation downloading. Required sets e.g. Action&Crime and/or ignored
sets e.g. Romance&Comedy.
2013-03-19 23:16:25 +01:00
Ruud
250236bd25 Option to force search 2013-03-19 23:12:47 +01:00
Prinz23
7f24563bba Add Advanced Option to deactivate "Too early to search for ..."
Advanced Option: Check Released
2013-03-19 22:58:18 +01:00
Ruud
5d6a9ad2d0 Merge branch 'refs/heads/develop' 2013-03-19 22:55:39 +01:00
Ruud
0115bf254e Force default profile on movies without profile. fix #1523 2013-03-19 22:55:10 +01:00
Ruud
607b5ea766 Run exe after install 2013-03-19 21:22:07 +01:00
Ruud
88579cd71a One up 2013-03-19 20:52:07 +01:00
Ruud
6c57316ce6 Use https for changelog 2013-03-19 20:46:00 +01:00
Ruud
6702683da3 Merge branch 'refs/heads/develop' into desktop 2013-03-19 20:34:38 +01:00
Ruud
b9c2b42725 Merge branch 'refs/heads/develop' 2013-03-19 20:28:46 +01:00
Ruud
e54928720a Don't download same quality twice. fix #1519 2013-03-19 20:24:48 +01:00
Ruud
f8f22cdef7 Description typo 2013-03-19 00:25:03 +01:00
Ruud
1ed58586a1 Force install install in AppData
Add images to installer
2013-03-18 23:56:54 +01:00
Ruud
e694276a8d Save view to different cookie so people don't have to reset. 2013-03-18 22:02:47 +01:00
Ruud
a8369b4e93 Merge branch 'refs/heads/develop'
Conflicts:
	version.py
2013-03-18 21:57:58 +01:00
Ruud
73b7bcc6ce Force dashboard view 2013-03-18 21:56:50 +01:00
Ruud
f08ccd4fd8 One up installer 2013-03-17 22:34:04 +01:00
Ruud
312562a9f5 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2013-03-17 16:42:53 +01:00
Ruud
fab8e66fe1 One up
Conflicts:
	version.py
2013-03-17 16:40:22 +01:00
Ruud
4db1b57c70 Merge branch 'refs/heads/develop' 2013-03-17 16:31:31 +01:00
Ruud
b06dbd3069 Merge branch 'refs/heads/develop' 2013-03-12 21:12:18 +01:00
Ruud
f84aa8c638 Merge branch 'refs/heads/develop' 2013-03-09 18:15:26 +01:00
Ruud
8e07dfc730 Merge branch 'refs/heads/develop' 2013-03-08 14:46:01 +01:00
Ruud
a49a00a25f Host to 0.0.0.0 2013-02-14 23:02:44 +01:00
Ruud
673843fb66 Merge branch 'refs/heads/develop' 2013-02-12 23:25:11 +01:00
Ruud
811f35b028 Merge branch 'refs/heads/develop' 2013-02-04 23:11:39 +01:00
Ruud
ec6e2c240f Merge branch 'refs/heads/develop' 2013-01-28 23:21:52 +01:00
Ruud
9e260a89af One up 2013-01-26 14:51:39 +01:00
Ruud
d233e4d22e Merge branch 'refs/heads/develop' into desktop 2013-01-26 13:54:56 +01:00
Ruud
23893dbcb9 Merge branch 'refs/heads/develop' into desktop 2013-01-25 20:13:58 +01:00
Ruud
3187a0f820 Merge branch 'refs/heads/develop' 2013-01-25 15:52:54 +01:00
Ruud
f86b9299c4 Merge branch 'refs/heads/develop' 2013-01-25 14:21:11 +01:00
Ruud
d27d0abeb0 Merge branch 'refs/heads/develop'
Conflicts:
	version.py
2013-01-24 23:35:37 +01:00
Ruud
506871b506 One up 2013-01-23 23:10:55 +01:00
Ruud
6115917660 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2013-01-23 22:57:07 +01:00
Ruud
21df8819d3 Merge branch 'refs/heads/develop' into desktop 2013-01-23 22:55:09 +01:00
Ruud
7c59348138 Merge branch 'refs/heads/develop' 2013-01-23 22:54:29 +01:00
Ruud
ab53f44157 Remove non-int backup folders. closes #1298 2013-01-23 22:23:52 +01:00
Ruud
b35f325d94 Merge branch 'refs/heads/develop' 2013-01-23 22:16:26 +01:00
Ruud
fb3f3e11f6 Merge branch 'refs/heads/develop' into desktop 2013-01-22 21:40:40 +01:00
Ruud
178c8942c3 Merge branch 'refs/heads/develop' into desktop 2013-01-14 19:54:22 +01:00
Ruud
393c14de54 Urlencode spotweb id. fix #1213 2013-01-07 23:12:08 +01:00
Ruud
51e747049d One up 2013-01-07 23:10:42 +01:00
Ruud
0582f7d694 Urlencode spotweb id. fix #1213 2013-01-07 23:10:06 +01:00
Ruud
fa7cac7538 Merge branch 'refs/heads/develop' into desktop 2013-01-07 22:41:55 +01:00
Ruud
bff17c0b95 Merge branch 'refs/heads/develop' 2013-01-07 22:40:37 +01:00
Ruud
d172828ac5 Merge branch 'refs/heads/develop' 2013-01-02 14:12:07 +01:00
Ruud
9500ac73fc Link to downloaders 2013-01-02 13:52:44 +01:00
Ruud
e2cf7e4421 Merge branch 'refs/heads/develop' 2013-01-02 13:44:34 +01:00
Ruud
9a314cfbc4 One up 2012-12-29 00:03:45 +01:00
Ruud
5941d0bf77 Add version to update url 2012-12-29 00:03:36 +01:00
Ruud
d326c1c25c Merge branch 'refs/heads/master' into desktop
Conflicts:
	version.py
2012-12-28 23:31:08 +01:00
Ruud
7e6234298d Merge branch 'refs/heads/develop' 2012-12-28 23:25:40 +01:00
Ruud
d4da206f93 Merge branch 'refs/heads/develop' 2012-12-22 16:33:47 +01:00
Ruud
985a168724 Merge branch 'refs/heads/develop' 2012-12-21 23:18:00 +01:00
Ruud
173c6194ed Merge branch 'refs/heads/develop' 2012-12-19 11:12:26 +01:00
Ruud
bcd23ad10c Merge branch 'refs/heads/develop' 2012-12-17 15:13:00 +01:00
Ruud
898e6f487d Merge branch 'refs/heads/develop' 2012-12-16 23:52:06 +01:00
Ruud
96472a9a8f One up 2012-12-16 23:51:58 +01:00
Ruud
27252561e2 Merge branch 'refs/heads/develop' into desktop 2012-12-16 23:51:24 +01:00
Ruud
6618c3927c Merge branch 'refs/heads/develop' 2012-12-11 23:15:06 +01:00
Ruud
c9e732651f One up 2012-12-01 12:16:58 +01:00
Ruud
7849e7170d Uninstall only create files, no wildcard *.* 2012-12-01 12:16:51 +01:00
Ruud
087894eb4e Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2012-12-01 11:50:08 +01:00
Ruud
4b58b40226 Merge branch 'refs/heads/develop' 2012-12-01 11:48:54 +01:00
Ruud
3ecc826629 Merge branch 'refs/heads/develop'
Conflicts:
	version.py
2012-11-11 22:06:48 +01:00
Ruud
25f1b8c7a7 Fedora init fix #1009 2012-11-02 18:32:15 +01:00
Ruud
e71da1f14d Use proper description for binary build. fix #1005 2012-11-02 18:24:13 +01:00
Ruud
938b14ba18 One up installer 2012-10-29 20:45:17 +01:00
Ruud
d6522d8f38 One up installer 2012-10-27 18:49:44 +02:00
Ruud
78eab890e7 Merge branch 'refs/heads/develop' into desktop 2012-10-27 18:25:36 +02:00
Ruud
1a56191f83 Don't unzip 2012-10-27 18:22:50 +02:00
Ruud
41c0f34d95 Properly restart 2012-10-27 18:22:40 +02:00
Ruud
37bf205d7a Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2012-10-27 11:56:57 +02:00
Ruud
32fe3796e4 Merge branch 'refs/heads/develop' 2012-10-26 22:22:47 +02:00
Ruud
359d1aaafa Merge branch 'refs/heads/develop' 2012-10-26 14:54:12 +02:00
Ruud
fb5d336351 Merge branch 'refs/heads/develop' 2012-10-26 14:36:04 +02:00
Ruud
eb30dff986 Merge branch 'refs/heads/develop' 2012-10-13 00:00:44 +02:00
Ruud
9312336962 Merge branch 'refs/heads/develop' 2012-09-24 09:36:59 +02:00
Ruud
aa1fa3eb9a Add description 2012-09-19 15:42:33 +02:00
Ruud
0e2f8a612c Extract zip after build, for testing 2012-09-19 15:29:07 +02:00
Frank Fenton
c087a6b49b Add Trakt notification 2012-09-18 02:44:38 +10:00
Ruud
ade4338ea6 Merge branch 'refs/heads/develop' 2012-09-16 21:32:16 +02:00
Ruud
55b20324c0 Merge branch 'refs/heads/develop' 2012-09-16 12:36:48 +02:00
Ruud
465e7b2abc Merge branch 'refs/heads/develop' into desktop 2012-09-16 12:36:17 +02:00
Ruud
578fb45785 Installer 1 up 2012-09-16 11:35:56 +02:00
Ruud
c0fb28301d Merge branch 'refs/heads/develop'
Conflicts:
	version.py
2012-09-16 10:46:39 +02:00
Ruud
96995bbbe5 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2012-09-16 10:45:19 +02:00
Ruud
4cfdafebbc Merge branch 'refs/heads/develop' into desktop 2012-09-14 13:15:47 +02:00
Ruud
f9c2503f81 Merge branch 'refs/heads/develop' 2012-09-14 13:15:35 +02:00
Ruud
b97acb8ef5 Merge branch 'refs/heads/develop' into desktop 2012-09-14 13:08:19 +02:00
Ruud
5b4cdf05b1 Merge branch 'refs/heads/develop' 2012-09-14 13:06:56 +02:00
Ruud
d68d2dfdb6 Updated installer 2012-09-09 21:48:38 +02:00
Ruud
39b269a454 Merge branch 'refs/heads/develop' into desktop 2012-09-09 17:32:47 +02:00
Ruud
ac081d3e10 Getting ready for build 2012-09-09 17:28:23 +02:00
Ruud
5d4efb60cf Merge branch 'refs/heads/develop' into desktop 2012-09-08 16:01:49 +02:00
Ruud
6f25a6bdfd Merge branch 'refs/heads/develop' 2012-09-03 10:32:09 +02:00
Ruud
23427e95f7 Merge branch 'refs/heads/develop' 2012-08-26 23:09:51 +02:00
Ruud
cc408b980c Merge branch 'refs/heads/develop' into desktop
Conflicts:
	couchpotato/core/_base/updater/main.py
2012-08-05 16:18:35 +02:00
Ruud
90a09e573b Merge branch 'refs/heads/develop'
Conflicts:
	couchpotato/core/_base/updater/main.py
2012-08-05 16:15:53 +02:00
Ruud
e1d7440b9d Wrong branch in master 2012-07-15 00:23:44 +02:00
Ruud
59590b3ac9 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	couchpotato/core/_base/updater/main.py
2012-07-14 00:35:00 +02:00
Ruud
ff759dacf3 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	couchpotato/core/_base/updater/main.py
2012-07-11 22:43:45 +02:00
Ruud
a328e44130 Merge branch 'desktop' of github.com:RuudBurger/CouchPotatoServer into desktop 2012-05-15 23:23:56 +02:00
Ruud
7924cac5f9 Update installer version 2012-05-15 23:21:24 +02:00
Ruud
1cef3b0c93 remove --nogit tag 2012-05-15 23:21:24 +02:00
Ruud
3cd59edc8b Import errors
File icon
2012-05-15 23:21:24 +02:00
Ruud
0d624af01d Working PNG 2012-05-15 23:21:24 +02:00
Ruud
a09132570c Change branch to desktop 2012-05-15 23:21:14 +02:00
Ruud
ee3fc38432 Better setup 2012-05-15 23:21:14 +02:00
Ruud
dbf0192c8e Inno setup, start 2012-05-15 23:21:14 +02:00
Ruud
6962cfc3f5 new Desktop runner 2012-05-15 23:21:14 +02:00
Ruud
e096ec3b5b Desktop files 2012-05-15 23:20:05 +02:00
Ruud
b30a74ae0c Merge branch 'refs/heads/develop' into desktop 2012-05-15 23:15:17 +02:00
Ruud
978eeb16c9 Update installer version 2012-05-15 23:14:20 +02:00
Ruud
e5c9d91657 Merge branch 'refs/heads/develop' into desktop 2012-05-15 22:27:22 +02:00
Ruud
fa81c3a07a Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2012-05-14 22:00:02 +02:00
Ruud
9cdd520d41 Merge branch 'refs/heads/develop' into desktop 2012-05-14 20:22:55 +02:00
Ruud
55d7898771 Merge branch 'refs/heads/develop' into desktop 2012-05-13 12:56:45 +02:00
Ruud
b8256bef97 Merge branch 'refs/heads/develop' into desktop 2012-05-12 00:35:52 +02:00
Ruud
5be9dc0b4a Merge branch 'refs/heads/develop' into desktop 2012-05-09 22:20:53 +02:00
Ruud
7d0be0cefb remove --nogit tag 2012-05-07 22:55:54 +02:00
Ruud
f7ce1edb13 Merge branch 'refs/heads/develop' into desktop 2012-05-07 22:44:01 +02:00
Ruud
5ad9280b60 Merge branch 'refs/heads/develop' into desktop 2012-05-07 22:27:55 +02:00
Ruud
2b353f1b20 Merge branch 'refs/heads/develop' into desktop 2012-05-04 17:29:15 +02:00
Ruud
75ab90b87b Merge branch 'refs/heads/develop' into desktop 2012-05-02 21:40:19 +02:00
Ruud
0219296120 Import errors
File icon
2012-05-02 21:34:45 +02:00
Ruud
20032b3a31 Working PNG 2012-05-01 07:35:44 +02:00
Ruud
ea9e9a8c90 Updater base 2012-05-01 07:35:27 +02:00
Ruud
f7b0ee145b Change branch to desktop 2012-04-30 21:37:04 +02:00
Ruud
cc866738ee Merge branch 'refs/heads/develop' into desktop 2012-04-30 21:32:56 +02:00
Ruud
eadccf6e33 Merge branch 'refs/heads/develop' into desktop 2012-04-29 00:00:25 +02:00
Ruud
b70b66e567 Merge branch 'refs/heads/develop' into desktop 2012-04-28 23:14:59 +02:00
Ruud
5b6792dc20 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	CouchPotato.py
	couchpotato/core/plugins/renamer/main.py
	couchpotato/core/plugins/trailer/__init__.py
2012-04-07 21:35:36 +02:00
Ruud
f498e7343a Better setup 2012-02-25 01:48:58 +01:00
Ruud
6962f441e6 Inno setup, start 2012-02-21 18:50:34 +01:00
Ruud
1def62b1b1 new Desktop runner 2012-02-19 17:13:37 +01:00
Ruud
a4a4a6a185 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	CouchPotato.py
2012-02-19 13:14:56 +01:00
Ruud
d4c9469c1a Remove nfo when not renaming as .orig.nfo 2012-02-19 12:53:55 +01:00
Ruud
3e2d4c5d7b Initial trailer support 2012-02-19 12:48:54 +01:00
Ruud
d03f711d69 kwargs in file.download for urlopen 2012-02-19 12:45:22 +01:00
Ruud
44dd8d9b96 Merge lists, not overwrite 2012-02-19 12:37:25 +01:00
Ruud
549a3be0d8 Merge branch 'refs/heads/develop' into desktop 2012-02-12 00:10:56 +01:00
Ruud
1bb2edf8ec Merge branch 'refs/heads/develop' into desktop 2012-02-11 23:33:14 +01:00
Ruud
84c6f36315 Desktop files 2012-02-11 23:06:14 +01:00
231 changed files with 23926 additions and 3081 deletions

231
Desktop.py Normal file
View File

@@ -0,0 +1,231 @@
from esky.util import appdir_from_executable #@UnresolvedImport
from threading import Thread
from version import VERSION
from wx.lib.softwareupdate import SoftwareUpdate
import os
import sys
import time
import webbrowser
import wx
# Include proper dirs
if hasattr(sys, 'frozen'):
import libs
base_path = os.path.dirname(os.path.dirname(os.path.abspath(libs.__file__)))
else:
base_path = os.path.dirname(os.path.abspath(__file__))
lib_dir = os.path.join(base_path, 'libs')
sys.path.insert(0, base_path)
sys.path.insert(0, lib_dir)
from couchpotato.environment import Env
class TaskBarIcon(wx.TaskBarIcon):
TBMENU_OPEN = wx.NewId()
TBMENU_SETTINGS = wx.NewId()
TBMENU_EXIT = wx.ID_EXIT
closed = False
menu = False
enabled = False
def __init__(self, frame):
wx.TaskBarIcon.__init__(self)
self.frame = frame
icon = wx.Icon('icon.png', wx.BITMAP_TYPE_PNG)
self.SetIcon(icon)
self.Bind(wx.EVT_TASKBAR_LEFT_UP, self.OnTaskBarClick)
self.Bind(wx.EVT_TASKBAR_RIGHT_UP, self.OnTaskBarClick)
self.Bind(wx.EVT_MENU, self.onOpen, id = self.TBMENU_OPEN)
self.Bind(wx.EVT_MENU, self.onSettings, id = self.TBMENU_SETTINGS)
self.Bind(wx.EVT_MENU, self.onTaskBarClose, id = self.TBMENU_EXIT)
def OnTaskBarClick(self, evt):
menu = self.CreatePopupMenu()
self.PopupMenu(menu)
menu.Destroy()
def enable(self):
self.enabled = True
if self.menu:
self.open_menu.Enable(True)
self.setting_menu.Enable(True)
self.open_menu.SetText('Open')
def CreatePopupMenu(self):
if not self.menu:
self.menu = wx.Menu()
self.open_menu = self.menu.Append(self.TBMENU_OPEN, 'Open')
self.setting_menu = self.menu.Append(self.TBMENU_SETTINGS, 'About')
self.exit_menu = self.menu.Append(self.TBMENU_EXIT, 'Quit')
if not self.enabled:
self.open_menu.Enable(False)
self.setting_menu.Enable(False)
self.open_menu.SetText('Loading...')
return self.menu
def onOpen(self, event):
url = self.frame.parent.getSetting('base_url')
webbrowser.open(url)
def onSettings(self, event):
url = self.frame.parent.getSetting('base_url') + '/settings/'
webbrowser.open(url)
def onTaskBarClose(self, evt):
if self.closed:
return
self.closed = True
self.RemoveIcon()
wx.CallAfter(self.frame.Close)
def makeIcon(self, img):
if "wxMSW" in wx.PlatformInfo:
img = img.Scale(16, 16)
elif "wxGTK" in wx.PlatformInfo:
img = img.Scale(22, 22)
icon = wx.IconFromBitmap(img.CopyFromBitmap())
return icon
class MainFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, None, style = wx.FRAME_NO_TASKBAR)
self.parent = parent
self.tbicon = TaskBarIcon(self)
class WorkerThread(Thread):
def __init__(self, desktop):
Thread.__init__(self)
self.daemon = True
self._desktop = desktop
self.start()
def run(self):
# Get options via arg
from couchpotato.runner import getOptions
args = ['--quiet']
self.options = getOptions(base_path, args)
# Load settings
settings = Env.get('settings')
settings.setFile(self.options.config_file)
# Create data dir if needed
self.data_dir = os.path.expanduser(Env.setting('data_dir'))
if self.data_dir == '':
from couchpotato.core.helpers.variable import getDataDir
self.data_dir = getDataDir()
if not os.path.isdir(self.data_dir):
os.makedirs(self.data_dir)
# Create logging dir
self.log_dir = os.path.join(self.data_dir, 'logs');
if not os.path.isdir(self.log_dir):
os.mkdir(self.log_dir)
try:
from couchpotato.runner import runCouchPotato
runCouchPotato(self.options, base_path, args, data_dir = self.data_dir, log_dir = self.log_dir, Env = Env, desktop = self._desktop)
except:
pass
self._desktop.frame.Close()
class CouchPotatoApp(wx.App, SoftwareUpdate):
settings = {}
events = {}
restart = False
closing = False
def OnInit(self):
# Updater
base_url = 'https://couchpota.to/updates/%s'
self.InitUpdates(base_url % VERSION + '/', base_url % 'changelog.html',
icon = wx.Icon('icon.png'))
self.frame = MainFrame(self)
self.frame.Bind(wx.EVT_CLOSE, self.onClose)
# CouchPotato thread
self.worker = WorkerThread(self)
return True
def onAppLoad(self):
self.frame.tbicon.enable()
def setSettings(self, settings = {}):
self.settings = settings
def getSetting(self, name):
return self.settings.get(name)
def addEvents(self, events = {}):
for name in events.iterkeys():
self.events[name] = events[name]
def onClose(self, event):
if not self.closing:
self.closing = True
self.frame.tbicon.onTaskBarClose(event)
onClose = self.events.get('onClose')
onClose(event)
def afterShutdown(self, restart = False):
self.frame.Destroy()
self.restart = restart
self.ExitMainLoop()
if __name__ == '__main__':
app = CouchPotatoApp(redirect = False)
app.MainLoop()
time.sleep(1)
if app.restart:
def appexe_from_executable(exepath):
appdir = appdir_from_executable(exepath)
exename = os.path.basename(exepath)
if sys.platform == "darwin":
if os.path.isdir(os.path.join(appdir, "Contents", "MacOS")):
return os.path.join(appdir, "Contents", "MacOS", exename)
return os.path.join(appdir, exename)
exe = appexe_from_executable(sys.executable)
os.chdir(os.path.dirname(exe))
os.execv(exe, [exe] + sys.argv[1:])

View File

@@ -11,16 +11,12 @@ api_nonblock = {}
class NonBlockHandler(RequestHandler):
def __init__(self, application, request, **kwargs):
cls = NonBlockHandler
cls.stoppers = []
super(NonBlockHandler, self).__init__(application, request, **kwargs)
stoppers = []
@asynchronous
def get(self, route):
cls = NonBlockHandler
start, stop = api_nonblock[route]
cls.stoppers.append(stop)
self.stoppers.append(stop)
start(self.onNewMessage, last_id = self.get_argument("last_id", None))
@@ -30,12 +26,11 @@ class NonBlockHandler(RequestHandler):
self.finish(response)
def on_connection_close(self):
cls = NonBlockHandler
for stop in cls.stoppers:
for stop in self.stoppers:
stop(self.onNewMessage)
cls.stoppers = []
self.stoppers = []
def addApiView(route, func, static = False, docs = None, **kwargs):

View File

@@ -70,7 +70,7 @@ config = [{
'name': 'development',
'default': 0,
'type': 'bool',
'description': 'Disables some checks/downloads for faster reloading.',
'description': 'Enable this if you\'re developing, and NOT in any other case, thanks.',
},
{
'name': 'data_dir',

View File

@@ -79,7 +79,7 @@ class Core(Plugin):
def shutdown():
self.initShutdown()
IOLoop.instance().add_callback(shutdown)
IOLoop.current().add_callback(shutdown)
return 'shutdown'
@@ -89,7 +89,7 @@ class Core(Plugin):
def restart():
self.initShutdown(restart = True)
IOLoop.instance().add_callback(restart)
IOLoop.current().add_callback(restart)
return 'restarting'
@@ -128,7 +128,7 @@ class Core(Plugin):
log.debug('Save to shutdown/restart')
try:
IOLoop.instance().stop()
IOLoop.current().stop()
except RuntimeError:
pass
except:

View File

@@ -1,10 +1,11 @@
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
from minify.cssmin import cssmin
from minify.jsmin import jsmin
import cssprefixer
import os
import traceback
@@ -23,7 +24,6 @@ class ClientScript(Plugin):
'script': [
'scripts/library/mootools.js',
'scripts/library/mootools_more.js',
'scripts/library/prefix_free.js',
'scripts/library/uniform.js',
'scripts/library/form_replacement/form_check.js',
'scripts/library/form_replacement/form_radio.js',
@@ -69,7 +69,8 @@ class ClientScript(Plugin):
addEvent('clientscript.get_styles', self.getStyles)
addEvent('clientscript.get_scripts', self.getScripts)
addEvent('app.load', self.minify)
if not Env.get('dev'):
addEvent('app.load', self.minify)
self.addCore()
@@ -108,8 +109,10 @@ class ClientScript(Plugin):
if file_type == 'script':
data = jsmin(f)
else:
data = cssmin(f)
data = cssprefixer.process(f, debug = False, minify = True)
data = data.replace('../images/', '../static/images/')
data = data.replace('../fonts/', '../static/fonts/')
data = data.replace('../../static/', '../static/') # Replace inside plugins
raw.append({'file': file_path, 'date': int(os.path.getmtime(file_path)), 'data': data})
@@ -119,7 +122,7 @@ class ClientScript(Plugin):
data += self.comment.get(file_type) % (r.get('file'), r.get('date'))
data += r.get('data') + '\n\n'
self.createFile(out, data.strip())
self.createFile(out, ss(data.strip()))
if not self.minified.get(file_type):
self.minified[file_type] = {}

View File

@@ -16,51 +16,19 @@ class Scheduler(Plugin):
addEvent('schedule.cron', self.cron)
addEvent('schedule.interval', self.interval)
addEvent('schedule.start', self.start)
addEvent('schedule.restart', self.start)
addEvent('app.load', self.start)
addEvent('schedule.remove', self.remove)
self.sched = Sched(misfire_grace_time = 60)
def remove(self, identifier):
for type in ['interval', 'cron']:
try:
self.sched.unschedule_job(getattr(self, type)[identifier]['job'])
log.debug('%s unscheduled %s', (type.capitalize(), identifier))
except:
pass
def start(self):
# Stop all running
self.stop()
# Crons
for identifier in self.crons:
try:
self.remove(identifier)
cron = self.crons[identifier]
job = self.sched.add_cron_job(cron['handle'], day = cron['day'], hour = cron['hour'], minute = cron['minute'])
cron['job'] = job
except ValueError, e:
log.error('Failed adding cronjob: %s', e)
# Intervals
for identifier in self.intervals:
try:
self.remove(identifier)
interval = self.intervals[identifier]
job = self.sched.add_interval_job(interval['handle'], hours = interval['hours'], minutes = interval['minutes'], seconds = interval['seconds'])
interval['job'] = job
except ValueError, e:
log.error('Failed adding interval cronjob: %s', e)
# Start it
log.debug('Starting scheduler')
self.sched.start()
self.started = True
log.debug('Scheduler started')
def remove(self, identifier):
for cron_type in ['intervals', 'crons']:
try:
self.sched.unschedule_job(getattr(self, cron_type)[identifier]['job'])
log.debug('%s unscheduled %s', (cron_type.capitalize(), identifier))
except:
pass
def doShutdown(self):
super(Scheduler, self).doShutdown()
@@ -82,6 +50,7 @@ class Scheduler(Plugin):
'day': day,
'hour': hour,
'minute': minute,
'job': self.sched.add_cron_job(handle, day = day, hour = hour, minute = minute)
}
def interval(self, identifier = '', handle = None, hours = 0, minutes = 0, seconds = 0):
@@ -93,4 +62,5 @@ class Scheduler(Plugin):
'hours': hours,
'minutes': minutes,
'seconds': seconds,
'job': self.sched.add_interval_job(handle, hours = hours, minutes = minutes, seconds = seconds)
}

View File

@@ -15,6 +15,7 @@ import tarfile
import time
import traceback
import version
import zipfile
log = CPLog(__name__)
@@ -32,7 +33,6 @@ class Updater(Plugin):
else:
self.updater = SourceUpdater()
fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6)
addEvent('app.load', self.autoUpdate)
addEvent('updater.info', self.info)
@@ -52,6 +52,15 @@ class Updater(Plugin):
'return': {'type': 'see updater.info'}
})
addEvent('setting.save.updater.enabled.after', self.setCrons)
def setCrons(self):
fireEvent('schedule.remove', 'updater.check', single = True)
if self.isEnabled():
fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6)
self.autoUpdate() # Check after enabling
def autoUpdate(self):
if self.check() and self.conf('automatic') and not self.updater.update_failed:
if self.updater.doUpdate():
@@ -255,11 +264,11 @@ class SourceUpdater(BaseUpdater):
def doUpdate(self):
try:
url = 'https://github.com/%s/%s/tarball/%s' % (self.repo_user, self.repo_name, self.branch)
destination = os.path.join(Env.get('cache_dir'), self.update_version.get('hash') + '.tar.gz')
extracted_path = os.path.join(Env.get('cache_dir'), 'temp_updater')
download_data = fireEvent('cp.source_url', repo = self.repo_user, repo_name = self.repo_name, branch = self.branch, single = True)
destination = os.path.join(Env.get('cache_dir'), self.update_version.get('hash')) + '.' + download_data.get('type')
destination = fireEvent('file.download', url = url, dest = destination, single = True)
extracted_path = os.path.join(Env.get('cache_dir'), 'temp_updater')
destination = fireEvent('file.download', url = download_data.get('url'), dest = destination, single = True)
# Cleanup leftover from last time
if os.path.isdir(extracted_path):
@@ -267,9 +276,14 @@ class SourceUpdater(BaseUpdater):
self.makeDir(extracted_path)
# Extract
tar = tarfile.open(destination)
tar.extractall(path = extracted_path)
tar.close()
if download_data.get('type') == 'zip':
zip = zipfile.ZipFile(destination)
zip.extractall(extracted_path)
else:
tar = tarfile.open(destination)
tar.extractall(path = extracted_path)
tar.close()
os.remove(destination)
if self.replaceWith(os.path.join(extracted_path, os.listdir(extracted_path)[0])):

View File

@@ -5,7 +5,7 @@ var UpdaterBase = new Class({
initialize: function(){
var self = this;
App.addEvent('load', self.info.bind(self, 1000))
App.addEvent('load', self.info.bind(self, 2000))
App.addEvent('unload', function(){
if(self.timer)
clearTimeout(self.timer);
@@ -84,7 +84,7 @@ var UpdaterBase = new Class({
'click': self.doUpdate.bind(self)
}
})
).inject($(document.body).getElement('.header'))
).inject(document.body)
},
doUpdate: function(){

View File

@@ -1,5 +1,6 @@
from base64 import b32decode, b16encode
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import Provider
import random
@@ -103,6 +104,12 @@ class Downloader(Provider):
log.error('Failed converting magnet url to torrent: %s', (torrent_hash))
return False
def downloadReturnId(self, download_id):
return {
'downloader': self.getName(),
'id': download_id
}
def isDisabled(self, manual, data):
return not self.isEnabled(manual, data)
@@ -116,3 +123,35 @@ class Downloader(Provider):
return super(Downloader, self).isEnabled() and \
((d_manual and manual) or (d_manual is False)) and \
(not data or self.isCorrectType(data.get('type')))
class StatusList(list):
provider = None
def __init__(self, provider, **kwargs):
self.provider = provider
self.kwargs = kwargs
super(StatusList, self).__init__()
def extend(self, results):
for r in results:
self.append(r)
def append(self, result):
new_result = self.fillResult(result)
super(StatusList, self).append(new_result)
def fillResult(self, result):
defaults = {
'id': 0,
'status': 'busy',
'downloader': self.provider.getName(),
'folder': '',
}
return mergeDicts(defaults, result)

View File

@@ -24,6 +24,12 @@ config = [{
'default': 'localhost:6789',
'description': 'Hostname with port. Usually <strong>localhost:6789</strong>',
},
{
'name': 'username',
'default': 'nzbget',
'advanced': True,
'description': 'Set a different username to connect. Default: nzbget',
},
{
'name': 'password',
'type': 'password',
@@ -48,6 +54,12 @@ config = [{
'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
},
{
'name': 'delete_failed',
'default': True,
'type': 'bool',
'description': 'Delete a release after the download has failed.',
},
],
}
],

View File

@@ -1,9 +1,11 @@
from base64 import standard_b64encode
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.helpers.variable import tryInt, md5
from couchpotato.core.logger import CPLog
from datetime import timedelta
import re
import shutil
import socket
import traceback
import xmlrpclib
@@ -14,7 +16,7 @@ class NZBGet(Downloader):
type = ['nzb']
url = 'http://nzbget:%(password)s@%(host)s/xmlrpc'
url = 'http://%(username)s:%(password)s@%(host)s/xmlrpc'
def download(self, data = {}, movie = {}, filedata = None):
@@ -24,7 +26,7 @@ class NZBGet(Downloader):
log.info('Sending "%s" to NZBGet.', data.get('name'))
url = self.url % {'host': self.conf('host'), 'password': self.conf('password')}
url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
nzb_name = ss('%s.nzb' % self.createNzbName(data, movie))
rpc = xmlrpclib.ServerProxy(url)
@@ -50,7 +52,124 @@ class NZBGet(Downloader):
if xml_response:
log.info('NZB sent successfully to NZBGet')
return True
nzb_id = md5(data['url']) # about as unique as they come ;)
couchpotato_id = "couchpotato=" + nzb_id
groups = rpc.listgroups()
file_id = [item['LastID'] for item in groups if item['NZBFilename'] == nzb_name]
confirmed = rpc.editqueue("GroupSetParameter", 0, couchpotato_id, file_id)
if confirmed:
log.debug('couchpotato parameter set in nzbget download')
return self.downloadReturnId(nzb_id)
else:
log.error('NZBGet could not add %s to the queue.', nzb_name)
return False
def getAllDownloadStatus(self):
log.debug('Checking NZBGet download status.')
url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
rpc = xmlrpclib.ServerProxy(url)
try:
if rpc.writelog('INFO', 'CouchPotato connected to check status'):
log.info('Successfully connected to NZBGet')
else:
log.info('Successfully connected to NZBGet, but unable to send a message')
except socket.error:
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
return False
except xmlrpclib.ProtocolError, e:
if e.errcode == 401:
log.error('Password is incorrect.')
else:
log.error('Protocol Error: %s', e)
return False
# Get NZBGet data
try:
status = rpc.status()
groups = rpc.listgroups()
queue = rpc.postqueue(0)
history = rpc.history()
except:
log.error('Failed getting data: %s', traceback.format_exc(1))
return False
statuses = StatusList(self)
for item in groups:
log.debug('Found %s in NZBGet download queue', item['NZBFilename'])
try:
nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0]
except:
nzb_id = item['NZBID']
statuses.append({
'id': nzb_id,
'name': item['NZBFilename'],
'original_status': 'DOWNLOADING' if item['ActiveDownloads'] > 0 else 'QUEUED',
# Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item
'timeleft': str(timedelta(seconds = item['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20)) if item['ActiveDownloads'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']) else -1,
})
for item in queue: # 'Parameters' is not passed in rpc.postqueue
log.debug('Found %s in NZBGet postprocessing queue', item['NZBFilename'])
statuses.append({
'id': item['NZBID'],
'name': item['NZBFilename'],
'original_status': item['Stage'],
'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1,
})
for item in history:
log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (item['NZBFilename'] , item['ParStatus'], item['ScriptStatus'] , item['Log']))
try:
nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0]
except:
nzb_id = item['NZBID']
statuses.append({
'id': nzb_id,
'name': item['NZBFilename'],
'status': 'completed' if item['ParStatus'] == 'SUCCESS' and item['ScriptStatus'] == 'SUCCESS' else 'failed',
'original_status': item['ParStatus'] + ', ' + item['ScriptStatus'],
'timeleft': str(timedelta(seconds = 0)),
'folder': item['DestDir']
})
return statuses
def removeFailed(self, item):
log.info('%s failed downloading, deleting...', item['name'])
url = self.url % {'host': self.conf('host'), 'password': self.conf('password')}
rpc = xmlrpclib.ServerProxy(url)
try:
if rpc.writelog('INFO', 'CouchPotato connected to delete some history'):
log.info('Successfully connected to NZBGet')
else:
log.info('Successfully connected to NZBGet, but unable to send a message')
except socket.error:
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
return False
except xmlrpclib.ProtocolError, e:
if e.errcode == 401:
log.error('Password is incorrect.')
else:
log.error('Protocol Error: %s', e)
return False
try:
history = rpc.history()
for hist in history:
if hist['Parameters'] and hist['Parameters']['couchpotato'] and hist['Parameters']['couchpotato'] == item['id']:
nzb_id = hist['ID']
path = hist['DestDir']
if rpc.editqueue('HistoryDelete', 0, "", [tryInt(nzb_id)]):
shutil.rmtree(path, True)
except:
log.error('Failed deleting: %s', traceback.format_exc(0))
return False
return True

View File

@@ -1,5 +1,5 @@
from base64 import b64encode
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import tryUrlencode, ss
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog
@@ -29,7 +29,9 @@ class NZBVortex(Downloader):
nzb_filename = self.createFileName(data, filedata, movie)
self.call('nzb/add', params = {'file': (ss(nzb_filename), filedata)}, multipart = True)
return True
raw_statuses = self.call('nzb')
nzb_id = [item['id'] for item in raw_statuses.get('nzbs', []) if item['name'] == nzb_filename][0]
return self.downloadReturnId(nzb_id)
except:
log.error('Something went wrong sending the NZB file: %s', traceback.format_exc())
return False
@@ -38,7 +40,7 @@ class NZBVortex(Downloader):
raw_statuses = self.call('nzb')
statuses = []
statuses = StatusList(self)
for item in raw_statuses.get('nzbs', []):
# Check status
@@ -53,7 +55,8 @@ class NZBVortex(Downloader):
'name': item['uiTitle'],
'status': status,
'original_status': item['state'],
'timeleft':-1,
'timeleft': -1,
'folder': item['destinationPath'],
})
return statuses

View File

@@ -1,8 +1,9 @@
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import tryUrlencode, ss
from couchpotato.core.helpers.variable import cleanHost, mergeDicts
from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
from datetime import timedelta
from urllib2 import URLError
import json
import traceback
@@ -17,8 +18,7 @@ class Sabnzbd(Downloader):
log.info('Sending "%s" to SABnzbd.', data.get('name'))
params = {
'apikey': self.conf('api_key'),
req_params = {
'cat': self.conf('category'),
'mode': 'addurl',
'nzbname': self.createNzbName(data, movie),
@@ -31,17 +31,15 @@ class Sabnzbd(Downloader):
# If it's a .rar, it adds the .rar extension, otherwise it stays .nzb
nzb_filename = self.createFileName(data, filedata, movie)
params['mode'] = 'addfile'
req_params['mode'] = 'addfile'
else:
params['name'] = data.get('url')
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(params)
req_params['name'] = data.get('url')
try:
if params.get('mode') is 'addfile':
sab = self.urlopen(url, timeout = 60, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True, show_error = False, headers = {'User-Agent': Env.getIdentifier()})
if req_params.get('mode') is 'addfile':
sab_data = self.call(req_params, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True)
else:
sab = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()})
sab_data = self.call(req_params)
except URLError:
log.error('Failed sending release, probably wrong HOST: %s', traceback.format_exc(0))
return False
@@ -49,17 +47,15 @@ class Sabnzbd(Downloader):
log.error('Failed sending release, use API key, NOT the NZB key: %s', traceback.format_exc(0))
return False
result = sab.strip()
if not result:
log.error('SABnzbd didn\'t return anything.')
return False
log.debug('Result text from SAB: %s', result[:40])
if result[:2] == 'ok':
log.debug('Result from SAB: %s', sab_data)
if sab_data.get('status') and not sab_data.get('error'):
log.info('NZB sent to SAB successfully.')
return True
if filedata:
return self.downloadReturnId(sab_data.get('nzo_ids')[0])
else:
return True
else:
log.error(result[:40])
log.error('Error getting data from SABNZBd: %s', sab_data)
return False
def getAllDownloadStatus(self):
@@ -85,14 +81,13 @@ class Sabnzbd(Downloader):
log.error('Failed getting history json: %s', traceback.format_exc(1))
return False
statuses = []
statuses = StatusList(self)
# Get busy releases
for item in queue.get('slots', []):
statuses.append({
'id': item['nzo_id'],
'name': item['filename'],
'status': 'busy',
'original_status': item['status'],
'timeleft': item['timeleft'] if not queue['paused'] else -1,
})
@@ -111,7 +106,8 @@ class Sabnzbd(Downloader):
'name': item['name'],
'status': status,
'original_status': item['status'],
'timeleft': 0,
'timeleft': str(timedelta(seconds = 0)),
'folder': item['storage'],
})
return statuses
@@ -133,21 +129,21 @@ class Sabnzbd(Downloader):
return True
def call(self, params, use_json = True):
def call(self, request_params, use_json = True, **kwargs):
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(params, {
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(request_params, {
'apikey': self.conf('api_key'),
'output': 'json'
}))
data = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()})
data = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()}, **kwargs)
if use_json:
d = json.loads(data)
if d.get('error'):
log.error('Error getting data from SABNZBd: %s', d.get('error'))
return {}
return d[params['mode']]
return d.get(request_params['mode']) or d
else:
return data

View File

@@ -41,15 +41,22 @@ config = [{
{
'name': 'directory',
'type': 'directory',
'description': 'Where should Transmission saved the downloaded files?',
'description': 'Download to this directory. Keep empty for default Transmission download directory.',
},
{
'name': 'ratio',
'default': 10,
'type': 'int',
'type': 'float',
'advanced': True,
'description': 'Stop transfer when reaching ratio',
},
{
'name': 'ratiomode',
'default': 0,
'type': 'int',
'advanced': True,
'description': '0 = Use session limit, 1 = Use transfer limit, 2 = Disable limit.',
},
{
'name': 'manual',
'default': 0,

View File

@@ -1,11 +1,15 @@
from base64 import b64encode
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import isInt
from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
from datetime import timedelta
import httplib
import json
import os.path
import re
import shutil
import traceback
import urllib2
log = CPLog(__name__)
@@ -18,7 +22,7 @@ class Transmission(Downloader):
def download(self, data, movie, filedata = None):
log.debug('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type')))
log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type')))
# Load host from config and split out port.
host = self.conf('host').split(':')
@@ -27,22 +31,24 @@ class Transmission(Downloader):
return False
# Set parameters for Transmission
folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1]
folder_path = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep)
# Create the empty folder to download too
self.makeDir(folder_path)
params = {
'paused': self.conf('paused', default = 0),
'download-dir': folder_path
}
if len(self.conf('directory', default = '')) > 0:
folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1]
folder_path = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep)
# Create the empty folder to download too
self.makeDir(folder_path)
params['download-dir'] = folder_path
torrent_params = {}
if self.conf('ratio'):
torrent_params = {
'seedRatioLimit': self.conf('ratio'),
'seedRatioMode': self.conf('ratio')
'seedRatioMode': self.conf('ratiomode')
}
if not filedata and data.get('type') == 'torrent':
@@ -58,15 +64,97 @@ class Transmission(Downloader):
else:
remote_torrent = trpc.add_torrent_file(b64encode(filedata), arguments = params)
if not remote_torrent:
return False
# Change settings of added torrents
if torrent_params:
elif torrent_params:
trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
return True
except Exception, err:
log.error('Failed to change settings for transfer: %s', err)
log.info('Torrent sent to Transmission successfully.')
return self.downloadReturnId(remote_torrent['torrent-added']['hashString'])
except:
log.error('Failed to change settings for transfer: %s', traceback.format_exc())
return False
def getAllDownloadStatus(self):
log.debug('Checking Transmission download status.')
# Load host from config and split out port.
host = self.conf('host').split(':')
if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.')
return False
# Go through Queue
try:
trpc = TransmissionRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
return_params = {
'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isFinished', 'downloadDir', 'uploadRatio']
}
queue = trpc.get_alltorrents(return_params)
except Exception, err:
log.error('Failed getting queue: %s', err)
return False
statuses = StatusList(self)
# Get torrents status
# CouchPotato Status
#status = 'busy'
#status = 'failed'
#status = 'completed'
# Transmission Status
#status = 0 => "Torrent is stopped"
#status = 1 => "Queued to check files"
#status = 2 => "Checking files"
#status = 3 => "Queued to download"
#status = 4 => "Downloading"
#status = 4 => "Queued to seed"
#status = 6 => "Seeding"
#To do :
# add checking file
# manage no peer in a range time => fail
for item in queue['torrents']:
log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / confRatio=%s / isFinished=%s', (item['name'], item['id'], item['downloadDir'], item['hashString'], item['percentDone'], item['status'], item['eta'], item['uploadRatio'], self.conf('ratio'), item['isFinished']))
if not os.path.isdir(Env.setting('from', 'renamer')):
log.error('Renamer "from" folder doesn\'t to exist.')
return
if (item['percentDone'] * 100) >= 100 and (item['status'] == 6 or item['status'] == 0) and item['uploadRatio'] > self.conf('ratio'):
try:
trpc.stop_torrent(item['hashString'], {})
statuses.append({
'id': item['hashString'],
'name': item['name'],
'status': 'completed',
'original_status': item['status'],
'timeleft': str(timedelta(seconds = 0)),
'folder': os.path.join(item['downloadDir'], item['name']),
})
except Exception, err:
log.error('Failed to stop and remove torrent "%s" with error: %s', (item['name'], err))
statuses.append({
'id': item['hashString'],
'name': item['name'],
'status': 'failed',
'original_status': item['status'],
'timeleft': str(timedelta(seconds = 0)),
})
else:
statuses.append({
'id': item['hashString'],
'name': item['name'],
'status': 'busy',
'original_status': item['status'],
'timeleft': str(timedelta(seconds = item['eta'])), # Is ETA in seconds??
})
return statuses
class TransmissionRPC(object):
@@ -97,6 +185,7 @@ class TransmissionRPC(object):
try:
open_request = urllib2.urlopen(request)
response = json.loads(open_request.read())
log.debug('request: %s', json.dumps(ojson))
log.debug('response: %s', json.dumps(response))
if response['result'] == 'success':
log.debug('Transmission action successfull')
@@ -146,3 +235,18 @@ class TransmissionRPC(object):
arguments['ids'] = torrent_id
post_data = {'arguments': arguments, 'method': 'torrent-set', 'tag': self.tag}
return self._request(post_data)
def get_alltorrents(self, arguments):
post_data = {'arguments': arguments, 'method': 'torrent-get', 'tag': self.tag}
return self._request(post_data)
def stop_torrent(self, torrent_id, arguments):
arguments['ids'] = torrent_id
post_data = {'arguments': arguments, 'method': 'torrent-stop', 'tag': self.tag}
return self._request(post_data)
def remove_torrent(self, torrent_id, remove_local_data, arguments):
arguments['ids'] = torrent_id
arguments['delete-local-data'] = remove_local_data
post_data = {'arguments': arguments, 'method': 'torrent-remove', 'tag': self.tag}
return self._request(post_data)

View File

@@ -1,10 +1,12 @@
from base64 import b16encode, b32decode
from bencode import bencode, bdecode
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import isInt, ss
from couchpotato.core.logger import CPLog
from hashlib import sha1
from multipartpost import MultipartPostHandler
from datetime import timedelta
import os
import cookielib
import httplib
import json
@@ -66,7 +68,7 @@ class uTorrent(Downloader):
self.utorrent_api.set_torrent(torrent_hash, torrent_params)
if self.conf('paused', default = 0):
self.utorrent_api.pause_torrent(torrent_hash)
return True
return self.downloadReturnId(torrent_hash)
except Exception, err:
log.error('Failed to send torrent to uTorrent: %s', err)
return False
@@ -103,7 +105,36 @@ class uTorrent(Downloader):
log.debug('Nothing in queue')
return False
statuses = []
statuses = StatusList(self)
download_folder = ''
settings_dict = {}
try:
data = self.utorrent_api.get_settings()
utorrent_settings = json.loads(data)
# Create settings dict
for item in utorrent_settings['settings']:
if item[1] == 0: # int
settings_dict[item[0]] = int(item[2] if not item[2].strip() == '' else '0')
elif item[1] == 1: # bool
settings_dict[item[0]] = True if item[2] == 'true' else False
elif item[1] == 2: # string
settings_dict[item[0]] = item[2]
log.debug('uTorrent settings: %s', settings_dict)
# Get the download path from the uTorrent settings
if settings_dict['dir_completed_download_flag']:
download_folder = settings_dict['dir_completed_download']
elif settings_dict['dir_active_download_flag']:
download_folder = settings_dict['dir_active_download']
else:
log.info('No download folder set in uTorrent. Please set a download folder')
return False
except Exception, err:
log.error('Failed to get settings from uTorrent: %s', err)
return False
# Get torrents
for item in queue.get('torrents', []):
@@ -113,12 +144,18 @@ class uTorrent(Downloader):
if item[21] == 'Finished' or item[21] == 'Seeding':
status = 'completed'
if settings_dict['dir_add_label']:
release_folder = os.path.join(download_folder, item[11], item[2])
else:
release_folder = os.path.join(download_folder, item[2])
statuses.append({
'id': item[0],
'name': item[2],
'status': status,
'original_status': item[1],
'timeleft': item[10],
'timeleft': str(timedelta(seconds = item[10])),
'folder': release_folder,
})
return statuses
@@ -195,3 +232,7 @@ class uTorrentAPI(object):
def get_status(self):
action = "list=1"
return self._request(action)
def get_settings(self):
action = "action=getsettings"
return self._request(action)

View File

@@ -16,10 +16,8 @@ def runHandler(name, handler, *args, **kwargs):
def addEvent(name, handler, priority = 100):
if events.get(name):
e = events[name]
else:
e = events[name] = Event(name = name, threads = 10, exc_info = True, traceback = True, lock = threading.RLock())
if not events.get(name):
events[name] = []
def createHandle(*args, **kwargs):
@@ -35,7 +33,10 @@ def addEvent(name, handler, priority = 100):
return h
e.handle(createHandle, priority = priority)
events[name].append({
'handler': createHandle,
'priority': priority,
})
def removeEvent(name, handler):
e = events[name]
@@ -43,6 +44,12 @@ def removeEvent(name, handler):
def fireEvent(name, *args, **kwargs):
if not events.get(name): return
e = Event(name = name, threads = 10, asynch = kwargs.get('async', False), exc_info = True, traceback = True, lock = threading.RLock())
for event in events[name]:
e.handle(event['handler'], priority = event['priority'])
#log.debug('Firing event %s', name)
try:
@@ -52,6 +59,7 @@ def fireEvent(name, *args, **kwargs):
'single': False, # Return single handler
'merge': False, # Merge items
'in_order': False, # Fire them in specific order, waits for the other to finish
'async': False
}
# Do options
@@ -62,13 +70,6 @@ def fireEvent(name, *args, **kwargs):
options[x] = val
except: pass
e = events[name]
# Lock this event
e.lock.acquire()
e.asynchronous = False
# Make sure only 1 event is fired at a time when order is wanted
kwargs['event_order_lock'] = threading.RLock() if options['in_order'] or options['single'] else None
kwargs['event_return_on_result'] = options['single']
@@ -76,9 +77,6 @@ def fireEvent(name, *args, **kwargs):
# Fire
result = e(*args, **kwargs)
# Release lock for this event
e.lock.release()
if options['single'] and not options['merge']:
results = None
@@ -104,13 +102,14 @@ def fireEvent(name, *args, **kwargs):
# Merge
if options['merge'] and len(results) > 0:
results.reverse() # Priority 1 is higher then 100
# Dict
if isinstance(results[0], dict):
results.reverse()
merged = {}
for result in results:
merged = mergeDicts(merged, result)
merged = mergeDicts(merged, result, prepend_list = True)
results = merged
# Lists
@@ -140,13 +139,8 @@ def fireEvent(name, *args, **kwargs):
log.error('%s: %s', (name, traceback.format_exc()))
def fireEventAsync(*args, **kwargs):
try:
my_thread = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
my_thread.setDaemon(True)
my_thread.start()
return True
except Exception, e:
log.error('%s: %s', (args[0], e))
kwargs['async'] = True
fireEvent(*args, **kwargs)
def errorHandler(error):
etype, value, tb = error

View File

@@ -10,6 +10,20 @@ import sys
log = CPLog(__name__)
def link(src, dst):
if os.name == 'nt':
import ctypes
if ctypes.windll.kernel32.CreateHardLinkW(unicode(dst), unicode(src), 0) == 0: raise ctypes.WinError()
else:
os.link(src, dst)
def symlink(src, dst):
if os.name == 'nt':
import ctypes
if ctypes.windll.kernel32.CreateSymbolicLinkW(unicode(dst), unicode(src), 1 if os.path.isdir(src) else 0) in [0, 1280]: raise ctypes.WinError()
else:
os.symlink(src, dst)
def getUserDir():
try:
import pwd
@@ -53,7 +67,7 @@ def getDataDir():
def isDict(object):
return isinstance(object, dict)
def mergeDicts(a, b):
def mergeDicts(a, b, prepend_list = False):
assert isDict(a), isDict(b)
dst = a.copy()
@@ -67,7 +81,7 @@ def mergeDicts(a, b):
if isDict(current_src[key]) and isDict(current_dst[key]):
stack.append((current_dst[key], current_src[key]))
elif isinstance(current_src[key], list) and isinstance(current_dst[key], list):
current_dst[key].extend(current_src[key])
current_dst[key] = current_src[key] + current_dst[key] if prepend_list else current_dst[key] + current_src[key]
current_dst[key] = removeListDuplicates(current_dst[key])
else:
current_dst[key] = current_src[key]

View File

@@ -2,13 +2,15 @@ from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.providers.base import Provider
from couchpotato.environment import Env
log = CPLog(__name__)
class Notification(Plugin):
class Notification(Provider):
type = 'notification'
default_title = Env.get('appname')
test_message = 'ZOMG Lazors Pewpewpew!'
@@ -16,11 +18,12 @@ class Notification(Plugin):
listen_to = [
'renamer.after', 'movie.snatched',
'updater.available', 'updater.updated',
'core.message',
]
dont_listen_to = []
def __init__(self):
addEvent('notify.%s' % self.getName().lower(), self.notify)
addEvent('notify.%s' % self.getName().lower(), self._notify)
addApiView(self.testNotifyName(), self.test)
@@ -33,10 +36,17 @@ class Notification(Plugin):
def notify(message = None, group = {}, data = None):
if not self.conf('on_snatch', default = True) and listener == 'movie.snatched':
return
return self.notify(message = message, data = data if data else group, listener = listener)
return self._notify(message = message, data = data if data else group, listener = listener)
return notify
def getNotificationImage(self, size = 'small'):
return 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/notify.couch.%s.png' % size
def _notify(self, *args, **kwargs):
if self.isEnabled():
return self.notify(*args, **kwargs)
def notify(self, message = '', data = {}, listener = None):
pass
@@ -46,7 +56,7 @@ class Notification(Plugin):
log.info('Sending test to %s', test_type)
success = self.notify(
success = self._notify(
message = self.test_message,
data = {},
listener = 'test'

View File

@@ -11,7 +11,6 @@ class Boxcar(Notification):
url = 'https://boxcar.io/devices/providers/7MNNXY3UIzVBwvzkKwkC/notifications'
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
try:
message = message.strip()

View File

@@ -1,12 +1,13 @@
from couchpotato import get_session
from couchpotato.api import addApiView, addNonBlockApiView
from couchpotato.core.event import addEvent
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import tryInt, splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from couchpotato.core.settings.model import Notification as Notif
from couchpotato.environment import Env
from sqlalchemy.sql.expression import or_
import threading
import time
@@ -21,11 +22,6 @@ class CoreNotifier(Notification):
messages = []
listeners = []
listen_to = [
'renamer.after', 'movie.snatched',
'updater.available', 'updater.updated',
]
def __init__(self):
super(CoreNotifier, self).__init__()
@@ -54,7 +50,10 @@ class CoreNotifier(Notification):
addNonBlockApiView('notification.listener', (self.addListener, self.removeListener))
addApiView('notification.listener', self.listener)
fireEvent('schedule.interval', 'core.check_messages', self.checkMessages, hours = 12, single = True)
addEvent('app.load', self.clean)
addEvent('app.load', self.checkMessages)
def clean(self):
@@ -112,6 +111,22 @@ class CoreNotifier(Notification):
'notifications': notifications
})
def checkMessages(self):
prop_name = 'messages.last_check'
last_check = tryInt(Env.prop(prop_name, default = 0))
messages = fireEvent('cp.messages', last_check = last_check, single = True)
for message in messages:
if message.get('time') > last_check:
fireEvent('core.message', message = message.get('message'), data = message)
if last_check < message.get('time'):
last_check = message.get('time')
Env.prop(prop_name, value = last_check)
def notify(self, message = '', data = {}, listener = None):
db = get_session()

View File

@@ -21,20 +21,17 @@ var NotificationBase = new Class({
App.addEvent('load', function(){
App.block.notification = new Block.Menu(self, {
'button_class': 'icon2.eye-open',
'class': 'notification_menu',
'onOpen': self.markAsRead.bind(self)
})
$(App.block.notification).inject(App.getBlock('search'), 'after');
self.badge = new Element('div.badge').inject(App.block.notification, 'top').hide();
/* App.getBlock('notification').addLink(new Element('a.more', {
'href': App.createUrl('notifications'),
'text': 'Show older notifications'
})); */
});
window.addEvent('load', function(){
self.startInterval.delay(Browser.safari ? 100 : 0, self)
self.startInterval.delay($(window).getSize().x <= 480 ? 2000 : 300, self)
});
},
@@ -47,14 +44,19 @@ var NotificationBase = new Class({
result.el = App.getBlock('notification').addLink(
new Element('span.'+(result.read ? 'read' : '' )).adopt(
new Element('span.message', {'text': result.message}),
new Element('span.message', {'html': result.message}),
new Element('span.added', {'text': added.timeDiffInWords(), 'title': added})
)
, 'top');
self.notifications.include(result);
if(!result.read)
if(result.data.important !== undefined && !result.read){
var sticky = true
App.fireEvent('message', [result.message, sticky, result])
}
else if(!result.read){
self.setBadge(self.notifications.filter(function(n){ return !n.read}).length)
}
},
@@ -64,20 +66,26 @@ var NotificationBase = new Class({
self.badge[value ? 'show' : 'hide']()
},
markAsRead: function(){
var self = this;
markAsRead: function(force_ids){
var self = this,
ids = force_ids;
var rn = self.notifications.filter(function(n){
return !n.read
})
if(!force_ids) {
var rn = self.notifications.filter(function(n){
return !n.read && n.data.important === undefined
})
var ids = []
rn.each(function(n){
ids.include(n.id)
})
var ids = []
rn.each(function(n){
ids.include(n.id)
})
}
if(ids.length > 0)
Api.request('notification.markread', {
'data': {
'ids': ids.join(',')
},
'onSuccess': function(){
self.setBadge('')
}
@@ -93,11 +101,20 @@ var NotificationBase = new Class({
return;
}
Api.request('notification.listener', {
self.request = Api.request('notification.listener', {
'data': {'init':true},
'onSuccess': self.processData.bind(self)
}).send()
setInterval(function(){
if(self.request && self.request.isRunning()){
self.request.cancel();
self.startPoll()
}
}, 120000);
},
startPoll: function(){
@@ -143,26 +160,41 @@ var NotificationBase = new Class({
self.startPoll()
},
showMessage: function(message){
showMessage: function(message, sticky, data){
var self = this;
if(!self.message_container)
self.message_container = new Element('div.messages').inject(document.body);
var new_message = new Element('div.message', {
'text': message
}).inject(self.message_container);
var new_message = new Element('div', {
'class': 'message' + (sticky ? ' sticky' : ''),
'html': message
}).inject(self.message_container, 'top');
setTimeout(function(){
new_message.addClass('show')
}, 10);
setTimeout(function(){
var hide_message = function(){
new_message.addClass('hide')
setTimeout(function(){
new_message.destroy();
}, 1000);
}, 4000);
}
if(sticky)
new_message.grab(
new Element('a.close.icon2', {
'events': {
'click': function(){
self.markAsRead([data.id]);
hide_message();
}
}
})
);
else
setTimeout(hide_message, 4000);
},

View File

@@ -12,7 +12,6 @@ log = CPLog(__name__)
class Email(Notification):
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
# Extract all the settings from settings
from_address = self.conf('from')
@@ -50,6 +49,5 @@ class Email(Notification):
return True
except:
log.error('E-mail failed: %s', traceback.format_exc())
return False
return False

View File

@@ -44,7 +44,6 @@ class Growl(Notification):
log.error('Failed register of growl: %s', traceback.format_exc())
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
self.register()

View File

@@ -13,7 +13,6 @@ class Notifo(Notification):
url = 'https://api.notifo.com/v1/send_notification'
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
try:
params = {

View File

@@ -9,7 +9,6 @@ log = CPLog(__name__)
class NotifyMyAndroid(Notification):
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
nma = pynma.PyNMA()
keys = splitString(self.conf('api_key'))

View File

@@ -9,7 +9,6 @@ log = CPLog(__name__)
class NotifyMyWP(Notification):
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
keys = splitString(self.conf('api_key'))
p = PyNMWP(keys, self.conf('dev_key'))

View File

@@ -46,7 +46,6 @@ class Plex(Notification):
return True
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
hosts = [x.strip() + ':3000' for x in self.conf('host').split(",")]
successful = 0

View File

@@ -13,7 +13,6 @@ class Prowl(Notification):
}
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
data = {
'apikey': self.conf('api_key'),

View File

@@ -12,16 +12,15 @@ class Pushalot(Notification):
}
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
data = {
'AuthorizationToken': self.conf('auth_token'),
'Title': self.default_title,
'Body': toUnicode(message),
'LinkTitle': toUnicode("CouchPotato"),
'link': toUnicode("https://couchpota.to/"),
'IsImportant': self.conf('important'),
'IsSilent': self.conf('silent'),
'Image': toUnicode(self.getNotificationImage('medium') + '?1'),
'Source': toUnicode(self.default_title)
}
headers = {

View File

@@ -1,4 +1,5 @@
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.variable import getTitle
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from httplib import HTTPSConnection
@@ -11,21 +12,26 @@ class Pushover(Notification):
app_token = 'YkxHMYDZp285L265L3IwH3LmzkTaCy'
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
http_handler = HTTPSConnection("api.pushover.net:443")
data = {
api_data = {
'user': self.conf('user_key'),
'token': self.app_token,
'message': toUnicode(message),
'priority': self.conf('priority')
'priority': self.conf('priority'),
}
if data and data.get('library'):
api_data.extend({
'url': toUnicode('http://www.imdb.com/title/%s/' % data['library']['identifier']),
'url_title': toUnicode('%s on IMDb' % getTitle(data['library'])),
})
http_handler.request('POST',
"/1/messages.json",
headers = {'Content-type': 'application/x-www-form-urlencoded'},
body = tryUrlencode(data)
body = tryUrlencode(api_data)
)
response = http_handler.getresponse()

View File

@@ -12,7 +12,6 @@ class Toasty(Notification):
}
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
data = {
'title': self.default_title,

View File

@@ -0,0 +1,30 @@
from .main import Trakt
def start():
return Trakt()
config = [{
'name': 'trakt',
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'trakt',
'label': 'Trakt',
'description': 'add movies to your collection once downloaded. Fill in your username and password in the <a href="../automation/">Automation Trakt settings</a>',
'options': [
{
'name': 'notification_enabled',
'default': False,
'type': 'enabler',
},
{
'name': 'remove_watchlist_enabled',
'label': 'Remove from watchlist',
'default': False,
'type': 'bool',
},
],
}
],
}]

View File

@@ -0,0 +1,46 @@
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
log = CPLog(__name__)
class Trakt(Notification):
urls = {
'base': 'http://api.trakt.tv/%s',
'library': 'movie/library/%s',
'unwatchlist': 'movie/unwatchlist/%s',
}
listen_to = ['movie.downloaded']
def notify(self, message = '', data = {}, listener = None):
post_data = {
'username': self.conf('automation_username'),
'password' : self.conf('automation_password'),
'movies': [{
'imdb_id': data['library']['identifier'],
'title': data['library']['titles'][0]['title'],
'year': data['library']['year']
}] if data else []
}
result = self.call((self.urls['library'] % self.conf('automation_api_key')), post_data)
if self.conf('remove_watchlist_enabled'):
result = result and self.call((self.urls['unwatchlist'] % self.conf('automation_api_key')), post_data)
return result
def call(self, method_url, post_data):
try:
response = self.getJsonData(self.urls['base'] % method_url, params = post_data, cache_timeout = 1)
if response:
if response.get('status') == "success":
log.info('Successfully called Trakt')
return True
except:
pass
log.error('Failed to call trakt, check your login.')
return False

View File

@@ -32,7 +32,6 @@ class Twitter(Notification):
addApiView('notify.%s.credentials' % self.getName().lower(), self.getCredentials)
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
api = Api(self.consumer_key, self.consumer_secret, self.conf('access_token_key'), self.conf('access_token_secret'))

View File

@@ -13,10 +13,8 @@ class XBMC(Notification):
listen_to = ['renamer.after']
use_json_notifications = {}
couch_logo_url = 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/xbmc-notify.png'
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
hosts = splitString(self.conf('host'))
@@ -28,7 +26,7 @@ class XBMC(Notification):
if self.use_json_notifications.get(host):
response = self.request(host, [
('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.couch_logo_url}),
('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}),
('VideoLibrary.Scan', {}),
])
else:
@@ -90,7 +88,7 @@ class XBMC(Notification):
self.use_json_notifications[host] = True
# send the text message
resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message, 'image':self.couch_logo_url})])
resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message, 'image': self.getNotificationImage('small')})])
for result in resp:
if (result.get('result') and result['result'] == 'OK'):
log.debug('Message delivered successfully!')
@@ -113,7 +111,7 @@ class XBMC(Notification):
server = 'http://%s/xbmcCmds/' % host
# Notification(title, message [, timeout , image])
cmd = "xbmcHttp?command=ExecBuiltIn(Notification(%s,%s,'',%s))" % (urllib.quote(data['title']), urllib.quote(data['message']), urllib.quote(self.couch_logo_url))
cmd = "xbmcHttp?command=ExecBuiltIn(Notification(%s,%s,'',%s))" % (urllib.quote(data['title']), urllib.quote(data['message']), urllib.quote(self.getNotificationImage('medium')))
server += cmd
# I have no idea what to set to, just tried text/plain and seems to be working :)

View File

@@ -36,6 +36,20 @@ config = [{
'unit': 'hours',
'description': 'hours',
},
{
'name': 'required_genres',
'label': 'Required Genres',
'default': '',
'placeholder': 'Example: Action, Crime & Drama',
'description': 'Ignore movies that don\'t contain at least one set of genres. Sets are separated by "," and each word within a set must be separated with "&"'
},
{
'name': 'ignored_genres',
'label': 'Ignored Genres',
'default': '',
'placeholder': 'Example: Horror, Comedy & Drama & Romance',
'description': 'Ignore movies that contain at least one set of genres. Sets work the same as above.'
},
],
},
],

View File

@@ -10,11 +10,16 @@ class Automation(Plugin):
def __init__(self):
fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12))
addEvent('app.load', self.setCrons)
if not Env.get('dev'):
addEvent('app.load', self.addMovies)
addEvent('setting.save.automation.hour.after', self.setCrons)
def setCrons(self):
fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12))
def addMovies(self):
movies = fireEvent('automation.get_movies', merge = True)

View File

@@ -9,6 +9,8 @@ from couchpotato.core.plugins.base import Plugin
from couchpotato.core.plugins.scanner.main import Scanner
from couchpotato.core.settings.model import FileType, File
from couchpotato.environment import Env
from flask.helpers import send_file
from werkzeug.exceptions import NotFound
import os.path
import time
import traceback
@@ -71,7 +73,7 @@ class FileManager(Plugin):
db = get_session()
for root, dirs, walk_files in os.walk(Env.get('cache_dir')):
for filename in walk_files:
if root == python_cache or 'minified' in filename: continue
if root == python_cache or 'minified' in filename or 'version' in filename: continue
file_path = os.path.join(root, filename)
f = db.query(File).filter(File.path == toUnicode(file_path)).first()
if not f:
@@ -81,11 +83,13 @@ class FileManager(Plugin):
def showCacheFile(self, filename = ''):
cache_dir = Env.get('cache_dir')
filename = os.path.basename(filename)
file_path = os.path.join(Env.get('cache_dir'), os.path.basename(filename))
from flask.helpers import send_from_directory
return send_from_directory(cache_dir, filename)
if not os.path.isfile(file_path):
log.error('File "%s" not found', file_path)
raise NotFound()
return send_file(file_path, conditional = True)
def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = {}):

View File

@@ -1,17 +1,18 @@
.page.log .nav {
display: block;
text-align: center;
padding: 20px 0;
padding: 0 0 30px;
margin: 0;
font-size: 20px;
position: fixed;
width: 960px;
width: 100%;
bottom: 0;
left: 0;
background: #4E5969;
}
.page.log .nav li {
display: inline;
display: inline-block;
padding: 5px 10px;
margin: 0;
cursor: pointer;
@@ -24,7 +25,17 @@
.page.log .nav li.active {
font-weight: bold;
cursor: default;
font-size: 30px;
background: rgba(255,255,255,.1);
}
@media all and (max-width: 480px) {
.page.log .nav {
font-size: 14px;
}
.page.log .nav li {
padding: 5px;
}
}
.page.log .loading {

View File

@@ -8,7 +8,7 @@ config = [{
'groups': [
{
'tab': 'manage',
'label': 'movie library manager',
'label': 'Movie Library Manager',
'description': 'Add your existing movie folders.',
'options': [
{

View File

@@ -185,6 +185,8 @@ class Manage(Plugin):
# Add it to release and update the info
fireEvent('release.add', group = group)
fireEventAsync('library.update', identifier = identifier, on_complete = self.createAfterUpdate(folder, identifier))
else:
self.in_progress[folder]['to_go'] = self.in_progress[folder]['to_go'] - 1
return addToLibrary

View File

@@ -108,9 +108,8 @@ class MoviePlugin(Plugin):
now = time.time()
week = 262080
done_status = fireEvent('status.get', 'done', single = True)
available_status = fireEvent('status.get', 'available', single = True)
snatched_status = fireEvent('status.get', 'snatched', single = True)
done_status, available_status, snatched_status = \
fireEvent('status.get', ['done', 'available', 'snatched'], single = True)
db = get_session()
@@ -316,7 +315,7 @@ class MoviePlugin(Plugin):
for title in movie.library.titles:
if title.default: default_title = title.title
fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True, message = 'Updating "%s"' % default_title)
fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True)
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(id))
@@ -367,10 +366,8 @@ class MoviePlugin(Plugin):
library = fireEvent('library.add', single = True, attrs = params, update_after = update_library)
# Status
status_active = fireEvent('status.add', 'active', single = True)
snatched_status = fireEvent('status.add', 'snatched', single = True)
ignored_status = fireEvent('status.add', 'ignored', single = True)
downloaded_status = fireEvent('status.add', 'downloaded', single = True)
status_active, snatched_status, ignored_status, done_status, downloaded_status = \
fireEvent('status.get', ['active', 'snatched', 'ignored', 'done', 'downloaded'], single = True)
default_profile = fireEvent('profile.default', single = True)
@@ -397,7 +394,7 @@ class MoviePlugin(Plugin):
# Clean snatched history
for release in m.releases:
if release.status_id in [downloaded_status.get('id'), snatched_status.get('id')]:
if release.status_id in [downloaded_status.get('id'), snatched_status.get('id'), done_status.get('id')]:
if params.get('ignore_previous', False):
release.status_id = ignored_status.get('id')
else:
@@ -548,8 +545,7 @@ class MoviePlugin(Plugin):
def restatus(self, movie_id):
active_status = fireEvent('status.get', 'active', single = True)
done_status = fireEvent('status.get', 'done', single = True)
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
db = get_session()

View File

@@ -1,13 +1,15 @@
var MovieList = new Class({
Implements: [Options],
Implements: [Events, Options],
options: {
navigation: true,
limit: 50,
load_more: true,
loader: true,
menu: [],
add_new: false
add_new: false,
force_view: false
},
movies: [],
@@ -42,8 +44,11 @@ var MovieList = new Class({
}) : null
);
self.changeView(self.getSavedView() || self.options.view || 'details');
if($(window).getSize().x <= 480 && !self.options.force_view)
self.changeView('list');
else
self.changeView(self.getSavedView() || self.options.view || 'details');
self.getMovies();
App.addEvent('movie.added', self.movieAdded.bind(self))
@@ -120,7 +125,7 @@ var MovieList = new Class({
if(!self.navigation_counter) return;
self.navigation_counter.set('text', (count || 0));
self.navigation_counter.set('text', (count || 0) + ' movies');
},
@@ -144,65 +149,67 @@ var MovieList = new Class({
var self = this;
var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
self.current_view = self.getSavedView();
self.el.addClass(self.current_view+'_list')
self.el.addClass('with_navigation')
self.navigation = new Element('div.alph_nav').adopt(
self.navigation_actions = new Element('ul.inlay.actions.reversed'),
self.navigation_counter = new Element('span.counter[title=Total]'),
self.navigation_alpha = new Element('ul.numbers', {
'events': {
'click:relay(li)': function(e, el){
self.movie_list.empty()
self.activateLetter(el.get('data-letter'))
self.getMovies()
self.navigation = new Element('div.alph_nav').grab(
new Element('div').adopt(
self.navigation_alpha = new Element('ul.numbers', {
'events': {
'click:relay(li)': function(e, el){
self.movie_list.empty()
self.activateLetter(el.get('data-letter'))
self.getMovies()
}
}
}
}),
self.navigation_search_input = new Element('input.inlay', {
'placeholder': 'Search',
'events': {
'keyup': self.search.bind(self),
'change': self.search.bind(self)
}
}),
self.navigation_menu = new Block.Menu(self),
self.mass_edit_form = new Element('div.mass_edit_form').adopt(
new Element('span.select').adopt(
self.mass_edit_select = new Element('input[type=checkbox].inlay', {
'events': {
'change': self.massEditToggleAll.bind(self)
}
}),
self.mass_edit_selected = new Element('span.count', {'text': 0}),
self.mass_edit_selected_label = new Element('span', {'text': 'selected'})
),
new Element('div.quality').adopt(
self.mass_edit_quality = new Element('select'),
new Element('a.button.orange', {
'text': 'Change quality',
'events': {
'click': self.changeQualitySelected.bind(self)
}
})
),
new Element('div.delete').adopt(
new Element('span[text=or]'),
new Element('a.button.red', {
'text': 'Delete',
'events': {
'click': self.deleteSelected.bind(self)
}
})
),
new Element('div.refresh').adopt(
new Element('span[text=or]'),
new Element('a.button.green', {
'text': 'Refresh',
'events': {
'click': self.refreshSelected.bind(self)
}
})
}),
self.navigation_counter = new Element('span.counter[title=Total]'),
self.navigation_actions = new Element('ul.inlay.actions.reversed'),
self.navigation_search_input = new Element('input.search.inlay', {
'title': 'Search through ' + self.options.identifier,
'placeholder': 'Search through ' + self.options.identifier,
'events': {
'keyup': self.search.bind(self),
'change': self.search.bind(self)
}
}),
self.navigation_menu = new Block.Menu(self),
self.mass_edit_form = new Element('div.mass_edit_form').adopt(
new Element('span.select').adopt(
self.mass_edit_select = new Element('input[type=checkbox].inlay', {
'events': {
'change': self.massEditToggleAll.bind(self)
}
}),
self.mass_edit_selected = new Element('span.count', {'text': 0}),
self.mass_edit_selected_label = new Element('span', {'text': 'selected'})
),
new Element('div.quality').adopt(
self.mass_edit_quality = new Element('select'),
new Element('a.button.orange', {
'text': 'Change quality',
'events': {
'click': self.changeQualitySelected.bind(self)
}
})
),
new Element('div.delete').adopt(
new Element('span[text=or]'),
new Element('a.button.red', {
'text': 'Delete',
'events': {
'click': self.deleteSelected.bind(self)
}
})
),
new Element('div.refresh').adopt(
new Element('span[text=or]'),
new Element('a.button.green', {
'text': 'Refresh',
'events': {
'click': self.refreshSelected.bind(self)
}
})
)
)
)
).inject(self.el, 'top');
@@ -247,18 +254,19 @@ var MovieList = new Class({
});
// Get available chars and highlight
Api.request('movie.available_chars', {
'data': Object.merge({
'status': self.options.status
}, self.filter),
'onComplete': function(json){
json.chars.split('').each(function(c){
self.letters[c.capitalize()].addClass('available')
})
}
});
if(self.navigation.isDisplayed() || self.navigation.isVisible())
Api.request('movie.available_chars', {
'data': Object.merge({
'status': self.options.status
}, self.filter),
'onSuccess': function(json){
json.chars.split('').each(function(c){
self.letters[c.capitalize()].addClass('available')
})
}
});
// Add menu or hide
if (self.options.menu.length > 0)
@@ -266,17 +274,7 @@ var MovieList = new Class({
self.navigation_menu.addLink(menu_item);
})
else
self.navigation_menu.hide()
self.nav_scrollspy = new ScrollSpy({
min: 10,
onEnter: function(){
self.navigation.addClass('float')
},
onLeave: function(){
self.navigation.removeClass('float')
}
});
self.navigation_menu.hide();
},
@@ -429,12 +427,12 @@ var MovieList = new Class({
.addClass(new_view+'_list')
self.current_view = new_view;
Cookie.write(self.options.identifier+'_view', new_view, {duration: 1000});
Cookie.write(self.options.identifier+'_view2', new_view, {duration: 1000});
},
getSavedView: function(){
var self = this;
return Cookie.read(self.options.identifier+'_view') || 'details';
return Cookie.read(self.options.identifier+'_view2');
},
search: function(){
@@ -475,12 +473,39 @@ var MovieList = new Class({
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 || 'movie.list', {
'data': Object.merge({
'status': self.options.status,
'limit_offset': self.options.limit + ',' + self.offset
}, self.filter),
'onComplete': function(json){
'onSuccess': function(json){
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.movies);
self.addMovies(json.movies, json.total);
if(self.scrollspy) {
@@ -488,7 +513,8 @@ var MovieList = new Class({
self.scrollspy.start();
}
self.checkIfEmpty()
self.checkIfEmpty();
self.fireEvent('loaded');
}
});
},
@@ -515,10 +541,10 @@ var MovieList = new Class({
self.title[is_empty ? 'hide' : 'show']()
if(self.description)
self.description[is_empty ? 'hide' : 'show']()
self.description.setStyle('display', [is_empty ? 'none' : ''])
if(is_empty && self.options.on_empty_element){
self.el.grab(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();

View File

@@ -89,41 +89,22 @@ MA.Release = new Class({
}
});
if(self.movie.data.releases.length == 0){
if(self.movie.data.releases.length == 0)
self.el.hide()
}
else {
var buttons_done = false;
self.movie.data.releases.sortBy('-info.score').each(function(release){
if(buttons_done) return;
var status = Status.get(release.status_id);
if((self.next_release && (status.identifier == 'ignored' || status.identifier == 'failed')) || (!self.next_release && status.identifier == 'available')){
self.hide_on_click = false;
self.show();
buttons_done = true;
}
});
}
else
self.showHelper();
},
show: function(e){
createReleases: function(){
var self = this;
if(e)
(e).preventDefault();
if(!self.options_container){
self.options_container = new Element('div.options').adopt(
self.release_container = new Element('div.releases.table').adopt(
self.trynext_container = new Element('div.buttons.try_container')
)
).inject(self.movie, 'top');
);
// Header
new Element('div.item.head').adopt(
@@ -238,9 +219,71 @@ MA.Release = new Class({
}
},
show: function(e){
var self = this;
if(e)
(e).preventDefault();
self.createReleases();
self.options_container.inject(self.movie, 'top');
self.movie.slide('in', self.options_container);
},
showHelper: function(e){
var self = this;
if(e)
(e).preventDefault();
self.createReleases();
self.trynext_container = new Element('div.buttons.trynext').inject(self.movie.info_container);
if(self.next_release || self.last_release){
self.trynext_container.adopt(
self.next_release ? [new Element('a.icon.readd', {
'text': self.last_release ? 'Download another release' : 'Download the best release',
'events': {
'click': self.tryNextRelease.bind(self)
}
}),
new Element('a.icon.download', {
'text': 'pick one yourself',
'events': {
'click': function(){
self.movie.quality.fireEvent('click');
}
}
})] : null,
new Element('a.icon.completed', {
'text': 'mark this movie done',
'events': {
'click': function(){
Api.request('movie.delete', {
'data': {
'id': self.movie.get('id'),
'delete_from': 'wanted'
},
'onComplete': function(){
var movie = $(self.movie);
movie.set('tween', {
'duration': 300,
'onComplete': function(){
self.movie.destroy()
}
});
movie.tween('height', 0);
}
});
}
}
})
)
}
},
get: function(release, type){
return release.info[type] || 'n/a'
},
@@ -251,14 +294,15 @@ MA.Release = new Class({
var release_el = self.release_container.getElement('#release_'+release.id),
icon = release_el.getElement('.download.icon');
icon.addClass('spinner');
self.movie.busy(true);
Api.request('release.download', {
'data': {
'id': release.id
},
'onComplete': function(json){
icon.removeClass('spinner')
self.movie.busy(false);
if(json.success)
icon.addClass('completed');
else
@@ -281,6 +325,8 @@ MA.Release = new Class({
tryNextRelease: function(movie_id){
var self = this;
self.createReleases();
if(self.last_release)
self.ignore(self.last_release);

View File

@@ -1,24 +1,73 @@
.movies {
padding: 60px 0 20px;
padding: 10px 0 20px;
position: relative;
z-index: 3;
width: 100%;
}
.movies > div {
clear: both;
}
.movies.thumbs_list > div:not(.description) {
margin-right: -4px;
text-align: center;
}
.movies .loading {
display: block;
padding: 20px 0 0 0;
width: 100%;
z-index: 3;
transition: all .4s cubic-bezier(0.9,0,0.1,1);
height: 40px;
opacity: 1;
position: absolute;
text-align: center;
}
.movies .loading.hide {
height: 0;
padding: 0;
opacity: 0;
margin-top: -20px;
overflow: hidden;
}
.movies .loading .spinner {
display: inline-block;
}
.movies .loading .message {
margin: 0 20px;
}
.movies h2 {
margin-bottom: 20px;
}
@media all and (max-width: 480px) {
.movies h2 {
font-size: 25px;
margin-bottom: 10px;
}
}
.movies > .description {
position: absolute;
top: 30px;
right: 0;
font-style: italic;
text-shadow: none;
opacity: 0.8;
}
.movies:hover > .description {
opacity: 1;
}
@media all and (max-width: 860px) {
.movies > .description {
display: none;
}
}
.movies.thumbs_list {
padding: 20px 0 20px;
@@ -28,19 +77,20 @@
padding-top: 6px;
}
.movies.mass_edit_list {
padding-top: 90px;
}
.movies .movie {
position: relative;
border-radius: 4px;
margin: 10px 0;
padding-left: 20px;
overflow: hidden;
width: 100%;
height: 180px;
transition: all 0.2s linear;
transition: all 0.6s cubic-bezier(0.9,0,0.1,1);
}
.movies.details_list .movie {
padding-left: 120px;
}
.movies.list_list .movie:not(.details_view),
.movies.mass_edit_list .movie {
@@ -48,13 +98,21 @@
}
.movies.thumbs_list .movie {
width: 153px;
height: 230px;
width: 16.66667%;
height: auto;
display: inline-block;
margin: 0 8px 0 0;
margin: 0;
padding: 0;
vertical-align: top;
border-radius: 0;
box-shadow: none;
border: 0;
}
.movies.thumbs_list .movie:nth-child(6n+6) {
margin: 0;
@media all and (max-width: 800px) {
.movies.thumbs_list .movie {
width: 25%;
}
}
.movies .movie .mask {
@@ -82,8 +140,8 @@
.movies .data {
padding: 20px;
height: 100%;
width: 840px;
position: absolute;
width: 100%;
position: relative;
right: 0;
border-radius: 0;
transition: all .6s cubic-bezier(0.9,0,0.1,1);
@@ -92,14 +150,15 @@
.movies.mass_edit_list .movie .data {
height: 30px;
padding: 3px 0 3px 10px;
width: 938px;
box-shadow: none;
border: 0;
background: none;
background: #4e5969;
}
.movies.thumbs_list .data {
position: absolute;
left: 0;
top: 0;
width: 100%;
padding: 10px;
height: 100%;
@@ -144,8 +203,11 @@
height: 100%;
border-radius: 4px 0 0 4px;
transition: all .6s cubic-bezier(0.9,0,0.1,1);
}
.movies.thumbs_list .poster {
position: relative;
border-radius: 0;
}
.movies.list_list .movie:not(.details_view) .poster,
.movies.mass_edit_list .poster {
width: 20px;
@@ -159,38 +221,72 @@
.movies.thumbs_list .poster {
width: 100%;
height: 100%;
transition: none;
}
.movies .poster img,
.options .poster img {
width: 101%;
height: 101%;
width: 100%;
height: 100%;
}
.movies.thumbs_list .poster img {
height: auto;
width: 100%;
top: 0;
bottom: 0;
border-radius: 0;
}
.movies .info {
position: relative;
height: 100%;
width: 100%;
}
.movies .info .title {
display: inline;
position: absolute;
font-size: 28px;
font-weight: bold;
margin-bottom: 10px;
margin-top: 2px;
left: 0;
top: 0;
width: 90%;
width: 100%;
padding-right: 80px;
transition: all 0.2s linear;
}
.movies .info .title span {
display: block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
width: 100%;
height: 30px;
line-height: 30px;
top: -5px;
position: relative;
}
.movies.thumbs_list .info .title span {
white-space: normal;
overflow: auto;
height: auto;
text-align: left;
}
@media all and (max-width: 480px) {
.movies.thumbs_list .movie .info .title span,
.movies.thumbs_list .movie .info .year {
font-size: 15px;
line-height: 15px;
overflow: hidden;
}
}
.movies.list_list .movie:not(.details_view) .info .title,
.movies.mass_edit_list .info .title {
font-size: 16px;
font-weight: normal;
text-overflow: ellipsis;
width: auto;
overflow: hidden;
}
.movies.thumbs_list .movie:not(.no_thumbnail) .info {
@@ -202,25 +298,22 @@
.movies.thumbs_list .info .title {
font-size: 21px;
text-shadow: 0 0 10px #000;
word-wrap: break-word;
padding: 0;
}
.movies .info .year {
position: absolute;
font-size: 30px;
margin-bottom: 10px;
color: #bbb;
width: 10%;
right: 0;
top: 0;
top: 1px;
text-align: right;
transition: all 0.2s linear;
font-weight: normal;
}
.movies.list_list .movie:not(.details_view) .info .year,
.movies.mass_edit_list .info .year {
font-size: 16px;
width: 6%;
font-size: 1.25em;
right: 10px;
}
@@ -232,16 +325,14 @@
top: auto;
right: auto;
color: #FFF;
text-shadow: none;
text-shadow: 0 0 6px #000;
}
.movies .info .description {
position: absolute;
top: 30px;
clear: both;
height: 80px;
bottom: 30px;
overflow: hidden;
position: absolute;
}
.movies .data:hover .description {
overflow: auto;
@@ -254,12 +345,17 @@
.movies .data .quality {
position: absolute;
bottom: 0;
bottom: 2px;
display: block;
min-height: 20px;
vertical-align: mid;
}
@media all and (max-width: 480px) {
.movies .data .quality {
display: none;
}
}
.movies .status_suggest .data .quality,
.movies.thumbs_list .data .quality {
display: none;
@@ -275,9 +371,8 @@
vertical-align: middle;
display: inline-block;
text-transform: uppercase;
text-shadow: none;
font-weight: normal;
margin: 0 2px;
margin: 0 4px 0 0;
border-radius: 2px;
background-color: rgba(255,255,255,0.1);
}
@@ -285,7 +380,7 @@
.movies.mass_edit_list .data .quality {
text-align: right;
right: 0;
margin-right: 50px;
margin-right: 60px;
z-index: 1;
}
@@ -315,18 +410,40 @@
bottom: 20px;
right: 20px;
line-height: 0;
margin-top: -25px;
top: 0;
opacity: 0;
display: none;
width: 0;
}
.movies.thumbs_list .data .actions {
bottom: 8px;
right: 10px;
@media all and (max-width: 480px) {
.movies .data .actions {
display: none !important;
}
}
.movies .data:hover .action { opacity: 0.6; }
.movies .data:hover .action:hover { opacity: 1; }
.movies.mass_edit_list .data .actions {
display: none;
.movies .movie:hover .data .actions {
opacity: 1;
display: inline-block;
width: auto;
}
.movies.details_list .data .actions {
top: auto;
bottom: 18px;
}
.movies .movie:hover .actions {
opacity: 1;
display: inline-block;
}
.movies.thumbs_list .data .actions {
bottom: 2px;
right: 10px;
top: auto;
}
.movies .movie:hover .action { opacity: 0.6; }
.movies .movie:hover .action:hover { opacity: 1; }
.movies .data .action {
background-repeat: no-repeat;
@@ -335,11 +452,14 @@
width: 26px;
height: 26px;
padding: 3px;
opacity: 0;
}
.movies.mass_edit_list .movie .data .actions {
display: none;
}
.movies.list_list .movie:not(.details_view) .data:hover .actions,
.movies.mass_edit_list .data:hover .actions {
.movies.list_list .movie:not(.details_view):hover .actions,
.movies.mass_edit_list .movie:hover .actions {
margin: 0;
background: #4e5969;
top: 2px;
@@ -354,7 +474,8 @@
font-size: 20px;
position: absolute;
padding: 70px 0 0;
width: 100%;
left: 120px;
right: 0;
}
.movies .delete_container .cancel {
}
@@ -372,8 +493,8 @@
.movies .options {
position: absolute;
margin-left: 120px;
width: 840px;
right: 0;
left: 120px;
}
.movies .options .form {
@@ -396,7 +517,6 @@
.movies .options .table .item.ignored span {
text-decoration: line-through;
color: rgba(255,255,255,0.4);
text-shadow: none;
}
.movies .options .table .item.ignored .delete {
background-image: url('../images/icon.undo.png');
@@ -430,7 +550,7 @@
overflow: hidden;
}
.movies .options .table .name {
width: 350px;
width: 340px;
overflow: hidden;
text-align: left;
padding: 0 10px;
@@ -464,6 +584,9 @@
text-align: center;
transition: all .6s cubic-bezier(0.9,0,0.1,1);
overflow: hidden;
left: 0;
position: absolute;
z-index: 10;
}
.movies .movie .trailer_container.hide {
height: 0 !important;
@@ -480,6 +603,7 @@
background: #4e5969;
border-radius: 0 0 2px 2px;
transition: all .2s cubic-bezier(0.9,0,0.1,1) .2s;
z-index: 11;
}
.movies .movie .hide_trailer.hide {
top: -30px;
@@ -510,6 +634,54 @@
.movies .movie .releases .last_release > :first-child {
margin-left: -6px;
}
.movies .movie .trynext {
display: inline;
position: absolute;
right: 135px;
z-index: 2;
opacity: 0;
background: #4e5969;
min-width: 300px;
text-align: right;
height: 100%;
padding: 3px 0;
top: 0;
}
@media all and (max-width: 480px) {
.movies .movie .trynext {
display: none;
}
}
.movies.mass_edit_list .trynext { display: none; }
.wanted .movies .movie .trynext {
padding-right: 50px;
}
.movies .movie:hover .trynext {
opacity: 1;
}
.movies.details_list .movie .trynext {
background: #47515f;
padding: 0;
right: 0;
bottom: 35px;
height: auto;
}
.movies .movie .trynext a {
background-position: 5px center;
padding: 0 5px 0 25px;
margin-right: 10px;
color: #FFF;
border-radius: 2px;
}
.movies .movie .trynext a:last-child {
margin: 0;
}
.movies .movie .trynext a:hover {
background-color: #369545;
}
.movies .load_more {
display: block;
@@ -523,24 +695,32 @@
.movies .alph_nav {
transition: box-shadow .4s linear;
position: fixed;
position: relative;
z-index: 4;
top: 0;
padding: 100px 60px 7px;
width: 1080px;
margin: 0 -60px;
box-shadow: 0 20px 20px -22px rgba(0,0,0,0.1);
background: #4e5969;
top: 0px;
right: 0;
margin: 0 auto;
width: 100%;
padding: 10px 0;
}
.movies .alph_nav.float {
box-shadow: 0 30px 30px -32px rgba(0,0,0,0.5);
border-radius: 0;
@media all and (max-width: 480px) {
.movies .alph_nav {
display: none;
}
}
.movies .alph_nav ul.numbers,
.movies .alph_nav > div {
position: relative;
max-width: 980px;
margin: 0 auto;
padding: 0;
min-height: 24px;
}
.movies .alph_nav .numbers,
.movies .alph_nav .counter,
.movies .alph_nav ul.actions {
.movies .alph_nav .actions {
list-style: none;
padding: 0 0 1px;
margin: 0;
@@ -549,49 +729,62 @@
}
.movies .alph_nav .counter {
width: 60px;
text-align: center;
text-align: right;
position: absolute;
right: 270px;
background: #4e5969;
padding: 4px 10px;
}
.movies .alph_nav .numbers li,
.movies .alph_nav .actions li {
display: inline-block;
vertical-align: top;
width: 20px;
height: 24px;
line-height: 26px;
line-height: 23px;
text-align: center;
cursor: pointer;
color: rgba(255,255,255,0.2);
border: 1px solid transparent;
transition: all 0.1s ease-in-out;
text-shadow: none;
}
.movies .alph_nav .numbers li:first-child {
width: 43px;
}
@media all and (max-width: 900px) {
.movies .alph_nav .numbers {
display: none;
}
}
.movies .alph_nav .numbers li {
width: auto;
padding: 0 4px;
}
.movies .alph_nav li.available {
color: rgba(255,255,255,0.8);
color: #FFF;
font-weight: bolder;
}
.movies .alph_nav li.active.available, .movies .alph_nav li.available:hover {
color: #fff;
font-size: 20px;
line-height: 20px;
.movies .alph_nav li.active.available,
.movies .alph_nav li.available:hover {
background: rgba(255,255,255,.1);
}
.movies .alph_nav input {
.movies .alph_nav .search {
padding: 6px 5px;
margin: 0 0 0 6px;
float: left;
width: 155px;
margin: 0 0 0 20px;
position: absolute;
right: 30px;
width: 154px;
height: 25px;
transition: all 0.6s cubic-bezier(0.9,0,0.1,1);
}
.movies .alph_nav .actions {
margin: 0 6px 0 0;
-moz-user-select: none;
position: absolute;
right: 183px;
}
.movies .alph_nav .actions li {
border-radius: 1px;
@@ -675,10 +868,12 @@
}
.movies .alph_nav .more_menu {
margin-left: 48px;
right: 0;
position: absolute;
}
.movies .alph_nav .more_menu > a {
background-color: #4e5969;
background-position: center -158px;
}
@@ -735,7 +930,6 @@
font-weight: bold;
display: inline-block;
text-transform: uppercase;
text-shadow: none;
font-weight: normal;
font-size: 20px;
border-left: 1px solid rgba(255, 255, 255, .2);

View File

@@ -22,7 +22,10 @@ var Movie = new Class({
addEvents: function(){
var self = this;
App.addEvent('movie.update.'+self.data.id, self.update.bind(self));
App.addEvent('movie.update.'+self.data.id, function(notification){
self.busy(false)
self.update.delay(2000, self, notification);
});
['movie.busy', 'searcher.started'].each(function(listener){
App.addEvent(listener+'.'+self.data.id, function(notification){
@@ -57,17 +60,19 @@ var Movie = new Class({
var self = this;
if(!set_busy){
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;
}, 400);
}
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;
}, 400);
}
}, 1000)
}
else if(!self.spinner) {
self.createMask();
@@ -126,12 +131,14 @@ var Movie = new Class({
self.thumbnail = File.Select.single('poster', self.data.library.files),
self.data_container = new Element('div.data.inlay.light').adopt(
self.info_container = new Element('div.info').adopt(
self.title = new Element('div.title', {
'text': self.getTitle() || 'n/a'
}),
self.year = new Element('div.year', {
'text': self.data.library.year || 'n/a'
}),
new Element('div.title').adopt(
self.title = new Element('span', {
'text': self.getTitle() || 'n/a'
}),
self.year = new Element('div.year', {
'text': self.data.library.year || 'n/a'
})
),
self.rating = new Element('div.rating.icon', {
'text': self.data.library.rating
}),

View File

@@ -1,113 +1,153 @@
.search_form {
display: inline-block;
vertical-align: middle;
width: 25%;
position: absolute;
right: 105px;
top: 0;
text-align: right;
height: 100%;
border-bottom: 4px solid transparent;
transition: all .4s cubic-bezier(0.9,0,0.1,1);
position: absolute;
z-index: 20;
border: 1px solid transparent;
border-width: 0 0 4px;
}
.search_form input {
padding: 4px 20px 4px 4px;
margin: 0;
font-size: 14px;
width: 100%;
height: 24px;
.search_form:hover {
border-color: #047792;
}
.search_form input:focus {
padding-right: 83px;
@media all and (max-width: 480px) {
.search_form {
right: 44px;
}
}
.search_form.focused,
.search_form.shown {
border-color: #04bce6;
}
.search_form .input {
height: 100%;
overflow: hidden;
width: 45px;
transition: all .4s cubic-bezier(0.9,0,0.1,1);
}
.search_form.focused .input,
.search_form.shown .input {
width: 380px;
background: #4e5969;
}
.search_form .input .enter {
background: #369545 url('../images/sprite.png') right -188px no-repeat;
padding: 0 20px 0 4px;
border-radius: 2px;
text-transform: uppercase;
font-size: 10px;
margin-left: -78px;
display: inline-block;
opacity: 0;
position: relative;
top: -2px;
cursor: pointer;
vertical-align: middle;
visibility: hidden;
}
.search_form.focused .input .enter {
visibility: visible;
.search_form .input input {
border-radius: 0;
display: block;
width: 100%;
border: 0;
background: rgba(255,255,255,.08);
color: #FFF;
font-size: 25px;
height: 100%;
padding: 10px;
width: 100%;
opacity: 0;
padding: 0 40px 0 10px;
transition: all .4s ease-in-out .2s;
}
.search_form.focused.filled .input .enter {
opacity: 1;
.search_form.focused .input input,
.search_form.shown .input input {
opacity: 1;
}
@media all and (max-width: 480px) {
.search_form .input input {
font-size: 15px;
}
.search_form.focused .input,
.search_form.shown .input {
width: 277px;
}
}
.search_form .input a {
width: 17px;
height: 20px;
display: inline-block;
margin: -2px 0 0 2px;
top: 4px;
right: 5px;
background: url('../images/sprite.png') left -37px no-repeat;
cursor: pointer;
opacity: 0;
transition: all 0.2s ease-in-out;
vertical-align: middle;
.search_form .input a {
position: absolute;
top: 0;
right: 0;
width: 44px;
height: 100%;
cursor: pointer;
vertical-align: middle;
text-align: center;
line-height: 66px;
font-size: 15px;
color: #FFF;
}
.search_form .input a:after {
content: "\e03e";
}
.search_form.filled .input a {
opacity: 1;
.search_form.shown.filled .input a:after {
content: "\e04e";
}
@media all and (max-width: 480px) {
.search_form .input a {
line-height: 44px;
}
}
.search_form .results_container {
text-align: left;
position: absolute;
background: #5c697b;
margin: 6px 0 0 -230px;
margin: 4px 0 0;
width: 470px;
min-height: 140px;
border-radius: 3px;
box-shadow: 0 20px 20px -10px rgba(0,0,0,0.55);
display: none;
}
@media all and (max-width: 480px) {
.search_form .results_container {
width: 320px;
}
}
.search_form.focused.filled .results_container,
.search_form.shown.filled .results_container {
display: block;
}
.search_form .results_container:before {
content: ' ';
height: 0;
position: relative;
width: 0;
border: 10px solid transparent;
border-bottom-color: #5c697b;
display: block;
top: -20px;
left: 346px;
}
.search_form .results {
max-height: 570px;
overflow-x: hidden;
padding: 10px 0;
margin-top: -18px;
}
.movie_result {
overflow: hidden;
height: 140px;
height: 50px;
position: relative;
}
.movie_result .options {
position: absolute;
height: 100%;
width: 100%;
top: 0;
left: 0;
left: 30px;
right: 0;
padding: 13px;
border: 1px solid transparent;
border-width: 1px 0;
border-radius: 0;
box-shadow: inset 0 1px 8px rgba(0,0,0,0.25);
}
.movie_result .options > .in_library_wanted {
margin-top: -7px;
}
.movie_result .options > div {
padding: 0 15px;
border: 0;
}
@@ -122,6 +162,13 @@
}
.movie_result .options select[name=title] { width: 180px; }
.movie_result .options select[name=profile] { width: 90px; }
@media all and (max-width: 480px) {
.movie_result .options select[name=title] { width: 90px; }
.movie_result .options select[name=profile] { width: 60px; }
}
.movie_result .options .button {
vertical-align: middle;
@@ -130,25 +177,21 @@
.movie_result .options .message {
height: 100%;
line-height: 140px;
font-size: 20px;
text-align: center;
color: #fff;
line-height: 20px;
}
.movie_result .data {
padding: 0 15px;
position: absolute;
height: 100%;
width: 100%;
top: 0;
left: 0;
left: 30px;
right: 0;
background: #5c697b;
cursor: pointer;
border-bottom: 1px solid #333;
border-top: 1px solid rgba(255,255,255, 0.15);
transition: all .6s cubic-bezier(0.9,0,0.1,1);
border-top: 1px solid rgba(255,255,255, 0.08);
transition: all .4s cubic-bezier(0.9,0,0.1,1);
}
.movie_result .data.open {
left: 100%;
@@ -158,53 +201,46 @@
.movie_result .in_wanted, .movie_result .in_library {
position: absolute;
margin-top: 105px;
bottom: 2px;
left: 14px;
font-size: 11px;
}
.movie_result .thumbnail {
width: 17%;
display: inline-block;
margin: 15px 3% 15px 0;
width: 34px;
min-height: 100%;
display: block;
margin: 0;
vertical-align: top;
border-radius: 3px;
box-shadow: 0 0 3px rgba(0,0,0,0.35);
}
.movie_result .info {
width: 80%;
display: inline-block;
vertical-align: top;
padding: 15px 0;
height: 120px;
overflow: hidden;
}
.movie_result .info .tagline {
max-height: 70px;
overflow: hidden;
display: inline-block;
}
.movie_result .add +.info {
margin-left: 20%;
position: absolute;
top: 20%;
left: 15px;
right: 60px;
vertical-align: middle;
}
.movie_result .info h2 {
font-weight: normal;
font-size: 20px;
display: block;
margin: 0;
font-size: 17px;
line-height: 20px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
width: 100%;
}
.movie_result .info h2 span {
padding: 0 5px;
position: absolute;
right: -60px;
}
.movie_result .info h2 span:before { content: "("; }
.movie_result .info h2 span:after { content: ")"; }
.search_form .mask,
.movie_result .mask {
border-radius: 3px;
position: absolute;
height: 100%;
width: 100%;

View File

@@ -7,33 +7,30 @@ Block.Search = new Class({
create: function(){
var self = this;
var focus_timer = 0;
self.el = new Element('div.search_form').adopt(
new Element('div.input').adopt(
self.input = new Element('input.inlay', {
self.input = new Element('input', {
'placeholder': 'Search & add a new movie',
'events': {
'keyup': self.keyup.bind(self),
'focus': function(){
if(focus_timer) clearTimeout(focus_timer);
self.el.addClass('focused')
if(this.get('value'))
self.hideResults(false)
},
'blur': function(){
(function(){
focus_timer = (function(){
self.el.removeClass('focused')
}).delay(2000);
}).delay(100);
}
}
}),
new Element('span.enter', {
new Element('a.icon2', {
'events': {
'click': self.keyup.bind(self)
},
'text':'Enter'
}),
new Element('a', {
'events': {
'click': self.clear.bind(self)
'click': self.clear.bind(self),
'touchend': self.clear.bind(self)
}
})
),
@@ -59,13 +56,21 @@ Block.Search = new Class({
var self = this;
(e).preventDefault();
self.last_q = '';
self.input.set('value', '');
self.input.focus()
if(self.last_q === ''){
self.input.blur()
self.last_q = null;
}
else {
self.movies = []
self.results.empty()
self.el.removeClass('filled')
self.last_q = '';
self.input.set('value', '');
self.input.focus()
self.movies = []
self.results.empty()
self.el.removeClass('filled')
}
},
hideResults: function(bool){
@@ -92,8 +97,10 @@ Block.Search = new Class({
self.el[self.q() ? 'addClass' : 'removeClass']('filled')
if(self.q() != self.last_q && (['enter'].indexOf(e.key) > -1 || e.type == 'click'))
self.autocomplete()
if(self.q() != self.last_q){
if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer)
self.autocomplete_timer = self.autocomplete.delay(300, self)
}
},
@@ -197,6 +204,11 @@ Block.Search.Item = new Class({
self.el = new Element('div.movie_result', {
'id': info.imdb
}).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', {
'tween': {
@@ -207,11 +219,6 @@ Block.Search.Item = new Class({
'click': self.showOptions.bind(self)
}
}).adopt(
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
'src': info.images.poster[0],
'height': null,
'width': null
}) : null,
new Element('div.info').adopt(
self.title = new Element('h2', {
'text': info.titles[0]
@@ -219,28 +226,11 @@ Block.Search.Item = new Class({
self.year = info.year ? new Element('span.year', {
'text': info.year
}) : null
),
self.tagline = new Element('span.tagline', {
'text': info.tagline ? info.tagline : info.plot,
'title': info.tagline ? info.tagline : info.plot
}),
self.director = self.info.director ? new Element('span.director', {
'text': 'Director:' + info.director
}) : null,
self.starring = info.actors ? new Element('span.actors', {
'text': 'Starring:'
}) : null
)
)
)
)
if(info.actors){
Object.each(info.actors, function(actor){
new Element('span', {
'text': actor
}).inject(self.starring)
})
}
info.titles.each(function(title){
self.alternativeTitle({
@@ -319,12 +309,9 @@ Block.Search.Item = new Class({
}
self.options_el.grab(
new Element('div').adopt(
self.thumbnail = (info.images && info.images.poster.length > 0) ? new Element('img.thumbnail', {
'src': info.images.poster[0],
'height': null,
'width': null
}) : null,
new Element('div', {
'class': self.info.in_wanted && self.info.in_wanted.profile || in_library ? 'in_library_wanted' : ''
}).adopt(
self.info.in_wanted && self.info.in_wanted.profile ? new Element('span.in_wanted', {
'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label
}) : (in_library ? new Element('span.in_library', {

View File

@@ -5,7 +5,7 @@ from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParams, getParam
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Profile, ProfileType
from couchpotato.core.settings.model import Profile, ProfileType, Movie
log = CPLog(__name__)
@@ -30,6 +30,21 @@ class ProfilePlugin(Plugin):
})
addEvent('app.initialize', self.fill, priority = 90)
addEvent('app.load', self.forceDefaults)
def forceDefaults(self):
# Get all active movies without profile
active_status = fireEvent('status.get', 'active', single = True)
db = get_session()
movies = db.query(Movie).filter(Movie.status_id == active_status.get('id'), Movie.profile == None).all()
if len(movies) > 0:
default_profile = self.default()
for movie in movies:
movie.profile_id = default_profile.get('id')
db.commit()
def allView(self):
@@ -129,6 +144,9 @@ class ProfilePlugin(Plugin):
db.delete(p)
db.commit()
# Force defaults on all empty profile movies
self.forceDefaults()
success = True
except Exception, e:
message = log.error('Failed deleting Profile: %s', e)

View File

@@ -6,15 +6,25 @@
border-bottom: 1px solid rgba(255,255,255,0.2);
}
.profile { border-bottom: 1px solid rgba(255,255,255,0.2) }
.profile {
border-bottom: 1px solid rgba(255,255,255,0.2);
position: relative;
}
.profile > .delete {
height: 20px;
width: 20px;
position: absolute;
margin-left: 690px;
padding: 14px;
padding: 25px 20px;
background-position: center;
right: 0;
cursor: pointer;
opacity: 0.6;
}
.profile > .delete:hover {
opacity: 1;
}
.profile .ctrlHolder:hover {
background: none;
}
.profile .qualities {
@@ -34,7 +44,8 @@
.profile .wait_for {
position: absolute;
margin: -45px 0 0 437px;
right: 60px;
top: 0;
}
.profile .wait_for input {
@@ -61,6 +72,10 @@
margin-right: 10px;
}
.profile .type .check {
margin-top: -1px;
}
.profile .quality_type select {
width: 186px;
margin-left: -1px;
@@ -71,13 +86,13 @@
}
.profile .types .type .handle {
background: url('./handle.png') center;
background: url('../../static/profile_plugin/handle.png') center;
display: inline-block;
height: 20px;
width: 20px;
cursor: grab;
cursor: -moz-grab;
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
margin: 0;
}
@@ -105,9 +120,9 @@
}
#profile_ordering li {
cursor: grab;
cursor: -moz-grab;
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
border-bottom: 1px solid rgba(255,255,255,0.2);
padding: 0 5px;
}
@@ -126,7 +141,7 @@
}
#profile_ordering li .handle {
background: url('./handle.png') center;
background: url('../../static/profile_plugin/handle.png') center;
width: 20px;
float: right;
}

View File

@@ -19,12 +19,12 @@ class QualityPlugin(Plugin):
qualities = [
{'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]},
{'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
{'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
{'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
{'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi']},
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']},
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},

View File

@@ -41,12 +41,15 @@ class Release(Plugin):
addEvent('release.clean', self.clean)
def add(self, group):
db = get_session()
identifier = '%s.%s.%s' % (group['library']['identifier'], group['meta_data'].get('audio', 'unknown'), group['meta_data']['quality']['identifier'])
done_status, snatched_status = fireEvent('status.get', ['done', 'snatched'], single = True)
# Add movie
done_status = fireEvent('status.get', 'done', single = True)
movie = db.query(Movie).filter_by(library_id = group['library'].get('id')).first()
if not movie:
movie = Movie(
@@ -58,7 +61,6 @@ class Release(Plugin):
db.commit()
# Add Release
snatched_status = fireEvent('status.get', 'snatched', single = True)
rel = db.query(Relea).filter(
or_(
Relea.identifier == identifier,
@@ -76,15 +78,19 @@ class Release(Plugin):
db.commit()
# Add each file type
added_files = []
for type in group['files']:
for cur_file in group['files'][type]:
added_file = self.saveFile(cur_file, type = type, include_media_info = type is 'movie')
try:
added_file = db.query(File).filter_by(id = added_file.get('id')).one()
rel.files.append(added_file)
db.commit()
except Exception, e:
log.debug('Failed to attach "%s" to release: %s', (cur_file, e))
added_files.append(added_file.get('id'))
# Add the release files in batch
try:
added_files = db.query(File).filter(or_(*[File.id == x for x in added_files])).all()
rel.files.extend(added_files)
db.commit()
except Exception, e:
log.debug('Failed to attach "%s" to release: %s', (cur_file, e))
fireEvent('movie.restatus', movie.id)
@@ -147,8 +153,7 @@ class Release(Plugin):
rel = db.query(Relea).filter_by(id = id).first()
if rel:
ignored_status = fireEvent('status.get', 'ignored', single = True)
available_status = fireEvent('status.get', 'available', single = True)
ignored_status, available_status = fireEvent('status.get', ['ignored', 'available'], single = True)
rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id')
db.commit()
@@ -160,7 +165,8 @@ class Release(Plugin):
db = get_session()
id = getParam('id')
status_snatched = fireEvent('status.add', 'snatched', single = True)
snatched_status, done_status = fireEvent('status.get', ['snatched', 'done'], single = True)
rel = db.query(Relea).filter_by(id = id).first()
if rel:
@@ -168,6 +174,8 @@ class Release(Plugin):
for info in rel.info:
item[info.identifier] = info.value
fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Snatching "%s"' % item['name'])
# Get matching provider
provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True)
@@ -182,8 +190,14 @@ class Release(Plugin):
}), manual = True, single = True)
if success:
rel.status_id = status_snatched.get('id')
db.commit()
db.expunge_all()
rel = db.query(Relea).filter_by(id = id).first() # Get release again
if rel.status_id != done_status.get('id'):
rel.status_id = snatched_status.get('id')
db.commit()
fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Successfully snatched "%s"' % item['name'])
return jsonified({
'success': success

View File

@@ -13,7 +13,7 @@ rename_options = {
'thename': 'The Moviename',
'year': 'Year (2011)',
'first': 'First letter (M)',
'quality': 'Quality (720P)',
'quality': 'Quality (720p)',
'video': 'Video (x264)',
'audio': 'Audio (DTS)',
'group': 'Releasegroup name',
@@ -112,6 +112,15 @@ config = [{
'label': 'Separator',
'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.',
},
{
'name': 'file_action',
'label': 'Torrent File Action',
'default': 'move',
'type': 'dropdown',
'values': [('Move', 'move'), ('Copy', 'copy'), ('Hard link', 'hardlink'), ('Sym link', 'symlink'), ('Move & Sym link', 'move_symlink')],
'description': 'Define which kind of file operation you want to use for torrents. Before you start using <a href="http://en.wikipedia.org/wiki/Hard_link">hard links</a> or <a href="http://en.wikipedia.org/wiki/Sym_link">sym links</a>, PLEASE read about their possible drawbacks.',
'advanced': True,
},
{
'advanced': True,
'name': 'ntfs_permission',

View File

@@ -2,12 +2,13 @@ from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import toUnicode, ss
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.helpers.request import getParams, jsonified
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
getImdb
getImdb, link, symlink
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library, File, Profile, Release
from couchpotato.core.settings.model import Library, File, Profile, Release, \
ReleaseInfo
from couchpotato.environment import Env
import errno
import os
@@ -18,7 +19,6 @@ import traceback
log = CPLog(__name__)
class Renamer(Plugin):
renaming_started = False
@@ -27,7 +27,12 @@ class Renamer(Plugin):
def __init__(self):
addApiView('renamer.scan', self.scanView, docs = {
'desc': 'For the renamer to check for new files to rename',
'desc': 'For the renamer to check for new files to rename in a folder',
'params': {
'movie_folder': {'desc': 'Optional: The folder of the movie to scan. Keep empty for default renamer folder.'},
'downloader' : {'desc': 'Optional: The downloader this movie has been downloaded with'},
'download_id': {'desc': 'Optional: The downloader\'s nzb/torrent ID'},
},
})
addEvent('renamer.scan', self.scan)
@@ -35,22 +40,42 @@ class Renamer(Plugin):
addEvent('app.load', self.scan)
addEvent('app.load', self.checkSnatched)
addEvent('app.load', self.setCrons)
if self.conf('run_every') > 0:
fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'))
# Enable / disable interval
addEvent('setting.save.renamer.enabled.after', self.setCrons)
addEvent('setting.save.renamer.run_every.after', self.setCrons)
addEvent('setting.save.renamer.force_every.after', self.setCrons)
if self.conf('force_every') > 0:
fireEvent('schedule.interval', 'renamer.check_snatched_forced', self.scan, hours = self.conf('force_every'))
def setCrons(self):
fireEvent('schedule.remove', 'renamer.check_snatched')
if self.isEnabled() and self.conf('run_every') > 0:
fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'), single = True)
fireEvent('schedule.remove', 'renamer.check_snatched_forced')
if self.isEnabled() and self.conf('force_every') > 0:
fireEvent('schedule.interval', 'renamer.check_snatched_forced', self.scan, hours = self.conf('force_every'), single = True)
return True
def scanView(self):
fireEventAsync('renamer.scan')
params = getParams()
movie_folder = params.get('movie_folder', None)
downloader = params.get('downloader', None)
download_id = params.get('download_id', None)
fireEventAsync('renamer.scan',
movie_folder = movie_folder,
download_info = {'id': download_id, 'downloader': downloader} if download_id else None
)
return jsonified({
'success': True
})
def scan(self):
def scan(self, movie_folder = None, download_info = None):
if self.isDisabled():
return
@@ -60,17 +85,42 @@ class Renamer(Plugin):
return
# Check to see if the "to" folder is inside the "from" folder.
if not os.path.isdir(self.conf('from')) or not os.path.isdir(self.conf('to')):
log.debug('"To" and "From" have to exist.')
if movie_folder and not os.path.isdir(movie_folder) or not os.path.isdir(self.conf('from')) or not os.path.isdir(self.conf('to')):
l = log.debug if movie_folder else log.error
l('Both the "To" and "From" have to exist.')
return
elif self.conf('from') in self.conf('to'):
log.error('The "to" can\'t be inside of the "from" folder. You\'ll get an infinite loop.')
return
groups = fireEvent('scanner.scan', folder = self.conf('from'), single = True)
elif (movie_folder and movie_folder in [self.conf('to'), self.conf('from')]):
log.error('The "to" and "from" folders can\'t be inside of or the same as the provided movie folder.')
return
self.renaming_started = True
# make sure the movie folder name is included in the search
folder = None
files = []
if movie_folder:
log.info('Scanning movie folder %s...', movie_folder)
movie_folder = movie_folder.rstrip(os.path.sep)
folder = os.path.dirname(movie_folder)
# Get all files from the specified folder
try:
for root, folders, names in os.walk(movie_folder):
files.extend([os.path.join(root, name) for name in names])
except:
log.error('Failed getting files from %s: %s', (movie_folder, traceback.format_exc()))
db = get_session()
# Extend the download info with info stored in the downloaded release
download_info = self.extendDownloadInfo(download_info)
groups = fireEvent('scanner.scan', folder = folder if folder else self.conf('from'),
files = files, download_info = download_info, return_ignored = False, single = True)
destination = self.conf('to')
folder_name = self.conf('folder_name')
file_name = self.conf('file_name')
@@ -79,12 +129,8 @@ class Renamer(Plugin):
separator = self.conf('separator')
# Statusses
done_status = fireEvent('status.get', 'done', single = True)
active_status = fireEvent('status.get', 'active', single = True)
downloaded_status = fireEvent('status.get', 'downloaded', single = True)
snatched_status = fireEvent('status.get', 'snatched', single = True)
db = get_session()
done_status, active_status, downloaded_status, snatched_status = \
fireEvent('status.get', ['done', 'active', 'downloaded', 'snatched'], single = True)
for group_identifier in groups:
@@ -304,7 +350,7 @@ class Renamer(Plugin):
else:
log.info('Better quality release already exists for %s, with quality %s', (movie.library.titles[0].title, release.quality.label))
# Add _EXISTS_ to the parent dir
# Add exists tag to the .ignore file
self.tagDir(group, 'exists')
# Notify on rename fail
@@ -325,7 +371,8 @@ class Renamer(Plugin):
db.commit()
# Remove leftover files
if self.conf('cleanup') and not self.conf('move_leftover') and remove_leftovers:
if self.conf('cleanup') and not self.conf('move_leftover') and remove_leftovers and \
not (self.conf('file_action') != 'move' and self.downloadIsTorrent(download_info)):
log.debug('Removing leftover files')
for current_file in group['files']['leftover']:
remove_files.append(current_file)
@@ -350,7 +397,7 @@ class Renamer(Plugin):
os.remove(src)
parent_dir = os.path.normpath(os.path.dirname(src))
if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and destination != parent_dir:
if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and not parent_dir in [destination, movie_folder] and not self.conf('from') in parent_dir:
delete_folders.append(parent_dir)
except:
@@ -375,12 +422,15 @@ class Renamer(Plugin):
self.makeDir(os.path.dirname(dst))
try:
self.moveFile(src, dst)
self.moveFile(src, dst, forcemove = not self.downloadIsTorrent(download_info))
group['renamed_files'].append(dst)
except:
log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc()))
self.tagDir(group, 'failed_rename')
if self.conf('file_action') != 'move' and self.downloadIsTorrent(download_info):
self.tagDir(group, 'renamed already')
# Remove matching releases
for release in remove_releases:
log.debug('Removing release %s', release.identifier)
@@ -426,36 +476,40 @@ class Renamer(Plugin):
return rename_files
# This adds a file to ignore / tag a release so it is ignored later
def tagDir(self, group, tag):
rename_files = {}
ignore_file = None
for movie_file in sorted(list(group['files']['movie'])):
ignore_file = '%s.ignore' % os.path.splitext(movie_file)[0]
break
if group['dirname']:
rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_%s_%s' % (tag.upper(), group['dirname']))
else: # Add it to filename
for file_type in group['files']:
for rename_me in group['files'][file_type]:
filename = os.path.basename(rename_me)
rename_files[rename_me] = rename_me.replace(filename, '_%s_%s' % (tag.upper(), filename))
text = """This file is from CouchPotato
It has marked this release as "%s"
This file hides the release from the renamer
Remove it if you want it to be renamed (again, or at least let it try again)
""" % tag
for src in rename_files:
if rename_files[src]:
dst = rename_files[src]
log.info('Renaming "%s" to "%s"', (src, dst))
if ignore_file:
self.createFile(ignore_file, text)
# Create dir
self.makeDir(os.path.dirname(dst))
try:
self.moveFile(src, dst)
except:
log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc()))
raise
def moveFile(self, old, dest):
def moveFile(self, old, dest, forcemove = False):
dest = ss(dest)
try:
shutil.move(old, dest)
if forcemove:
shutil.move(old, dest)
elif self.conf('file_action') == 'hardlink':
link(old, dest)
elif self.conf('file_action') == 'symlink':
symlink(old, dest)
elif self.conf('file_action') == 'copy':
shutil.copy(old, dest)
elif self.conf('file_action') == 'move_symlink':
shutil.move(old, dest)
symlink(dest, old)
else:
shutil.move(old, dest)
try:
os.chmod(dest, Env.getPermission('file'))
@@ -524,16 +578,14 @@ class Renamer(Plugin):
loge('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc()))
def checkSnatched(self):
if self.checking_snatched:
log.debug('Already checking snatched')
self.checking_snatched = True
snatched_status = fireEvent('status.get', 'snatched', single = True)
ignored_status = fireEvent('status.get', 'ignored', single = True)
failed_status = fireEvent('status.get', 'failed', single = True)
done_status = fireEvent('status.get', 'done', single = True)
snatched_status, ignored_status, failed_status, done_status = \
fireEvent('status.get', ['snatched', 'ignored', 'failed', 'done'], single = True)
db = get_session()
rels = db.query(Release).filter_by(status_id = snatched_status.get('id')).all()
@@ -571,8 +623,16 @@ class Renamer(Plugin):
found = False
for item in statuses:
if item['name'] == nzbname or rel_dict['info']['name'] in item['name'] or getImdb(item['name']) == movie_dict['library']['identifier']:
found_release = False
if rel_dict['info'].get('download_id'):
if item['id'] == rel_dict['info']['download_id'] and item['downloader'] == rel_dict['info']['download_downloader']:
log.debug('Found release by id: %s', item['id'])
found_release = True
else:
if item['name'] == nzbname or rel_dict['info']['name'] in item['name'] or getImdb(item['name']) == movie_dict['library']['identifier']:
found_release = True
if found_release:
timeleft = 'N/A' if item['timeleft'] == -1 else item['timeleft']
log.debug('Found %s: %s, time to go: %s', (item['name'], item['status'].upper(), timeleft))
@@ -580,16 +640,18 @@ class Renamer(Plugin):
pass
elif item['status'] == 'failed':
fireEvent('download.remove_failed', item, single = True)
rel.status_id = failed_status.get('id')
rel.last_edit = int(time.time())
db.commit()
if self.conf('next_on_failed'):
fireEvent('searcher.try_next_release', movie_id = rel.movie_id)
else:
rel.status_id = failed_status.get('id')
rel.last_edit = int(time.time())
db.commit()
elif item['status'] == 'completed':
log.info('Download of %s completed!', item['name'])
scan_required = True
if item['id'] and item['downloader'] and item['folder']:
fireEventAsync('renamer.scan', movie_folder = item['folder'], download_info = item)
else:
scan_required = True
found = True
break
@@ -606,3 +668,38 @@ class Renamer(Plugin):
self.checking_snatched = False
return True
def extendDownloadInfo(self, download_info):
rls = None
if download_info and download_info.get('id') and download_info.get('downloader'):
db = get_session()
rlsnfo_dwnlds = db.query(ReleaseInfo).filter_by(identifier = 'download_downloader', value = download_info.get('downloader')).all()
rlsnfo_ids = db.query(ReleaseInfo).filter_by(identifier = 'download_id', value = download_info.get('id')).all()
for rlsnfo_dwnld in rlsnfo_dwnlds:
for rlsnfo_id in rlsnfo_ids:
if rlsnfo_id.release == rlsnfo_dwnld.release:
rls = rlsnfo_id.release
break
if rls: break
if not rls:
log.error('Download ID %s from downloader %s not found in releases', (download_info.get('id'), download_info.get('downloader')))
if rls:
rls_dict = rls.to_dict({'info':{}})
download_info.update({
'imdb_id': rls.movie.library.identifier,
'quality': rls.quality.identifier,
'type': rls_dict.get('info', {}).get('type')
})
return download_info
def downloadIsTorrent(self, download_info):
return download_info and download_info.get('type') in ['torrent', 'torrent_magnet']

View File

@@ -11,6 +11,7 @@ from subliminal.videos import Video
import enzyme
import os
import re
import threading
import time
import traceback
@@ -74,7 +75,7 @@ class Scanner(Plugin):
'hdtv': ['hdtv']
}
clean = '[ _\,\.\(\)\[\]\-](french|swedisch|danish|dutch|swesub|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|video_ts|audio_ts|480p|480i|576p|576i|720p|720i|1080p|1080i|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|cd[1-9]|\[.*\])([ _\,\.\(\)\[\]\-]|$)'
clean = '[ _\,\.\(\)\[\]\-](extended.cut|directors.cut|french|swedisch|danish|dutch|swesub|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|video_ts|audio_ts|480p|480i|576p|576i|720p|720i|1080p|1080i|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|cd[1-9]|\[.*\])([ _\,\.\(\)\[\]\-]|$)'
multipart_regex = [
'[ _\.-]+cd[ _\.-]*([0-9a-d]+)', #*cd1
'[ _\.-]+dvd[ _\.-]*([0-9a-d]+)', #*dvd1
@@ -100,7 +101,7 @@ class Scanner(Plugin):
addEvent('scanner.name_year', self.getReleaseNameYear)
addEvent('scanner.partnumber', self.getPartNumber)
def scan(self, folder = None, files = None, simple = False, newer_than = 0, on_found = None):
def scan(self, folder = None, files = None, download_info = None, simple = False, newer_than = 0, return_ignored = True, on_found = None):
folder = ss(os.path.normpath(folder))
@@ -118,8 +119,7 @@ class Scanner(Plugin):
try:
files = []
for root, dirs, walk_files in os.walk(folder):
for filename in walk_files:
files.append(os.path.join(root, filename))
files.extend(os.path.join(root, filename) for filename in walk_files)
except:
log.error('Failed getting files from %s: %s', (folder, traceback.format_exc()))
else:
@@ -177,17 +177,25 @@ class Scanner(Plugin):
# Group files minus extension
ignored_identifiers = []
for identifier, group in movie_files.iteritems():
if identifier not in group['identifiers'] and len(identifier) > 0: group['identifiers'].append(identifier)
log.debug('Grouping files: %s', identifier)
has_ignored = 0
for file_path in group['unsorted_files']:
wo_ext = file_path[:-(len(getExt(file_path)) + 1)]
ext = getExt(file_path)
wo_ext = file_path[:-(len(ext) + 1)]
found_files = set([i for i in leftovers if wo_ext in i])
group['unsorted_files'].extend(found_files)
leftovers = leftovers - found_files
has_ignored += 1 if ext == 'ignore' else 0
if has_ignored > 0:
ignored_identifiers.append(identifier)
# Break if CP wants to shut down
if self.shuttingDown():
break
@@ -313,6 +321,11 @@ class Scanner(Plugin):
del movie_files
# Make sure only one movie was found if a download ID is provided
if download_info and not len(valid_files) == 1:
log.info('Download ID provided (%s), but more than one group found (%s). Ignoring Download ID...', (download_info.get('imdb_id'), len(valid_files)))
download_info = None
# Determine file types
processed_movies = {}
total_found = len(valid_files)
@@ -322,15 +335,17 @@ class Scanner(Plugin):
except:
break
if return_ignored is False and identifier in ignored_identifiers:
log.debug('Ignore file found, ignoring release: %s' % identifier)
continue
# Group extra (and easy) files first
# images = self.getImages(group['unsorted_files'])
group['files'] = {
'movie_extra': self.getMovieExtras(group['unsorted_files']),
'subtitle': self.getSubtitles(group['unsorted_files']),
'subtitle_extra': self.getSubtitlesExtras(group['unsorted_files']),
'nfo': self.getNfo(group['unsorted_files']),
'trailer': self.getTrailers(group['unsorted_files']),
#'backdrop': images['backdrop'],
'leftover': set(group['unsorted_files']),
}
@@ -345,7 +360,7 @@ class Scanner(Plugin):
continue
log.debug('Getting metadata for %s', identifier)
group['meta_data'] = self.getMetaData(group, folder = folder)
group['meta_data'] = self.getMetaData(group, folder = folder, download_info = download_info)
# Subtitle meta
group['subtitle_language'] = self.getSubtitleLanguage(group) if not simple else {}
@@ -375,7 +390,7 @@ class Scanner(Plugin):
del group['unsorted_files']
# Determine movie
group['library'] = self.determineMovie(group)
group['library'] = self.determineMovie(group, download_info = download_info)
if not group['library']:
log.error('Unable to determine movie: %s', group['identifiers'])
else:
@@ -388,6 +403,11 @@ class Scanner(Plugin):
if on_found:
on_found(group, total_found, total_found - len(processed_movies))
# Wait for all the async events calm down a bit
while threading.activeCount() > 100 and not self.shuttingDown():
log.debug('Too many threads active, waiting a few seconds')
time.sleep(10)
if len(processed_movies) > 0:
log.info('Found %s movies in the folder %s', (len(processed_movies), folder))
else:
@@ -395,7 +415,7 @@ class Scanner(Plugin):
return processed_movies
def getMetaData(self, group, folder = ''):
def getMetaData(self, group, folder = '', download_info = None):
data = {}
files = list(group['files']['movie'])
@@ -417,9 +437,13 @@ class Scanner(Plugin):
if data.get('audio'): break
# Use the quality guess first, if that failes use the quality we wanted to download
data['quality'] = fireEvent('quality.guess', files = files, extra = data, single = True)
if not data['quality']:
data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True)
if download_info and download_info.get('quality'):
data['quality'] = fireEvent('quality.single', download_info.get('quality'), single = True)
else:
data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True)
data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 or data['quality'].get('hd') else 'SD'
@@ -495,17 +519,22 @@ class Scanner(Plugin):
return detected_languages
def determineMovie(self, group):
imdb_id = None
def determineMovie(self, group, download_info = None):
# Get imdb id from downloader
imdb_id = download_info and download_info.get('imdb_id')
if imdb_id:
log.debug('Found movie via imdb id from it\'s download id: %s', download_info.get('imdb_id'))
files = group['files']
# Check for CP(imdb_id) string in the file paths
for cur_file in files['movie']:
imdb_id = self.getCPImdb(cur_file)
if imdb_id:
log.debug('Found movie via CP tag: %s', cur_file)
break
if not imdb_id:
for cur_file in files['movie']:
imdb_id = self.getCPImdb(cur_file)
if imdb_id:
log.debug('Found movie via CP tag: %s', cur_file)
break
# Check and see if nfo contains the imdb-id
if not imdb_id:

View File

@@ -116,13 +116,13 @@ def sizeScore(size):
def providerScore(provider):
if provider in ['OMGWTFNZBs', 'PassThePopcorn', 'SceneAccess', 'TorrentLeech']:
return 20
if provider in ['Newznab']:
return 10
try:
score = tryInt(Env.setting('extra_score', section = provider.lower(), default = 0))
except:
score = 0
return 0
return score
def duplicateScore(nzb_name, movie_name):

View File

@@ -25,12 +25,13 @@ config = [{
'label': 'Required words',
'default': '',
'placeholder': 'Example: DTS, AC3 & English',
'description': 'Ignore releases that don\'t contain at least one set of words. Sets are separated by "," and each word within a set must be separated with "&"'
'description': 'A release should contain at least one set of words. Sets are separated by "," and each word within a set must be separated with "&"'
},
{
'name': 'ignored_words',
'label': 'Ignored words',
'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs',
'description': 'Ignores releases that match any of these sets. (Works like explained above)'
},
{
'name': 'preferred_method',
@@ -40,6 +41,14 @@ config = [{
'type': 'dropdown',
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrents', 'torrent')],
},
{
'name': 'always_search',
'default': False,
'advanced': True,
'type': 'bool',
'label': 'Always search',
'description': 'Search for movies even before there is a ETA. Enabling this will probably get you a lot of fakes.',
},
],
}, {
'tab': 'searcher',

View File

@@ -50,7 +50,12 @@ class Searcher(Plugin):
}"""},
})
# Schedule cronjob
addEvent('app.load', self.setCrons)
addEvent('setting.save.searcher.cron_day.after', self.setCrons)
addEvent('setting.save.searcher.cron_hour.after', self.setCrons)
addEvent('setting.save.searcher.cron_minute.after', self.setCrons)
def setCrons(self):
fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
def allMoviesView(self):
@@ -141,8 +146,7 @@ class Searcher(Plugin):
pre_releases = fireEvent('quality.pre_releases', single = True)
release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True)
available_status = fireEvent('status.get', 'available', single = True)
ignored_status = fireEvent('status.get', 'ignored', single = True)
available_status, ignored_status = fireEvent('status.get', ['available', 'ignored'], single = True)
found_releases = []
@@ -157,7 +161,7 @@ class Searcher(Plugin):
ret = False
for quality_type in movie['profile']['types']:
if not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates):
if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates):
log.info('Too early to search for %s, %s', (quality_type['quality']['identifier'], default_title))
continue
@@ -165,7 +169,7 @@ class Searcher(Plugin):
# See if better quality is available
for release in movie['releases']:
if release['quality']['order'] < quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id')]:
if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id')]:
has_better_quality += 1
# Don't search for quality lower then already available.
@@ -285,10 +289,10 @@ class Searcher(Plugin):
if filedata == 'try_next':
return filedata
successful = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
if successful:
download_result = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
log.debug('Downloader result: %s', download_result)
if download_result:
try:
# Mark release as snatched
db = get_session()
@@ -298,6 +302,15 @@ class Searcher(Plugin):
done_status = fireEvent('status.get', 'done', single = True)
rls.status_id = done_status.get('id') if not renamer_enabled else snatched_status.get('id')
# Save download-id info if returned
if isinstance(download_result, dict):
for key in download_result:
rls_info = ReleaseInfo(
identifier = 'download_%s' % key,
value = toUnicode(download_result.get(key))
)
rls.info.append(rls_info)
db.commit()
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
@@ -333,7 +346,7 @@ class Searcher(Plugin):
return True
log.info('Tried to download, but none of the "%s" downloaders are enabled', (data.get('type', '')))
log.info('Tried to download, but none of the "%s" downloaders are enabled or gave an error', (data.get('type', '')))
return False
@@ -357,7 +370,7 @@ class Searcher(Plugin):
return search_types
def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs):
def correctMovie(self, nzb = None, movie = None, quality = None, **kwargs):
imdb_results = kwargs.get('imdb_results', False)
retention = Env.setting('retention', section = 'nzb')
@@ -370,8 +383,9 @@ class Searcher(Plugin):
movie_words = re.split('\W+', simplifyString(movie_name))
nzb_name = simplifyString(nzb['name'])
nzb_words = re.split('\W+', nzb_name)
required_words = splitString(self.conf('required_words').lower())
# Make sure it has required words
required_words = splitString(self.conf('required_words').lower())
req_match = 0
for req_set in required_words:
req = splitString(req_set, '&')
@@ -381,19 +395,24 @@ class Searcher(Plugin):
log.info2("Wrong: Required word missing: %s" % nzb['name'])
return False
# Ignore releases
ignored_words = splitString(self.conf('ignored_words').lower())
blacklisted = list(set(nzb_words) & set(ignored_words) - set(movie_words))
if self.conf('ignored_words') and blacklisted:
log.info2("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted)))
ignored_match = 0
for ignored_set in ignored_words:
ignored = splitString(ignored_set, '&')
ignored_match += len(list(set(nzb_words) & set(ignored))) == len(ignored)
if self.conf('ignored_words') and ignored_match:
log.info2("Wrong: '%s' contains 'ignored words'" % (nzb['name']))
return False
# Ignore porn stuff
pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic']
pron_words = list(set(nzb_words) & set(pron_tags) - set(movie_words))
if pron_words:
log.info('Wrong: %s, probably pr0n', (nzb['name']))
return False
#qualities = fireEvent('quality.all', single = True)
preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True)
# Contains lower quality string

View File

@@ -25,13 +25,14 @@ class StatusPlugin(Plugin):
'available': 'Available',
'suggest': 'Suggest',
}
status_cached = {}
def __init__(self):
addEvent('status.add', self.add)
addEvent('status.get', self.add) # Alias for .add
addEvent('status.get', self.get)
addEvent('status.get_by_id', self.getById)
addEvent('status.all', self.all)
addEvent('app.initialize', self.fill)
addEvent('app.load', self.all) # Cache all statuses
addApiView('status.list', self.list, docs = {
'desc': 'Check for available update',
@@ -67,26 +68,40 @@ class StatusPlugin(Plugin):
s = status.to_dict()
temp.append(s)
#db.close()
# Update cache
self.status_cached[status.identifier] = s
return temp
def add(self, identifier):
def get(self, identifiers):
if not isinstance(identifiers, (list)):
identifiers = [identifiers]
db = get_session()
return_list = []
s = db.query(Status).filter_by(identifier = identifier).first()
if not s:
s = Status(
identifier = identifier,
label = toUnicode(identifier.capitalize())
)
db.add(s)
db.commit()
for identifier in identifiers:
status_dict = s.to_dict()
if self.status_cached.get(identifier):
return_list.append(self.status_cached.get(identifier))
continue
#db.close()
return status_dict
s = db.query(Status).filter_by(identifier = identifier).first()
if not s:
s = Status(
identifier = identifier,
label = toUnicode(identifier.capitalize())
)
db.add(s)
db.commit()
status_dict = s.to_dict()
self.status_cached[identifier] = status_dict
return_list.append(status_dict)
return return_list if len(identifiers) > 1 else return_list[0]
def fill(self):

View File

@@ -20,7 +20,7 @@ config = [{
},
{
'name': 'languages',
'description': 'Comma separated, 2 letter country code. Example: en, nl',
'description': 'Comma separated, 2 letter country code. Example: en, nl. See the codes at <a href="http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes">on Wikipedia</a>',
},
# {
# 'name': 'automatic',

View File

@@ -22,14 +22,14 @@ config = [{
'name': 'quality',
'default': '720p',
'type': 'dropdown',
'values': [('1080P', '1080p'), ('720P', '720p'), ('480P', '480p')],
'values': [('1080p', '1080p'), ('720p', '720p'), ('480P', '480p')],
},
{
'name': 'name',
'label': 'Naming',
'default': '<filename>-trailer',
'advanced': True,
'description': 'Use <filename> to use above settings.'
'description': 'Use <strong>&lt;filename&gt;</strong> to use above settings.'
},
],
},

View File

@@ -22,10 +22,15 @@ class Trailer(Plugin):
return False
for trailer in trailers.get(self.conf('quality'), []):
filename = self.conf('name').replace('<filename>', group['filename']) + ('.%s' % getExt(trailer))
ext = getExt(trailer)
filename = self.conf('name').replace('<filename>', group['filename']) + ('.%s' % ('mp4' if len(ext) > 5 else ext))
destination = os.path.join(group['destination_dir'], filename)
if not os.path.isfile(destination):
fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True)
trailer_file = fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True)
if os.path.getsize(trailer_file) < (1024 * 1024): # Don't trust small trailers (1MB), try next one
os.unlink(trailer_file)
continue
else:
log.debug('Trailer already exists: %s', destination)

View File

@@ -5,6 +5,7 @@
bottom: 0;
left: 0;
right: 0;
padding: 0;
}
.page.userscript .frame.loading {
@@ -12,3 +13,26 @@
font-size: 20px;
padding: 20px;
}
.page.userscript .movie_result {
height: 140px;
}
.page.userscript .movie_result .thumbnail {
width: 90px;
}
.page.userscript .movie_result .options {
left: 90px;
padding: 54px 15px;
}
.page.userscript .movie_result .year {
display: none;
}
.page.userscript .movie_result .options select[name="title"] {
width: 190px;
}
.page.userscript .movie_result .options select[name="profile"] {
width: 70px;
}

View File

@@ -63,28 +63,19 @@ var UserscriptSettingTab = new Class({
self.settings = App.getPage('Settings')
self.settings.addEvent('create', function(){
// See if userscript can be installed
var userscript = false;
try {
if(Components.interfaces.gmIGreasemonkeyService)
userscript = true
}
catch(e){
userscript = Browser.chrome === true;
}
var host_url = window.location.protocol + '//' + window.location.host;
self.settings.createGroup({
'name': 'userscript',
'label': 'Install the bookmarklet' + (userscript ? ' or userscript' : ''),
'label': 'Install the bookmarklet or userscript',
'description': 'Easily add movies via imdb.com, appletrailers and more'
}).inject(self.settings.tabs.automation.content, 'top').adopt(
(userscript ? [new Element('a.userscript.button', {
new Element('a.userscript.button', {
'text': 'Install userscript',
'href': Api.createUrl('userscript.get')+randomString()+'/couchpotato.user.js',
'target': '_self'
}), new Element('span.or[text=or]')] : null),
'target': '_blank'
}),
new Element('span.or[text=or]'),
new Element('span.bookmarklet').adopt(
new Element('a.button.green', {
'text': '+CouchPotato',

View File

@@ -1,4 +1,9 @@
// ==UserScript==
//
// If you can read this, you need to enable or install the Greasemonkey add-on for firefox
// If you are using Chrome, download this file and drag it to the extensions tab
// Other browsers, use the bookmarklet
//
// @name CouchPotato UserScript
// @description Add movies like a real CouchPotato
// @grant none

View File

@@ -1,49 +1,60 @@
.page.wizard .uniForm {
width: 80%;
margin: 0 auto 30px;
margin: 0 0 30px;
width: 83%;
}
.page.wizard h1 {
padding: 10px 30px;
margin: 0;
padding: 10px 0;
margin: 0 5px;
display: block;
font-size: 30px;
margin-top: 80px;
}
.page.wizard .description {
padding: 10px 30px;
font-size: 18px;
padding: 10px 5px;
font-size: 1.45em;
line-height: 1.4em;
display: block;
}
.page.wizard .tab_wrapper {
background: #5c697b;
padding: 10px 0;
font-size: 18px;
height: 65px;
font-size: 1.75em;
position: fixed;
top: 0;
margin: 0;
width: 100%;
min-width: 960px;
left: 0;
z-index: 2;
box-shadow: 0 0 50px rgba(0,0,0,0.55);
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
.page.wizard .tab_wrapper .tabs {
text-align: center;
padding: 0;
margin: 0;
margin: 0 auto;
display: block;
height: 100%;
width: 100%;
max-width: 960px;
}
.page.wizard .tabs li {
display: inline-block;
height: 100%;
}
.page.wizard .tabs li a {
padding: 20px 10px;
height: 100%;
display: block;
color: #FFF;
font-weight: normal;
border-bottom: 4px solid transparent;
}
.page.wizard .tabs li:hover a { border-color: #047792; }
.page.wizard .tabs li.done a { border-color: #04bce6; }
.page.wizard .tab_wrapper .pointer {
border-right: 10px solid transparent;
@@ -61,27 +72,13 @@
.page.wizard form > div {
min-height: 300px;
}
.page.wizard .wgroup_finish {
height: 300px;
}
.page.wizard .wgroup_finish h1 {
text-align: center;
}
.page.wizard .wgroup_finish .wizard_support,
.page.wizard .wgroup_finish .description {
font-size: 25px;
line-height: 120%;
margin: 20px 0;
text-align: center;
}
.page.wizard .button.green {
padding: 20px;
font-size: 25px;
margin: 10px 30px 80px;
display: block;
text-align: center;
}
.page.wizard .button.green {
padding: 20px;
font-size: 25px;
margin: 10px 0 80px;
display: block;
}
.page.wizard .tab_nzb_providers {
margin: 20px 0 0 0;

View File

@@ -9,27 +9,12 @@ Page.Wizard = new Class({
headers: {
'welcome': {
'title': 'Welcome to the new CouchPotato',
'description': 'To get started, fill in each of the following settings as much as you can. <br />Maybe first start with importing your movies from the previous CouchPotato',
'description': 'To get started, fill in each of the following settings as much as you can.',
'content': new Element('div', {
'styles': {
'margin': '0 0 0 30px'
}
}).adopt(
new Element('div', {
'html': 'Select the <strong>data.db</strong>. It should be in your CouchPotato root directory.'
}),
self.import_iframe = new Element('iframe', {
'styles': {
'height': 40,
'width': 300,
'border': 0,
'overflow': 'hidden'
}
})
),
'event': function(){
self.import_iframe.set('src', Api.createUrl('v1.import'))
}
})
},
'general': {
'title': 'General',
@@ -178,7 +163,7 @@ Page.Wizard = new Class({
'href': App.createUrl('wizard/'+group),
'text': (self.headers[group].label || group).capitalize()
})
).inject(tabs);
).inject(tabs)
}
else
@@ -214,13 +199,7 @@ Page.Wizard = new Class({
self.el.getElement('.t_searcher').hide();
// Add pointer
new Element('.tab_wrapper').wraps(tabs).adopt(
self.pointer = new Element('.pointer', {
'tween': {
'transition': 'quint:in:out'
}
})
);
new Element('.tab_wrapper').wraps(tabs);
// Add nav
var minimum = self.el.getSize().y-window.getSize().y;
@@ -232,16 +211,18 @@ Page.Wizard = new Class({
if(!t) return;
var func = function(){
var ct = t.getCoordinates();
self.pointer.tween('left', ct.left+(ct.width/2)-(self.pointer.getWidth()/2));
// Activate all previous ones
self.groups.each(function(groups2, nr2){
var t2 = self.el.getElement('.t_'+groups2);
t2[nr2 > nr ? 'removeClass' : 'addClass' ]('done');
})
g.tween('opacity', 1);
}
if(nr == 0)
func();
var ss = new ScrollSpy( {
new ScrollSpy( {
min: function(){
var c = g.getCoordinates();
var top = c.top-(window.getSize().y/2);

View File

@@ -2,6 +2,7 @@ from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import Provider
from couchpotato.environment import Env
from couchpotato.core.helpers.variable import splitString
import time
log = CPLog(__name__)
@@ -59,7 +60,26 @@ class Automation(Provider):
type_value = movie.get(minimal_type, 0)
type_min = self.getMinimal(minimal_type)
if type_value < type_min:
log.info('%s too low for %s, need %s has %s', (minimal_type, movie['imdb'], type_min, type_value))
log.info('%s too low for %s, need %s has %s', (minimal_type, movie['original_title'], type_min, type_value))
return False
movie_genres = [genre.lower() for genre in movie['genres']]
required_genres = splitString(self.getMinimal('required_genres').lower())
ignored_genres = splitString(self.getMinimal('ignored_genres').lower())
req_match = 0
for req_set in required_genres:
req = splitString(req_set, '&')
req_match += len(list(set(movie_genres) & set(req))) == len(req)
if self.getMinimal('required_genres') and req_match == 0:
log.info2("Required genre(s) missing for %s" % movie['original_title'])
return False
for ign_set in ignored_genres:
ign = splitString(ign_set, '&')
if len(list(set(movie_genres) & set(ign))) == len(ign):
log.info2("%s has blacklisted genre(s): %s" % (movie['original_title'], ign))
return False
return True

View File

@@ -0,0 +1,34 @@
from .main import Letterboxd
def start():
return Letterboxd()
config = [{
'name': 'letterboxd',
'groups': [
{
'tab': 'automation',
'list': 'watchlist_providers',
'name': 'letterboxd_automation',
'label': 'Letterboxd',
'description': 'Import movies from any public <a href="http://letterboxd.com/">Letterboxd</a> watchlist',
'options': [
{
'name': 'automation_enabled',
'default': False,
'type': 'enabler',
},
{
'name': 'automation_urls_use',
'label': 'Use',
},
{
'name': 'automation_urls',
'label': 'Username',
'type': 'combined',
'combine': ['automation_urls_use', 'automation_urls'],
},
],
},
],
}]

View File

@@ -0,0 +1,49 @@
from bs4 import BeautifulSoup
from couchpotato.core.helpers.variable import tryInt, splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation
import re
log = CPLog(__name__)
class Letterboxd(Automation):
url = 'http://letterboxd.com/%s/watchlist/'
pattern = re.compile(r'(.*)\((\d*)\)')
def getIMDBids(self):
urls = splitString(self.conf('automation_urls'))
if len(urls) == 0:
return []
movies = []
for movie in self.getWatchlist():
imdb_id = self.search(movie.get('title'), movie.get('year'), imdb_only = True)
movies.append(imdb_id)
return movies
def getWatchlist(self):
enablers = [tryInt(x) for x in splitString(self.conf('automation_urls_use'))]
urls = splitString(self.conf('automation_urls'))
index = -1
movies = []
for username in urls:
index += 1
if not enablers[index]:
continue
soup = BeautifulSoup(self.getHTMLData(self.url % username))
for movie in soup.find_all('a', attrs = { 'class': 'frame' }):
match = filter(None, self.pattern.split(movie['title']))
movies.append({'title': match[0], 'year': match[1] })
return movies

View File

@@ -104,6 +104,7 @@ class YarrProvider(Provider):
try:
cookiejar = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar))
opener.addheaders = []
urllib2.install_opener(opener)
log.info2('Logging into %s', self.urls['login'])
f = opener.open(self.urls['login'], self.getLoginParams())

View File

@@ -52,8 +52,7 @@ class MovieResultModifier(Plugin):
if l:
# Statuses
active_status = fireEvent('status.get', 'active', single = True)
done_status = fireEvent('status.get', 'done', single = True)
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
for movie in l.movies:
if movie.status_id == active_status['id']:

View File

@@ -2,9 +2,11 @@ from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.request import jsonified, getParams
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.movie.base import MovieProvider
from couchpotato.core.settings.model import Movie
from couchpotato.environment import Env
import time
log = CPLog(__name__)
@@ -18,19 +20,37 @@ class CouchPotatoApi(MovieProvider):
'is_movie': 'https://couchpota.to/api/ismovie/%s/',
'eta': 'https://couchpota.to/api/eta/%s/',
'suggest': 'https://couchpota.to/api/suggest/',
'updater': 'https://couchpota.to/api/updater/?%s',
'messages': 'https://couchpota.to/api/messages/?%s',
}
http_time_between_calls = 0
api_version = 1
def __init__(self):
#addApiView('movie.suggest', self.suggestView)
addEvent('movie.info', self.getInfo, priority = 1)
addEvent('movie.search', self.search, priority = 1)
addEvent('movie.release_date', self.getReleaseDate)
addEvent('movie.suggest', self.suggest)
addEvent('movie.is_movie', self.isMovie)
addEvent('cp.source_url', self.getSourceUrl)
addEvent('cp.messages', self.getMessages)
def getMessages(self, last_check = 0):
data = self.getJsonData(self.urls['messages'] % tryUrlencode({
'last_check': last_check,
}), headers = self.getRequestHeaders(), cache_timeout = 10)
return data
def getSourceUrl(self, repo = None, repo_name = None, branch = None):
return self.getJsonData(self.urls['updater'] % tryUrlencode({
'repo': repo,
'name': repo_name,
'branch': branch,
}), headers = self.getRequestHeaders())
def search(self, q, limit = 12):
return self.getJsonData(self.urls['search'] % tryUrlencode(q), headers = self.getRequestHeaders())
@@ -96,4 +116,5 @@ class CouchPotatoApi(MovieProvider):
'X-CP-Version': fireEvent('app.version', single = True),
'X-CP-API': self.api_version,
'X-CP-Time': time.time(),
'X-CP-Identifier': '+%s' % Env.setting('api_key', 'core')[:10], # Use first 10 as identifier, so we don't need to use IP address in api stats
}

View File

@@ -18,6 +18,14 @@ config = [{
'name': 'enabled',
'type': 'enabler',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
},
],

View File

@@ -27,6 +27,14 @@ config = [{
'default': '',
'type': 'password',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
},
],

View File

@@ -12,9 +12,10 @@ config = [{
'list': 'nzb_providers',
'name': 'newznab',
'order': 10,
'description': 'Enable <a href="http://newznab.com/" target="_blank">NewzNab providers</a> such as <a href="https://nzb.su" target="_blank">NZB.su</a>, \
'description': 'Enable <a href="http://newznab.com/" target="_blank">NewzNab</a> such as <a href="https://nzb.su" target="_blank">NZB.su</a>, \
<a href="https://nzbs.org" target="_blank">NZBs.org</a>, <a href="http://dognzb.cr/" target="_blank">DOGnzb.cr</a>, \
<a href="https://github.com/spotweb/spotweb" target="_blank">Spotweb</a> or <a href="https://nzbgeek.info/" target="_blank">NZBGeek</a>',
<a href="https://github.com/spotweb/spotweb" target="_blank">Spotweb</a>, <a href="https://nzbgeek.info/" target="_blank">NZBGeek</a>, \
<a href="https://smackdownonyou.com" target="_blank">SmackDown</a>, <a href="https://www.nzbfinder.ws" target="_blank">NZBFinder</a>',
'wizard': True,
'options': [
{
@@ -23,20 +24,27 @@ config = [{
},
{
'name': 'use',
'default': '0,0,0,0'
'default': '0,0,0,0,0,0'
},
{
'name': 'host',
'default': 'nzb.su,dognzb.cr,nzbs.org,https://index.nzbgeek.info',
'default': 'nzb.su,dognzb.cr,nzbs.org,https://index.nzbgeek.info, https://smackdownonyou.com, https://www.nzbfinder.ws',
'description': 'The hostname of your newznab provider',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'default': '0,0,0,0,0,0',
'description': 'Starting score for each release found via this provider.',
},
{
'name': 'api_key',
'default': ',,,',
'default': ',,,,,',
'label': 'Api Key',
'description': 'Can be found on your profile page',
'type': 'combined',
'combine': ['use', 'host', 'api_key'],
'combine': ['use', 'host', 'api_key', 'extra_score'],
},
],
},

View File

@@ -1,6 +1,6 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import cleanHost, splitString
from couchpotato.core.helpers.variable import cleanHost, splitString, tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import ResultList
from couchpotato.core.providers.nzb.base import NZBProvider
@@ -76,6 +76,7 @@ class Newznab(NZBProvider, RSS):
'url': (self.getUrl(host['host'], self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host),
'detail_url': '%sdetails/%s' % (cleanHost(host['host']), tryUrlencode(nzb_id)),
'content': self.getTextElement(nzb, 'description'),
'score': host['extra_score'],
})
def getHosts(self):
@@ -83,13 +84,15 @@ class Newznab(NZBProvider, RSS):
uses = splitString(str(self.conf('use')))
hosts = splitString(self.conf('host'))
api_keys = splitString(self.conf('api_key'))
extra_score = splitString(self.conf('extra_score'))
list = []
for nr in range(len(hosts)):
list.append({
'use': uses[nr],
'host': hosts[nr],
'api_key': api_keys[nr]
'api_key': api_keys[nr],
'extra_score': tryInt(extra_score[nr]) if len(extra_score) > nr else 0
})
return list

View File

@@ -18,6 +18,14 @@ config = [{
'name': 'enabled',
'type': 'enabler',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
},
],

View File

@@ -19,6 +19,14 @@ config = [{
'type': 'enabler',
'default': True,
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
},
],

View File

@@ -35,6 +35,14 @@ config = [{
'label': 'English only',
'description': 'Only search for English spoken movies on Nzbsrus',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
},
],

View File

@@ -19,6 +19,14 @@ config = [{
'type': 'enabler',
'default': True,
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
},
],

View File

@@ -27,6 +27,14 @@ config = [{
'label': 'Api Key',
'default': '',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'default': 20,
'type': 'int',
'description': 'Starting score for each release found via this provider.',
}
],
},
],

View File

@@ -34,6 +34,14 @@ config = [{
'type': 'bool',
'description': 'Only search for [FreeLeech] torrents.',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
},
],

View File

@@ -19,6 +19,14 @@ config = [{
'type': 'enabler',
'default': True,
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
},
],

View File

@@ -37,6 +37,14 @@ config = [{
{
'name': 'passkey',
'default': '',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 20,
'description': 'Starting score for each release found via this provider.',
}
],
}

View File

@@ -19,6 +19,14 @@ config = [{
'type': 'enabler',
'default': True,
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
},
],

View File

@@ -28,6 +28,14 @@ config = [{
'default': '',
'type': 'password',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 20,
'description': 'Starting score for each release found via this provider.',
}
],
},
],

View File

@@ -28,6 +28,14 @@ config = [{
'default': '',
'type': 'password',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
},
],

View File

@@ -24,6 +24,14 @@ config = [{
'advanced': True,
'label': 'Proxy server',
'description': 'Domain for requests, keep empty to let CouchPotato pick.',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
}

View File

@@ -28,6 +28,14 @@ config = [{
'default': '',
'type': 'password',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
},
],

View File

@@ -28,6 +28,14 @@ config = [{
'default': '',
'type': 'password',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 20,
'description': 'Starting score for each release found via this provider.',
}
],
},
],

View File

@@ -74,3 +74,6 @@ class TorrentLeech(TorrentProvider):
'remember_me': 'on',
'login': 'submit',
})
def loginSuccess(self, output):
return '/user/account/logout' in output.lower() or 'welcome back' in output.lower()

View File

@@ -0,0 +1,6 @@
from .main import Criticker
def start():
return Criticker()
config = []

View File

@@ -0,0 +1,6 @@
from couchpotato.core.providers.userscript.base import UserscriptBase
class Criticker(UserscriptBase):
includes = ['http://www.criticker.com/film/*']

View File

@@ -189,6 +189,9 @@ class Settings(object):
self.set(section, option, (new_value if new_value else value).encode('unicode_escape'))
self.save()
# After save (for re-interval etc)
fireEvent('setting.save.%s.%s.after' % (section, option), single = True)
return jsonified({
'success': True,
})

View File

@@ -76,7 +76,7 @@ class LibraryTitle(Entity):
title = Field(Unicode)
simple_title = Field(Unicode, index = True)
default = Field(Boolean, index = True)
default = Field(Boolean, default = False, index = True)
language = OneToMany('Language')
libraries = ManyToOne('Library')
@@ -141,12 +141,12 @@ class Status(Entity):
class Quality(Entity):
"""Quality name of a release, DVD, 720P, DVD-Rip etc"""
"""Quality name of a release, DVD, 720p, DVD-Rip etc"""
using_options(order_by = 'order')
identifier = Field(String(20), unique = True)
label = Field(Unicode(20))
order = Field(Integer, index = True)
order = Field(Integer, default = 0, index = True)
size_min = Field(Integer)
size_max = Field(Integer)
@@ -160,21 +160,27 @@ class Profile(Entity):
using_options(order_by = 'order')
label = Field(Unicode(50))
order = Field(Integer, index = True)
core = Field(Boolean)
hide = Field(Boolean)
order = Field(Integer, default = 0, index = True)
core = Field(Boolean, default = False)
hide = Field(Boolean, default = False)
movie = OneToMany('Movie')
types = OneToMany('ProfileType', cascade = 'all, delete-orphan')
def to_dict(self, deep = {}, exclude = []):
orig_dict = super(Profile, self).to_dict(deep = deep, exclude = exclude)
orig_dict['core'] = orig_dict.get('core') or False
orig_dict['hide'] = orig_dict.get('hide') or False
return orig_dict
class ProfileType(Entity):
""""""
using_options(order_by = 'order')
order = Field(Integer, index = True)
finish = Field(Boolean)
wait_for = Field(Integer)
order = Field(Integer, default = 0, index = True)
finish = Field(Boolean, default = True)
wait_for = Field(Integer, default = 0)
quality = ManyToOne('Quality')
profile = ManyToOne('Profile')
@@ -185,7 +191,7 @@ class File(Entity):
path = Field(Unicode(255), nullable = False, unique = True)
part = Field(Integer, default = 1)
available = Field(Boolean)
available = Field(Boolean, default = True)
type = ManyToOne('FileType')
properties = OneToMany('FileProperty')

View File

@@ -190,9 +190,12 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
version_control(db, repo, version = latest_db_version)
current_db_version = db_version(db, repo)
if current_db_version < latest_db_version and not development:
log.info('Doing database upgrade. From %d to %d', (current_db_version, latest_db_version))
upgrade(db, repo)
if current_db_version < latest_db_version:
if development:
log.error('There is a database migration ready, but you are running development mode, so it won\'t be used. If you see this, you are stupid. Please disable development mode.')
else:
log.info('Doing database upgrade. From %d to %d', (current_db_version, latest_db_version))
upgrade(db, repo)
# Configure Database
from couchpotato.core.settings.model import setup
@@ -238,7 +241,8 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
from tornado.ioloop import IOLoop
web_container = WSGIContainer(app)
web_container._log = _log
loop = IOLoop.instance()
loop = IOLoop.current()
application = Application([
(r'%s/api/%s/nonblock/(.*)/' % (url_base, api_key), NonBlockHandler),

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 213 KiB

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