Compare commits

...

239 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
1cd8040692 One up 2013-03-17 16:39:09 +01:00
Ruud
4db1b57c70 Merge branch 'refs/heads/develop' 2013-03-17 16:31:31 +01:00
Ruud
7268e02386 zindex fixes & empty home element 2013-03-17 15:50:45 +01:00
Ruud
805aa3ca9f Split query to fix title bug. fix #1510 2013-03-17 15:14:04 +01:00
Ruud
29cb34551c Hide title and description by default 2013-03-17 14:42:24 +01:00
Ruud
d267be4455 Only sleep on 404 when not in dev mode 2013-03-17 14:10:29 +01:00
Ruud
92f4ade371 Save the last view properly 2013-03-17 12:55:07 +01:00
Ruud
9235eda73b Reverse merging using priority 2013-03-17 12:42:14 +01:00
Ruud
1fe23afd1b Don't mark first title default 2013-03-17 11:40:22 +01:00
Ruud
09637c3069 Revert "Search priority"
This reverts commit 2cafd509fc.
2013-03-17 11:39:54 +01:00
Ruud
2cafd509fc Search priority 2013-03-17 02:01:48 +01:00
Ruud
62cc570ab2 Mask zindex fix 2013-03-17 01:52:27 +01:00
Ruud
1ec9370e68 Make sure to set default title on refresh. fix #1436 2013-03-17 01:40:36 +01:00
Ruud
5b4c60ecba Optimize dashboard.soon with joins 2013-03-17 01:14:15 +01:00
Ruud
7b7488ece8 Dashboard split
Do more with snatched and other statusses
2013-03-16 22:23:11 +01:00
Ruud
4ba7ff9f27 Search mask fix 2013-03-16 22:21:33 +01:00
Ruud
df2d1aca4b Allow email notification to send to multiple addresses 2013-03-16 15:27:38 +01:00
Ruud
4fcba70c9a Cleanup dashboard snatched movies 2013-03-16 11:53:55 +01:00
Ruud
d0fc20ca6e Add last_edit to movie and release tables 2013-03-16 11:51:46 +01:00
Ruud
9402b54f9b Force to wanted after wizard 2013-03-15 16:38:26 +01:00
Ruud
f0e7795b9b Ubuntu init script /etc/default 2013-03-15 14:33:34 +01:00
dfiore1230
bba18d8bc9 added the ability to source /etc/default/couchpotato file
added the ability to source /etc/default/couchpotato file by testing for file existence and source when available

added lines 39 - 45
2013-03-15 13:58:33 +01:00
Ruud
0494e5fc8f Cleanup pushalot notifier 2013-03-13 22:08:51 +01:00
Travis La Marr
df1b46272d Pushalot notifier for Windows Phone 7/8 and Windows 8 2013-03-13 21:34:18 +01:00
Ruud
b06dbd3069 Merge branch 'refs/heads/develop' 2013-03-12 21:12:18 +01:00
Ruud Burger
ed068f09b0 Only chown PID file 2013-03-12 10:40:20 +01:00
Ruud Burger
5e852d05ee Only remove PID file 2013-03-12 08:29:29 +01:00
Ruud Burger
d111393bd6 Remove PID path 2013-03-12 08:21:23 +01:00
Ruud
f84aa8c638 Merge branch 'refs/heads/develop' 2013-03-09 18:15:26 +01:00
Ruud
89bff73431 Decode torrent hash for magnets also 2013-03-09 18:15:06 +01:00
Ruud
8e07dfc730 Merge branch 'refs/heads/develop' 2013-03-08 14:46:01 +01:00
Ruud
cd16dddf13 Make sure to use the correct hash for utorrent 2013-03-08 14:45:32 +01:00
Ruud
25605c45b9 IPTorrent download url fix
Thanks seedboy
2013-03-08 14:28:46 +01:00
Ruud
b6d0d54609 Add params to cache_key 2013-03-04 23:11:40 +01:00
Ruud
98981dac27 Suggestions 2013-03-04 23:11:26 +01:00
Ruud
ddf03cbcf2 Diskspace event 2013-03-04 23:11:20 +01:00
Ruud
1e1abf407c Dashboard 2013-03-04 23:11:13 +01:00
Ruud
1267cdac4d Remove print from TPB provider 2013-02-24 00:18:10 +01:00
Ruud
05bcee12ae No need for folder for pid file 2013-02-24 00:17:57 +01:00
Ruud
fc3f15e0cf Remove dots and spaces from left movie name. fixes #1428 2013-02-23 17:45:27 +01:00
Ruud
0a7765f639 uTorrent status support. closes #1391
Thanks to Stourwalk
2013-02-23 16:36:12 +01:00
Ruud
c214458770 IPTorrents, don't continue if nothing found. fixes #1423 2013-02-23 16:09:53 +01:00
Ruud
bfe501c84a Better XBMC notification image. close #1427 2013-02-23 16:01:20 +01:00
Ruud
e034465df8 Show newznab name in release list. fix #1400 2013-02-23 15:58:36 +01:00
Ruud
a7b78d4131 Tornado update 2013-02-22 23:20:16 +01:00
Ruud
3eed34c710 Gzip Tornado response 2013-02-22 22:56:08 +01:00
Ruud
9cb3bef156 Fallback to non-minified scripts 2013-02-22 21:23:38 +01:00
Ruud
46c7e3fbed IPTorrent support. closes #1411
Thanks to @seedboy
2013-02-15 21:36:59 +01:00
Ruud
a49a00a25f Host to 0.0.0.0 2013-02-14 23:02:44 +01:00
Ruud
eed0382b41 Host to 0.0.0.0 2013-02-14 23:01:34 +01:00
Ruud
673843fb66 Merge branch 'refs/heads/develop' 2013-02-12 23:25:11 +01:00
Ruud
4e45c94fc3 Renamer NTFS permission fix #778 2013-02-12 23:23:18 +01:00
Ruud
0a11dc6673 Set file permissions on .nzb or torrent file. closes #1362
Thanks clinton
2013-02-12 23:12:40 +01:00
Ruud
4ede2c20a1 Goodfilm automation provider. closes #1366 2013-02-12 23:10:34 +01:00
Ruud
af0cf523e3 Fedora init script. closes #1399 2013-02-12 22:56:02 +01:00
Ruud
3908e00650 Stop progress search on fail. fix #1409 2013-02-12 22:49:44 +01:00
Ruud
f9bdf6da1c Send correct headers to SABNZBd. fix #1406 2013-02-12 22:42:26 +01:00
Ruud
811f35b028 Merge branch 'refs/heads/develop' 2013-02-04 23:11:39 +01:00
Ruud
87cdf9222d Hide test notification button 2013-02-04 23:05:21 +01:00
Ruud
2ca2cc9597 Don't fire openpage twice on start 2013-02-04 22:36:22 +01:00
Ruud
edb232df60 Don't fire progress untill other request ended 2013-02-04 22:35:25 +01:00
Ruud
af113c0ffd Minifier 2 2013-02-04 21:59:12 +01:00
Ruud
856b495995 Minifier 2013-02-04 21:48:02 +01:00
Ruud
a56bbf0b3b CP API cleanup 2013-02-03 21:50:29 +01:00
Ruud
4b54113f08 Use CP api for movie check 2013-02-03 18:20:11 +01:00
Ruud
52371b7705 Daemonize cleanup 2013-02-02 23:16:02 +01:00
Ruud
629bead919 Raise current exception 2013-02-02 12:02:54 +01:00
Ruud
c7cd72787f Ignore extracted folder. fix #1369 2013-02-02 11:49:12 +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
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
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
393c14de54 Urlencode spotweb id. fix #1213 2013-01-07 23:12:08 +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
Frank Fenton
c087a6b49b Add Trakt notification 2012-09-18 02:44:38 +10:00
260 changed files with 27403 additions and 4934 deletions

View File

@@ -166,8 +166,8 @@ class CouchPotatoApp(wx.App, SoftwareUpdate):
def OnInit(self):
# Updater
base_url = 'http://couchpota.to/updates/%s/' % VERSION
self.InitUpdates(base_url, base_url + 'changelog.html',
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)

View File

@@ -78,6 +78,7 @@ def page_not_found(error):
r = '%s%s' % (request.url.rstrip('/'), index_url + '#' + url)
return redirect(r)
else:
time.sleep(0.1)
if not Env.get('dev'):
time.sleep(0.1)
return 'Wrong API key used', 404

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:
@@ -179,7 +179,7 @@ class Core(Plugin):
if Env.get('daemonized'): return
def signal_handler(signal, frame):
fireEvent('app.shutdown')
fireEvent('app.shutdown', single = True)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)

View File

@@ -1,15 +1,60 @@
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.jsmin import jsmin
import cssprefixer
import os
import traceback
log = CPLog(__name__)
class ClientScript(Plugin):
urls = {
'style': {},
'script': {},
core_static = {
'style': [
'style/main.css',
'style/uniform.generic.css',
'style/uniform.css',
'style/settings.css',
],
'script': [
'scripts/library/mootools.js',
'scripts/library/mootools_more.js',
'scripts/library/uniform.js',
'scripts/library/form_replacement/form_check.js',
'scripts/library/form_replacement/form_radio.js',
'scripts/library/form_replacement/form_dropdown.js',
'scripts/library/form_replacement/form_selectoption.js',
'scripts/library/question.js',
'scripts/library/scrollspy.js',
'scripts/library/spin.js',
'scripts/couchpotato.js',
'scripts/api.js',
'scripts/library/history.js',
'scripts/page.js',
'scripts/block.js',
'scripts/block/navigation.js',
'scripts/block/footer.js',
'scripts/block/menu.js',
'scripts/page/home.js',
'scripts/page/wanted.js',
'scripts/page/settings.js',
'scripts/page/about.js',
'scripts/page/manage.js',
],
}
urls = {'style': {}, 'script': {}, }
minified = {'style': {}, 'script': {}, }
paths = {'style': {}, 'script': {}, }
comment = {
'style': '/*** %s:%d ***/\n',
'script': '// %s:%d\n'
}
html = {
@@ -24,6 +69,69 @@ class ClientScript(Plugin):
addEvent('clientscript.get_styles', self.getStyles)
addEvent('clientscript.get_scripts', self.getScripts)
if not Env.get('dev'):
addEvent('app.load', self.minify)
self.addCore()
def addCore(self):
for static_type in self.core_static:
for rel_path in self.core_static.get(static_type):
file_path = os.path.join(Env.get('app_dir'), 'couchpotato', 'static', rel_path)
core_url = 'api/%s/static/%s?%s' % (Env.setting('api_key'), rel_path, tryInt(os.path.getmtime(file_path)))
if static_type == 'script':
self.registerScript(core_url, file_path, position = 'front')
else:
self.registerStyle(core_url, file_path, position = 'front')
def minify(self):
for file_type in ['style', 'script']:
ext = 'js' if file_type is 'script' else 'css'
positions = self.paths.get(file_type, {})
for position in positions:
files = positions.get(position)
self._minify(file_type, files, position, position + '.' + ext)
def _minify(self, file_type, files, position, out):
cache = Env.get('cache_dir')
out_name = 'minified_' + out
out = os.path.join(cache, out_name)
raw = []
for file_path in files:
f = open(file_path, 'r').read()
if file_type == 'script':
data = jsmin(f)
else:
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})
# Combine all files together with some comments
data = ''
for r in raw:
data += self.comment.get(file_type) % (r.get('file'), r.get('date'))
data += r.get('data') + '\n\n'
self.createFile(out, ss(data.strip()))
if not self.minified.get(file_type):
self.minified[file_type] = {}
if not self.minified[file_type].get(position):
self.minified[file_type][position] = []
minified_url = 'api/%s/file.cache/%s?%s' % (Env.setting('api_key'), out_name, tryInt(os.path.getmtime(out)))
self.minified[file_type][position].append(minified_url)
def getStyles(self, *args, **kwargs):
return self.get('style', *args, **kwargs)
@@ -35,22 +143,30 @@ class ClientScript(Plugin):
data = '' if as_html else []
try:
try:
if not Env.get('dev'):
return self.minified[type][location]
except:
pass
return self.urls[type][location]
except Exception, e:
log.error(e)
except:
log.error('Error getting minified %s, %s: %s', (type, location, traceback.format_exc()))
return data
def registerStyle(self, path, position = 'head'):
self.register(path, 'style', position)
def registerStyle(self, api_path, file_path, position = 'head'):
self.register(api_path, file_path, 'style', position)
def registerScript(self, path, position = 'head'):
self.register(path, 'script', position)
def registerScript(self, api_path, file_path, position = 'head'):
self.register(api_path, file_path, 'script', position)
def register(self, filepath, type, location):
def register(self, api_path, file_path, type, location):
if not self.urls[type].get(location):
self.urls[type][location] = []
self.urls[type][location].append(api_path)
filePath = filepath
self.urls[type][location].append(filePath)
if not self.paths[type].get(location):
self.paths[type][location] = []
self.paths[type][location].append(file_path)

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

@@ -1,6 +1,7 @@
from __future__ import with_statement
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
import os
import traceback
@@ -36,6 +37,7 @@ class Blackhole(Downloader):
log.info('Downloading %s to %s.', (data.get('type'), fullPath))
with open(fullPath, 'wb') as f:
f.write(filedata)
os.chmod(fullPath, Env.getPermission('file'))
return True
else:
log.info('File %s already exists.', fullPath)

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,7 +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
@@ -16,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),
@@ -30,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)
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)
sab_data = self.call(req_params)
except URLError:
log.error('Failed sending release, probably wrong HOST: %s', traceback.format_exc(0))
return False
@@ -48,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):
@@ -84,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,
})
@@ -110,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
@@ -132,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)
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,11 +1,15 @@
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
import re
import time
import urllib
@@ -37,6 +41,7 @@ class uTorrent(Downloader):
if not filedata and data.get('type') == 'torrent':
log.error('Failed sending torrent, no data')
return False
if data.get('type') == 'torrent_magnet':
torrent_hash = re.findall('urn:btih:([\w]{32,40})', data.get('url'))[0].upper()
torrent_params['trackers'] = '%0D%0A%0D%0A'.join(self.torrent_trackers)
@@ -45,6 +50,10 @@ class uTorrent(Downloader):
torrent_hash = sha1(bencode(info)).hexdigest().upper()
torrent_filename = self.createFileName(data, filedata, movie)
# Convert base 32 to hex
if len(torrent_hash) == 32:
torrent_hash = b16encode(b32decode(torrent_hash))
# Send request to uTorrent
try:
if not self.utorrent_api:
@@ -59,11 +68,99 @@ 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
def getAllDownloadStatus(self):
log.debug('Checking uTorrent 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
try:
self.utorrent_api = uTorrentAPI(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
except Exception, err:
log.error('Failed to get uTorrent object: %s', err)
return False
data = ''
try:
data = self.utorrent_api.get_status()
queue = json.loads(data)
if queue.get('error'):
log.error('Error getting data from uTorrent: %s', queue.get('error'))
return False
except Exception, err:
log.error('Failed to get status from uTorrent: %s', err)
return False
if queue.get('torrents', []) == []:
log.debug('Nothing in queue')
return False
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', []):
# item[21] = Paused | Downloading | Seeding | Finished
status = 'busy'
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': str(timedelta(seconds = item[10])),
'folder': release_folder,
})
return statuses
class uTorrentAPI(object):
@@ -94,9 +191,7 @@ class uTorrentAPI(object):
try:
open_request = self.opener.open(request)
response = open_request.read()
log.debug('response: %s', response)
if response:
log.debug('uTorrent action successfull')
return response
else:
log.debug('Unknown failure sending command to uTorrent. Return text is: %s', response)
@@ -133,3 +228,11 @@ class uTorrentAPI(object):
def pause_torrent(self, hash):
action = "action=pause&hash=%s" % hash
return self._request(action)
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,11 +102,14 @@ def fireEvent(name, *args, **kwargs):
# Merge
if options['merge'] and len(results) > 0:
# 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
@@ -138,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]
@@ -168,4 +182,4 @@ def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
return ''.join(random.choice(chars) for x in range(size))
def splitString(str, split_on = ','):
return [x.strip() for x in str.split(split_on)]
return [x.strip() for x in str.split(split_on)] if str else []

View File

@@ -0,0 +1,25 @@
from migrate.changeset.schema import create_column
from sqlalchemy.schema import MetaData, Column, Table, Index
from sqlalchemy.types import Integer
meta = MetaData()
def upgrade(migrate_engine):
meta.bind = migrate_engine
# Change release, add last_edit and index
last_edit_column = Column('last_edit', Integer)
release = Table('release', meta, last_edit_column)
create_column(last_edit_column, release)
Index('ix_release_last_edit', release.c.last_edit).create()
# Change movie last_edit
last_edit_column = Column('last_edit', Integer)
movie = Table('movie', meta, last_edit_column)
Index('ix_movie_last_edit', movie.c.last_edit).create()
def downgrade(migrate_engine):
pass

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);
},
@@ -178,11 +210,14 @@ var NotificationBase = new Class({
},
addTestButton: function(fieldset, plugin_name){
var self = this;
var self = this,
button_name = self.testButtonName(fieldset);
if(button_name.contains('Notifications')) return;
new Element('.ctrlHolder.test_button').adopt(
new Element('a.button', {
'text': self.testButtonName(fieldset),
'text': button_name,
'events': {
'click': function(){
var button = fieldset.getElement('.test_button .button');
@@ -191,7 +226,7 @@ var NotificationBase = new Class({
Api.request('notify.'+plugin_name+'.test', {
'onComplete': function(json){
button.set('text', self.testButtonName(fieldset));
button.set('text', button_name);
if(json.success){
var message = new Element('span.success', {

View File

@@ -1,4 +1,5 @@
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from email.mime.text import MIMEText
@@ -11,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')
@@ -39,7 +39,7 @@ class Email(Notification):
# Send the e-mail
log.debug("Sending the email")
mailserver.sendmail(from_address, to_address, message.as_string())
mailserver.sendmail(from_address, splitString(to_address), message.as_string())
# Close the SMTP connection
mailserver.quit()
@@ -49,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

@@ -0,0 +1,48 @@
from .main import Pushalot
def start():
return Pushalot()
config = [{
'name': 'pushalot',
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'pushalot',
'description': 'for Windows Phone and Windows 8',
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
},
{
'name': 'auth_token',
'label': 'Auth Token',
},
{
'name': 'silent',
'label': 'Silent',
'default': 0,
'type': 'bool',
'description': 'Don\'t send Toast notifications. Only update Live Tile',
},
{
'name': 'important',
'label': 'High Priority',
'default': 0,
'type': 'bool',
'description': 'Send message with High priority.',
},
{
'name': 'on_snatch',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Also send message when movie is snatched.',
},
],
}
],
}]

View File

@@ -0,0 +1,36 @@
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
import traceback
log = CPLog(__name__)
class Pushalot(Notification):
urls = {
'api': 'https://pushalot.com/api/sendmessage'
}
def notify(self, message = '', data = {}, listener = None):
data = {
'AuthorizationToken': self.conf('auth_token'),
'Title': self.default_title,
'Body': toUnicode(message),
'IsImportant': self.conf('important'),
'IsSilent': self.conf('silent'),
'Image': toUnicode(self.getNotificationImage('medium') + '?1'),
'Source': toUnicode(self.default_title)
}
headers = {
'Content-type': 'application/x-www-form-urlencoded'
}
try:
self.urlopen(self.urls['api'], headers = headers, params = data, multipart = True, show_error = False)
return True
except:
log.error('PushAlot failed: %s', traceback.format_exc())
return False

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

@@ -64,7 +64,7 @@ class Plugin(object):
for f in glob.glob(os.path.join(self.plugin_path, 'static', '*')):
ext = getExt(f)
if ext in ['js', 'css']:
fireEvent('register_%s' % ('script' if ext in 'js' else 'style'), path + os.path.basename(f))
fireEvent('register_%s' % ('script' if ext in 'js' else 'style'), path + os.path.basename(f), f)
def showStatic(self, filename):
d = os.path.join(self.plugin_path, 'static')
@@ -240,7 +240,6 @@ class Plugin(object):
del kwargs['cache_timeout']
data = self.urlopen(url, **kwargs)
if data:
self.setCache(cache_key, data, timeout = cache_timeout)
return data

View File

@@ -15,7 +15,7 @@ if os.name == 'nt':
raise ImportError("Missing the win32file module, which is a part of the prerequisite \
pywin32 package. You can get it from http://sourceforge.net/projects/pywin32/files/pywin32/");
else:
import win32file
import win32file #@UnresolvedImport
class FileBrowser(Plugin):
@@ -98,7 +98,7 @@ class FileBrowser(Plugin):
def has_hidden_attribute(self, filepath):
try:
attrs = ctypes.windll.kernel32.GetFileAttributesW(unicode(filepath))
attrs = ctypes.windll.kernel32.GetFileAttributesW(unicode(filepath)) #@UndefinedVariable
assert attrs != -1
result = bool(attrs & 2)
except (AttributeError, AssertionError):

View File

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

View File

@@ -0,0 +1,134 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.request import jsonified, getParams
from couchpotato.core.helpers.variable import splitString, tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie
from sqlalchemy.orm import joinedload_all
import random
import time
log = CPLog(__name__)
class Dashboard(Plugin):
def __init__(self):
addApiView('dashboard.suggestions', self.suggestView)
addApiView('dashboard.soon', self.getSoonView)
def newSuggestions(self):
movies = fireEvent('movie.list', status = ['active', 'done'], limit_offset = (20, 0), single = True)
movie_identifiers = [m['library']['identifier'] for m in movies[1]]
ignored_movies = fireEvent('movie.list', status = ['ignored', 'deleted'], limit_offset = (100, 0), single = True)
ignored_identifiers = [m['library']['identifier'] for m in ignored_movies[1]]
suggestions = fireEvent('movie.suggest', movies = movie_identifiers, ignore = ignored_identifiers, single = True)
suggest_status = fireEvent('status.get', 'suggest', single = True)
for suggestion in suggestions:
fireEvent('movie.add', params = {'identifier': suggestion}, force_readd = False, search_after = False, status_id = suggest_status.get('id'))
def suggestView(self):
db = get_session()
movies = db.query(Movie).limit(20).all()
identifiers = [m.library.identifier for m in movies]
suggestions = fireEvent('movie.suggest', movies = identifiers, single = True)
return jsonified({
'result': True,
'suggestions': suggestions
})
def getSoonView(self):
params = getParams()
db = get_session()
now = time.time()
# Get profiles first, determine pre or post theater
profiles = fireEvent('profile.all', single = True)
qualities = fireEvent('quality.all', single = True)
pre_releases = fireEvent('quality.pre_releases', single = True)
id_pre = {}
for quality in qualities:
id_pre[quality.get('id')] = quality.get('identifier') in pre_releases
# See what the profile contain and cache it
profile_pre = {}
for profile in profiles:
contains = {}
for profile_type in profile.get('types', []):
contains['theater' if id_pre.get(profile_type.get('quality_id')) else 'dvd'] = True
profile_pre[profile.get('id')] = contains
# Get all active movies
active_status = fireEvent('status.get', 'active', single = True)
subq = db.query(Movie).filter(Movie.status_id == active_status.get('id')).subquery()
q = db.query(Movie).join((subq, subq.c.id == Movie.id)) \
.options(joinedload_all('releases')) \
.options(joinedload_all('profile.types')) \
.options(joinedload_all('library.titles')) \
.options(joinedload_all('library.files')) \
.options(joinedload_all('status')) \
.options(joinedload_all('files'))
# Add limit
limit_offset = params.get('limit_offset')
limit = 12
if limit_offset:
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
limit = tryInt(splt[0])
all_movies = q.all()
if params.get('random', False):
random.shuffle(all_movies)
movies = []
for movie in all_movies:
pp = profile_pre.get(movie.profile.id)
eta = movie.library.info.get('release_date', {}) or {}
coming_soon = False
# Theater quality
if pp.get('theater') and fireEvent('searcher.could_be_released', True, eta, single = True):
coming_soon = True
if pp.get('dvd') and fireEvent('searcher.could_be_released', False, eta, single = True):
coming_soon = True
if coming_soon:
temp = movie.to_dict({
'profile': {'types': {}},
'releases': {'files':{}, 'info': {}},
'library': {'titles': {}, 'files':{}},
'files': {},
})
# Don't list older movies
if ((not params.get('late') and (not eta.get('dvd') or (eta.get('dvd') and eta.get('dvd') > (now - 2419200)))) or \
(params.get('late') and eta.get('dvd') and eta.get('dvd') < (now - 2419200))):
movies.append(temp)
if len(movies) >= limit:
break
return jsonified({
'success': True,
'empty': len(movies) == 0,
'movies': movies,
})
getLateView = getSoonView

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: 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

@@ -4,6 +4,7 @@ var File = new Class({
var self = this;
if(!file){
self.empty = true;
self.el = new Element('div');
return
}

View File

@@ -38,7 +38,7 @@ class LibraryPlugin(Plugin):
title = LibraryTitle(
title = toUnicode(attrs.get('title')),
simple_title = self.simplifyTitle(attrs.get('title'))
simple_title = self.simplifyTitle(attrs.get('title')),
)
l.titles.append(title)
@@ -96,6 +96,7 @@ class LibraryPlugin(Plugin):
titles = info.get('titles', [])
log.debug('Adding titles: %s', titles)
counter = 0
for title in titles:
if not title:
continue
@@ -103,9 +104,10 @@ class LibraryPlugin(Plugin):
t = LibraryTitle(
title = title,
simple_title = self.simplifyTitle(title),
default = title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == u'' and toUnicode(titles[0]) == title)
default = (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == u'' and toUnicode(titles[0]) == title)
)
library.titles.append(t)
counter += 1
db.commit()

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

@@ -2,11 +2,13 @@ from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import getTitle, splitString
from couchpotato.core.helpers.variable import splitString, getTitle
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
import ctypes
import os
import sys
import time
import traceback
@@ -22,6 +24,7 @@ class Manage(Plugin):
fireEvent('scheduler.interval', identifier = 'manage.update_library', handle = self.updateLibrary, hours = 2)
addEvent('manage.update', self.updateLibrary)
addEvent('manage.diskspace', self.getDiskSpace)
# Add files after renaming
def after_rename(message = None, group = {}):
@@ -182,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
@@ -192,6 +197,7 @@ class Manage(Plugin):
self.in_progress[folder]['to_go'] = self.in_progress[folder]['to_go'] - 1
total = self.in_progress[folder]['total']
movie_dict = fireEvent('movie.get', identifier, single = True)
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = None if total > 5 else 'Added "%s" to manage.' % getTitle(movie_dict['library']))
return afterUpdate
@@ -214,3 +220,31 @@ class Manage(Plugin):
for group in groups.itervalues():
if group['library'] and group['library'].get('identifier'):
fireEvent('release.add', group = group)
def getDiskSpace(self):
free_space = {}
for folder in self.directories():
size = None
if os.path.isdir(folder):
if os.name == 'nt':
_, total, free = ctypes.c_ulonglong(), ctypes.c_ulonglong(), \
ctypes.c_ulonglong()
if sys.version_info >= (3,) or isinstance(folder, unicode):
fun = ctypes.windll.kernel32.GetDiskFreeSpaceExW #@UndefinedVariable
else:
fun = ctypes.windll.kernel32.GetDiskFreeSpaceExA #@UndefinedVariable
ret = fun(folder, ctypes.byref(_), ctypes.byref(total), ctypes.byref(free))
if ret == 0:
raise ctypes.WinError()
used = total.value - free.value
return [total.value, used, free.value]
else:
s = os.statvfs(folder)
size = [s.f_blocks * s.f_frsize / (1024 * 1024), (s.f_bavail * s.f_frsize) / (1024 * 1024)]
free_space[folder] = size
return free_space

View File

@@ -6,11 +6,13 @@ from couchpotato.core.helpers.request import getParams, jsonified, getParam
from couchpotato.core.helpers.variable import getImdb, splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library, LibraryTitle, Movie
from couchpotato.core.settings.model import Library, LibraryTitle, Movie, \
Release
from couchpotato.environment import Env
from sqlalchemy.orm import joinedload_all
from sqlalchemy.sql.expression import or_, asc, not_
from sqlalchemy.sql.expression import or_, asc, not_, desc
from string import ascii_lowercase
import time
log = CPLog(__name__)
@@ -41,6 +43,7 @@ class MoviePlugin(Plugin):
'desc': 'List movies in wanted list',
'params': {
'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'},
'release_status': {'type': 'array or csv', 'desc': 'Filter movie by status of its releases. Example:"snatched,available"'},
'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'},
'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'},
'search': {'desc': 'Search movie title'},
@@ -94,6 +97,33 @@ class MoviePlugin(Plugin):
addEvent('movie.list', self.list)
addEvent('movie.restatus', self.restatus)
# Clean releases that didn't have activity in the last week
addEvent('app.load', self.cleanReleases)
fireEvent('schedule.interval', 'movie.clean_releases', self.cleanReleases, hours = 4)
def cleanReleases(self):
log.debug('Removing releases from dashboard')
now = time.time()
week = 262080
done_status, available_status, snatched_status = \
fireEvent('status.get', ['done', 'available', 'snatched'], single = True)
db = get_session()
# get movies last_edit more than a week ago
movies = db.query(Movie) \
.filter(Movie.status_id == done_status.get('id'), Movie.last_edit < (now - week)) \
.all()
#
for movie in movies:
for rel in movie.releases:
if rel.status_id in [available_status.get('id'), snatched_status.get('id')]:
fireEvent('release.delete', id = rel.id, single = True)
def getView(self):
movie_id = getParam('id')
@@ -121,20 +151,29 @@ class MoviePlugin(Plugin):
return results
def list(self, status = ['active'], limit_offset = None, starts_with = None, search = None):
def list(self, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None):
db = get_session()
# Make a list from string
if not isinstance(status, (list, tuple)):
if status and not isinstance(status, (list, tuple)):
status = [status]
if release_status and not isinstance(release_status, (list, tuple)):
release_status = [release_status]
q = db.query(Movie) \
.join(Movie.library, Library.titles) \
.outerjoin(Movie.releases, Movie.library, Library.titles) \
.filter(LibraryTitle.default == True) \
.filter(or_(*[Movie.status.has(identifier = s) for s in status])) \
.group_by(Movie.id)
# Filter on movie status
if status and len(status) > 0:
q = q.filter(or_(*[Movie.status.has(identifier = s) for s in status]))
# Filter on release status
if release_status and len(release_status) > 0:
q = q.filter(or_(*[Release.status.has(identifier = s) for s in release_status]))
total_count = q.count()
filter_or = []
@@ -154,7 +193,10 @@ class MoviePlugin(Plugin):
if filter_or:
q = q.filter(or_(*filter_or))
q = q.order_by(asc(LibraryTitle.simple_title))
if order == 'release_order':
q = q.order_by(desc(Release.last_edit))
else:
q = q.order_by(asc(LibraryTitle.simple_title))
q = q.subquery()
q2 = db.query(Movie).join((q, q.c.id == Movie.id)) \
@@ -166,7 +208,7 @@ class MoviePlugin(Plugin):
.options(joinedload_all('files'))
if limit_offset:
splt = splitString(limit_offset)
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
limit = splt[0]
offset = 0 if len(splt) is 1 else splt[1]
q2 = q2.limit(limit).offset(offset)
@@ -185,7 +227,7 @@ class MoviePlugin(Plugin):
#db.close()
return (total_count, movies)
def availableChars(self, status = ['active']):
def availableChars(self, status = None, release_status = None):
chars = ''
@@ -194,11 +236,20 @@ class MoviePlugin(Plugin):
# Make a list from string
if not isinstance(status, (list, tuple)):
status = [status]
if release_status and not isinstance(release_status, (list, tuple)):
release_status = [release_status]
q = db.query(Movie) \
.join(Movie.library, Library.titles, Movie.status) \
.options(joinedload_all('library.titles')) \
.filter(or_(*[Movie.status.has(identifier = s) for s in status]))
.outerjoin(Movie.releases, Movie.library, Library.titles, Movie.status) \
.options(joinedload_all('library.titles'))
# Filter on movie status
if status and len(status) > 0:
q = q.filter(or_(*[Movie.status.has(identifier = s) for s in status]))
# Filter on release status
if release_status and len(release_status) > 0:
q = q.filter(or_(*[Release.status.has(identifier = s) for s in release_status]))
results = q.all()
@@ -206,20 +257,29 @@ class MoviePlugin(Plugin):
char = movie.library.titles[0].simple_title[0]
char = char if char in ascii_lowercase else '#'
if char not in chars:
chars += char
chars += str(char)
#db.close()
return chars
return ''.join(sorted(chars, key = str.lower))
def listView(self):
params = getParams()
status = params.get('status', ['active'])
status = splitString(params.get('status', None))
release_status = splitString(params.get('release_status', None))
limit_offset = params.get('limit_offset', None)
starts_with = params.get('starts_with', None)
search = params.get('search', None)
order = params.get('order', None)
total_movies, movies = self.list(status = status, limit_offset = limit_offset, starts_with = starts_with, search = search)
total_movies, movies = self.list(
status = status,
release_status = release_status,
limit_offset = limit_offset,
starts_with = starts_with,
search = search,
order = order
)
return jsonified({
'success': True,
@@ -231,8 +291,9 @@ class MoviePlugin(Plugin):
def charView(self):
params = getParams()
status = params.get('status', ['active'])
chars = self.availableChars(status)
status = splitString(params.get('status', None))
release_status = splitString(params.get('release_status', None))
chars = self.availableChars(status, release_status)
return jsonified({
'success': True,
@@ -254,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))
@@ -283,7 +344,7 @@ class MoviePlugin(Plugin):
'movies': movies,
})
def add(self, params = {}, force_readd = True, search_after = True, update_library = False):
def add(self, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None):
if not params.get('identifier'):
msg = 'Can\'t add movie without imdb identifier.'
@@ -292,9 +353,8 @@ class MoviePlugin(Plugin):
return False
else:
try:
url = 'http://thetvdb.com/api/GetSeriesByRemoteID.php?imdbid=%s' % params.get('identifier')
tvdb = self.getCache('thetvdb.%s' % params.get('identifier'), url = url, show_error = False)
if tvdb and 'series' in tvdb.lower():
is_movie = fireEvent('movie.is_movie', identifier = params.get('identifier'), single = True)
if not is_movie:
msg = 'Can\'t add movie, seems to be a TV show.'
log.error(msg)
fireEvent('notify.frontend', type = 'movie.is_tvshow', message = msg)
@@ -306,8 +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)
status_snatched = fireEvent('status.add', 'snatched', 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)
@@ -319,7 +379,7 @@ class MoviePlugin(Plugin):
m = Movie(
library_id = library.get('id'),
profile_id = params.get('profile_id', default_profile.get('id')),
status_id = status_active.get('id'),
status_id = status_id if status_id else status_active.get('id'),
)
db.add(m)
db.commit()
@@ -331,10 +391,14 @@ class MoviePlugin(Plugin):
fireEventAsync('library.update', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete)
search_after = False
elif force_readd:
# Clean snatched history
for release in m.releases:
if release.status_id == status_snatched.get('id'):
release.delete()
if release.status_id in [downloaded_status.get('id'), snatched_status.get('id'), done_status.get('id')]:
if params.get('ignore_previous', False):
release.status_id = ignored_status.get('id')
else:
fireEvent('release.delete', release.id, single = True)
m.profile_id = params.get('profile_id', default_profile.get('id'))
else:
@@ -342,7 +406,8 @@ class MoviePlugin(Plugin):
added = False
if force_readd:
m.status_id = status_active.get('id')
m.status_id = status_id if status_id else status_active.get('id')
m.last_edit = int(time.time())
do_search = True
db.commit()
@@ -448,7 +513,7 @@ class MoviePlugin(Plugin):
total_deleted = 0
new_movie_status = None
for release in movie.releases:
if delete_from == 'wanted':
if delete_from in ['wanted', 'snatched']:
if release.status_id != done_status.get('id'):
db.delete(release)
total_deleted += 1
@@ -480,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,36 +1,54 @@
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: [],
movies_added: {},
letters: {},
filter: {
'startswith': null,
'search': null
},
filter: null,
initialize: function(options){
var self = this;
self.setOptions(options);
self.offset = 0;
self.filter = self.options.filter || {
'startswith': null,
'search': null
}
self.el = new Element('div.movies').adopt(
self.title = self.options.title ? new Element('h2', {
'text': self.options.title,
'styles': {'display': 'none'}
}) : null,
self.description = self.options.description ? new Element('div.description', {
'html': self.options.description,
'styles': {'display': 'none'}
}) : null,
self.movie_list = new Element('div'),
self.load_more = new Element('a.load_more', {
self.load_more = self.options.load_more ? new Element('a.load_more', {
'events': {
'click': self.loadMore.bind(self)
}
})
}) : null
);
if($(window).getSize().x <= 480 && !self.options.force_view)
self.changeView('list');
else
self.changeView(self.getSavedView() || self.options.view || 'details');
self.getMovies();
App.addEvent('movie.added', self.movieAdded.bind(self))
@@ -70,22 +88,14 @@ var MovieList = new Class({
if(self.options.navigation)
self.createNavigation();
self.movie_list.addEvents({
'mouseenter:relay(.movie)': function(e, el){
el.addClass('hover');
},
'mouseleave:relay(.movie)': function(e, el){
el.removeClass('hover');
}
});
self.scrollspy = new ScrollSpy({
min: function(){
var c = self.load_more.getCoordinates()
return c.top - window.document.getSize().y - 300
},
onEnter: self.loadMore.bind(self)
});
if(self.options.load_more)
self.scrollspy = new ScrollSpy({
min: function(){
var c = self.load_more.getCoordinates()
return c.top - window.document.getSize().y - 300
},
onEnter: self.loadMore.bind(self)
});
self.created = true;
},
@@ -96,7 +106,7 @@ var MovieList = new Class({
if(!self.created) self.create();
// do scrollspy
if(movies.length < self.options.limit){
if(movies.length < self.options.limit && self.scrollspy){
self.load_more.hide();
self.scrollspy.stop();
}
@@ -115,24 +125,20 @@ var MovieList = new Class({
if(!self.navigation_counter) return;
self.navigation_counter.set('text', (count || 0));
self.navigation_counter.set('text', (count || 0) + ' movies');
},
createMovie: function(movie, inject_at){
var self = this;
// Attach proper actions
var a = self.options.actions,
status = Status.get(movie.status_id);
var actions = a[status.identifier.capitalize()] || a.Wanted || {};
var m = new Movie(self, {
'actions': actions,
'actions': self.options.actions,
'view': self.current_view,
'onSelect': self.calculateSelected.bind(self)
}, movie);
$(m).inject(self.movie_list, inject_at || 'bottom');
m.fireEvent('injected');
self.movies.include(m)
@@ -143,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');
@@ -216,7 +224,7 @@ var MovieList = new Class({
});
// Actions
['mass_edit', 'thumbs', 'list'].each(function(view){
['mass_edit', 'details', 'list'].each(function(view){
self.navigation_actions.adopt(
new Element('li.'+view+(self.current_view == view ? '.active' : '')+'[data-view='+view+']', {
'events': {
@@ -246,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)
@@ -265,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();
},
@@ -398,11 +397,16 @@ var MovieList = new Class({
var self = this;
self.movies = []
self.calculateSelected()
self.navigation_alpha.getElements('.active').removeClass('active')
if(self.mass_edit_select)
self.calculateSelected()
if(self.navigation_alpha)
self.navigation_alpha.getElements('.active').removeClass('active')
self.offset = 0;
self.load_more.show();
self.scrollspy.start();
if(self.scrollspy){
self.load_more.show();
self.scrollspy.start();
}
},
activateLetter: function(letter){
@@ -418,21 +422,17 @@ var MovieList = new Class({
changeView: function(new_view){
var self = this;
self.movies.each(function(movie){
movie.changeView(new_view)
});
self.el
.removeClass(self.current_view+'_list')
.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') || 'thumbs';
return Cookie.read(self.options.identifier+'_view2');
},
search: function(){
@@ -468,20 +468,53 @@ var MovieList = new Class({
getMovies: function(){
var self = this;
if(self.scrollspy) self.scrollspy.stop();
self.load_more.set('text', 'loading...');
Api.request('movie.list', {
if(self.scrollspy){
self.scrollspy.stop();
self.load_more.set('text', 'loading...');
}
if(self.movies.length == 0 && self.options.loader){
self.loader_first = new Element('div.loading').adopt(
new Element('div.message', {'text': self.options.title ? 'Loading \'' + self.options.title + '\'' : 'Loading...'})
).inject(self.el, 'top');
createSpinner(self.loader_first, {
radius: 4,
length: 4,
width: 1
});
self.el.setStyle('min-height', 93);
}
Api.request(self.options.api_call || '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);
self.load_more.set('text', 'load more movies');
if(self.scrollspy) self.scrollspy.start();
if(self.scrollspy) {
self.load_more.set('text', 'load more movies');
self.scrollspy.start();
}
self.checkIfEmpty()
self.checkIfEmpty();
self.fireEvent('loaded');
}
});
},
@@ -502,10 +535,16 @@ var MovieList = new Class({
checkIfEmpty: function(){
var self = this;
var is_empty = self.movies.length == 0 && self.total_movies == 0;
var is_empty = self.movies.length == 0 && (self.total_movies == 0 || self.total_movies === undefined);
if(self.title)
self.title[is_empty ? 'hide' : 'show']()
if(self.description)
self.description.setStyle('display', [is_empty ? 'none' : ''])
if(is_empty && self.options.on_empty_element){
self.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

@@ -0,0 +1,745 @@
var MovieAction = new Class({
class_name: 'action icon',
initialize: function(movie){
var self = this;
self.movie = movie;
self.create();
if(self.el)
self.el.addClass(self.class_name)
},
create: function(){},
disable: function(){
this.el.addClass('disable')
},
enable: function(){
this.el.removeClass('disable')
},
createMask: function(){
var self = this;
self.mask = new Element('div.mask', {
'styles': {
'z-index': '1'
}
}).inject(self.movie, 'top').fade('hide');
//self.positionMask();
},
positionMask: function(){
var self = this,
movie = $(self.movie),
s = movie.getSize()
return;
return self.mask.setStyles({
'width': s.x,
'height': s.y
}).position({
'relativeTo': movie
})
},
toElement: function(){
return this.el || null
}
});
var MA = {};
MA.IMDB = new Class({
Extends: MovieAction,
id: null,
create: function(){
var self = this;
self.id = self.movie.get('identifier');
self.el = new Element('a.imdb', {
'title': 'Go to the IMDB page of ' + self.movie.getTitle(),
'href': 'http://www.imdb.com/title/'+self.id+'/',
'target': '_blank'
});
if(!self.id) self.disable();
}
});
MA.Release = new Class({
Extends: MovieAction,
create: function(){
var self = this;
self.el = new Element('a.releases.icon.download', {
'title': 'Show the releases that are available for ' + self.movie.getTitle(),
'events': {
'click': self.show.bind(self)
}
});
if(self.movie.data.releases.length == 0)
self.el.hide()
else
self.showHelper();
},
createReleases: function(){
var self = this;
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')
)
);
// Header
new Element('div.item.head').adopt(
new Element('span.name', {'text': 'Release name'}),
new Element('span.status', {'text': 'Status'}),
new Element('span.quality', {'text': 'Quality'}),
new Element('span.size', {'text': 'Size'}),
new Element('span.age', {'text': 'Age'}),
new Element('span.score', {'text': 'Score'}),
new Element('span.provider', {'text': 'Provider'})
).inject(self.release_container)
self.movie.data.releases.sortBy('-info.score').each(function(release){
var status = Status.get(release.status_id),
quality = Quality.getProfile(release.quality_id) || {},
info = release.info,
provider = self.get(release, 'provider') + (release.info['provider_extra'] ? self.get(release, 'provider_extra') : '');
release.status = status;
var release_name = self.get(release, 'name');
if(release.files && release.files.length > 0){
try {
var movie_file = release.files.filter(function(file){
var type = File.Type.get(file.type_id);
return type && type.identifier == 'movie'
}).pick();
release_name = movie_file.path.split(Api.getOption('path_sep')).getLast();
}
catch(e){}
}
// Create release
new Element('div', {
'class': 'item '+status.identifier,
'id': 'release_'+release.id
}).adopt(
new Element('span.name', {'text': release_name, 'title': release_name}),
new Element('span.status', {'text': status.identifier, 'class': 'release_status '+status.identifier}),
new Element('span.quality', {'text': quality.get('label') || 'n/a'}),
new Element('span.size', {'text': release.info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}),
new Element('span.age', {'text': self.get(release, 'age')}),
new Element('span.score', {'text': self.get(release, 'score')}),
new Element('span.provider', { 'text': provider, 'title': provider }),
release.info['detail_url'] ? new Element('a.info.icon', {
'href': release.info['detail_url'],
'target': '_blank'
}) : null,
new Element('a.download.icon', {
'events': {
'click': function(e){
(e).preventDefault();
if(!this.hasClass('completed'))
self.download(release);
}
}
}),
new Element('a.delete.icon', {
'events': {
'click': function(e){
(e).preventDefault();
self.ignore(release);
this.getParent('.item').toggleClass('ignored')
}
}
})
).inject(self.release_container)
if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){
if(!self.last_release || (self.last_release && self.last_release.status.identifier != 'snatched' && status.identifier == 'snatched'))
self.last_release = release;
}
else if(!self.next_release && status.identifier == 'available'){
self.next_release = release;
}
});
if(self.last_release){
self.release_container.getElement('#release_'+self.last_release.id).addClass('last_release');
}
if(self.next_release){
self.release_container.getElement('#release_'+self.next_release.id).addClass('next_release');
}
if(self.next_release || self.last_release){
self.trynext_container.adopt(
new Element('span.or', {
'text': 'This movie is snatched, if anything went wrong, download'
}),
self.last_release ? new Element('a.button.orange', {
'text': 'the same release again',
'events': {
'click': self.trySameRelease.bind(self)
}
}) : null,
self.next_release && self.last_release ? new Element('span.or', {
'text': ','
}) : null,
self.next_release ? [new Element('a.button.green', {
'text': self.last_release ? 'another release' : 'the best release',
'events': {
'click': self.tryNextRelease.bind(self)
}
}),
new Element('span.or', {
'text': 'or pick one below'
})] : null
)
}
}
},
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'
},
download: function(release){
var self = this;
var release_el = self.release_container.getElement('#release_'+release.id),
icon = release_el.getElement('.download.icon');
self.movie.busy(true);
Api.request('release.download', {
'data': {
'id': release.id
},
'onComplete': function(json){
self.movie.busy(false);
if(json.success)
icon.addClass('completed');
else
icon.addClass('attention').set('title', 'Something went wrong when downloading, please check logs.');
}
});
},
ignore: function(release){
var self = this;
Api.request('release.ignore', {
'data': {
'id': release.id
}
})
},
tryNextRelease: function(movie_id){
var self = this;
self.createReleases();
if(self.last_release)
self.ignore(self.last_release);
if(self.next_release)
self.download(self.next_release);
},
trySameRelease: function(movie_id){
var self = this;
if(self.last_release)
self.download(self.last_release);
}
});
MA.Trailer = new Class({
Extends: MovieAction,
id: null,
create: function(){
var self = this;
self.el = new Element('a.trailer', {
'title': 'Watch the trailer of ' + self.movie.getTitle(),
'events': {
'click': self.watch.bind(self)
}
});
},
watch: function(offset){
var self = this;
var data_url = 'http://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18'
var url = data_url.substitute({
'title': encodeURI(self.movie.getTitle()),
'year': self.movie.get('year'),
'offset': offset || 1
}),
size = $(self.movie).getSize(),
height = (size.x/16)*9,
id = 'trailer-'+randomString();
self.player_container = new Element('div[id='+id+']');
self.container = new Element('div.hide.trailer_container')
.adopt(self.player_container)
.inject($(self.movie), 'top');
self.container.setStyle('height', 0);
self.container.removeClass('hide');
self.close_button = new Element('a.hide.hide_trailer', {
'text': 'Hide trailer',
'events': {
'click': self.stop.bind(self)
}
}).inject(self.movie);
self.container.setStyle('height', height);
$(self.movie).setStyle('height', height);
new Request.JSONP({
'url': url,
'onComplete': function(json){
var video_url = json.feed.entry[0].id.$t.split('/'),
video_id = video_url[video_url.length-1];
self.player = new YT.Player(id, {
'height': height,
'width': size.x,
'videoId': video_id,
'playerVars': {
'autoplay': 1,
'showsearch': 0,
'wmode': 'transparent',
'iv_load_policy': 3
}
});
self.close_button.removeClass('hide');
var quality_set = false;
var change_quality = function(state){
if(!quality_set && (state.data == 1 || state.data || 2)){
try {
self.player.setPlaybackQuality('hd720');
quality_set = true;
}
catch(e){
}
}
}
self.player.addEventListener('onStateChange', change_quality);
}
}).send()
},
stop: function(){
var self = this;
self.player.stopVideo();
self.container.addClass('hide');
self.close_button.addClass('hide');
$(self.movie).setStyle('height', null);
setTimeout(function(){
self.container.destroy()
self.close_button.destroy();
}, 1800)
}
});
MA.Edit = new Class({
Extends: MovieAction,
create: function(){
var self = this;
self.el = new Element('a.edit', {
'title': 'Change movie information, like title and quality.',
'events': {
'click': self.editMovie.bind(self)
}
});
},
editMovie: function(e){
var self = this;
(e).preventDefault();
if(!self.options_container){
self.options_container = new Element('div.options').adopt(
new Element('div.form').adopt(
self.title_select = new Element('select', {
'name': 'title'
}),
self.profile_select = new Element('select', {
'name': 'profile'
}),
new Element('a.button.edit', {
'text': 'Save & Search',
'events': {
'click': self.save.bind(self)
}
})
)
).inject(self.movie, 'top');
Array.each(self.movie.data.library.titles, function(alt){
new Element('option', {
'text': alt.title
}).inject(self.title_select);
if(alt['default'])
self.title_select.set('value', alt.title);
});
Quality.getActiveProfiles().each(function(profile){
var profile_id = profile.id ? profile.id : profile.data.id;
new Element('option', {
'value': profile_id,
'text': profile.label ? profile.label : profile.data.label
}).inject(self.profile_select);
if(self.movie.profile && self.movie.profile.data && self.movie.profile.data.id == profile_id)
self.profile_select.set('value', profile_id);
});
}
self.movie.slide('in', self.options_container);
},
save: function(e){
(e).preventDefault();
var self = this;
Api.request('movie.edit', {
'data': {
'id': self.movie.get('id'),
'default_title': self.title_select.get('value'),
'profile_id': self.profile_select.get('value')
},
'useSpinner': true,
'spinnerTarget': $(self.movie),
'onComplete': function(){
self.movie.quality.set('text', self.profile_select.getSelected()[0].get('text'));
self.movie.title.set('text', self.title_select.getSelected()[0].get('text'));
}
});
self.movie.slide('out');
}
})
MA.Refresh = new Class({
Extends: MovieAction,
create: function(){
var self = this;
self.el = new Element('a.refresh', {
'title': 'Refresh the movie info and do a forced search',
'events': {
'click': self.doRefresh.bind(self)
}
});
},
doRefresh: function(e){
var self = this;
(e).preventDefault();
Api.request('movie.refresh', {
'data': {
'id': self.movie.get('id')
}
});
}
});
MA.Readd = new Class({
Extends: MovieAction,
create: function(){
var self = this;
var movie_done = Status.get(self.movie.data.status_id).identifier == 'done';
if(!movie_done)
var snatched = self.movie.data.releases.filter(function(release){
return release.status && (release.status.identifier == 'snatched' || release.status.identifier == 'downloaded' || release.status.identifier == 'done');
}).length;
if(movie_done || snatched && snatched > 0)
self.el = new Element('a.readd', {
'title': 'Readd the movie and mark all previous snatched/downloaded as ignored',
'events': {
'click': self.doReadd.bind(self)
}
});
},
doReadd: function(e){
var self = this;
(e).preventDefault();
Api.request('movie.add', {
'data': {
'identifier': self.movie.get('identifier'),
'ignore_previous': 1
}
});
}
});
MA.Delete = new Class({
Extends: MovieAction,
Implements: [Chain],
create: function(){
var self = this;
self.el = new Element('a.delete', {
'title': 'Remove the movie from this CP list',
'events': {
'click': self.showConfirm.bind(self)
}
});
},
showConfirm: function(e){
var self = this;
(e).preventDefault();
if(!self.delete_container){
self.delete_container = new Element('div.buttons.delete_container').adopt(
new Element('a.cancel', {
'text': 'Cancel',
'events': {
'click': self.hideConfirm.bind(self)
}
}),
new Element('span.or', {
'text': 'or'
}),
new Element('a.button.delete', {
'text': 'Delete ' + self.movie.title.get('text'),
'events': {
'click': self.del.bind(self)
}
})
).inject(self.movie, 'top');
}
self.movie.slide('in', self.delete_container);
},
hideConfirm: function(e){
var self = this;
(e).preventDefault();
self.movie.slide('out');
},
del: function(e){
(e).preventDefault();
var self = this;
var movie = $(self.movie);
self.chain(
function(){
self.callChain();
},
function(){
Api.request('movie.delete', {
'data': {
'id': self.movie.get('id'),
'delete_from': self.movie.list.options.identifier
},
'onComplete': function(){
movie.set('tween', {
'duration': 300,
'onComplete': function(){
self.movie.destroy()
}
});
movie.tween('height', 0);
}
});
}
);
self.callChain();
}
});
MA.Files = new Class({
Extends: MovieAction,
create: function(){
var self = this;
self.el = new Element('a.directory', {
'title': 'Available files',
'events': {
'click': self.showFiles.bind(self)
}
});
},
showFiles: function(e){
var self = this;
(e).preventDefault();
if(!self.options_container){
self.options_container = new Element('div.options').adopt(
self.files_container = new Element('div.files.table')
).inject(self.movie, 'top');
// Header
new Element('div.item.head').adopt(
new Element('span.name', {'text': 'File'}),
new Element('span.type', {'text': 'Type'}),
new Element('span.is_available', {'text': 'Available'})
).inject(self.files_container)
Array.each(self.movie.data.releases, function(release){
var rel = new Element('div.release').inject(self.files_container);
Array.each(release.files, function(file){
new Element('div.file.item').adopt(
new Element('span.name', {'text': file.path}),
new Element('span.type', {'text': File.Type.get(file.type_id).name}),
new Element('span.available', {'text': file.available})
).inject(rel)
});
});
}
self.movie.slide('in', self.options_container);
},
});

View File

@@ -1,50 +1,185 @@
.movies {
padding: 60px 0 20px;
padding: 10px 0 20px;
position: relative;
z-index: 3;
width: 100%;
}
.movies.mass_edit_list {
padding-top: 90px;
.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;
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;
}
.home .movies {
padding-top: 6px;
}
.movies .movie {
position: relative;
border-radius: 4px;
margin: 10px 0;
padding-left: 20px;
overflow: hidden;
width: 100%;
transition: all 0.2s linear;
height: 180px;
transition: all 0.6s cubic-bezier(0.9,0,0.1,1);
}
.movies .movie.list_view, .movies .movie.mass_edit_view {
.movies.details_list .movie {
padding-left: 120px;
}
.movies.list_list .movie:not(.details_view),
.movies.mass_edit_list .movie {
height: 32px;
}
.movies.thumbs_list .movie {
width: 16.66667%;
height: auto;
display: inline-block;
margin: 0;
padding: 0;
vertical-align: top;
border-radius: 0;
box-shadow: none;
border: 0;
}
@media all and (max-width: 800px) {
.movies.thumbs_list .movie {
width: 25%;
}
}
.movies .movie .mask {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
.movies.list_list .movie:not(.details_view),
.movies.mass_edit_list .movie {
margin: 1px 0;
border-radius: 0;
background: no-repeat;
box-shadow: none;
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.movies .movie.list_view:hover, .movies .movie.mass_edit_view:hover {
background: rgba(255,255,255,0.03);
}
.movies .movie_container {
overflow: hidden;
.movies.list_list .movie:hover:not(.details_view),
.movies.mass_edit_list .movie {
background: rgba(255,255,255,0.03);
}
.movies .data {
padding: 20px;
height: 180px;
width: 840px;
height: 100%;
width: 100%;
position: relative;
float: right;
right: 0;
border-radius: 0;
transition: all 0.2s linear;
transition: all .6s cubic-bezier(0.9,0,0.1,1);
}
.movies .list_view .data, .movies .mass_edit_view .data {
.movies.list_list .movie:not(.details_view) .data,
.movies.mass_edit_list .movie .data {
height: 30px;
padding: 3px 0 3px 10px;
width: 938px;
box-shadow: none;
border: 0;
background: #4e5969;
}
.movies.thumbs_list .data {
position: absolute;
left: 0;
top: 0;
width: 100%;
padding: 10px;
height: 100%;
background: none;
transition: none;
}
.movies.thumbs_list .movie.no_thumbnail .data { background-image: linear-gradient(-30deg, rgba(255, 0, 85, .2) 0,rgba(125, 185, 235, .2) 100%);
}
.movies.thumbs_list .movie.no_thumbnail:nth-child(2n+6) .data { background-image: linear-gradient(-20deg, rgba(125, 0, 215, .2) 0, rgba(4, 55, 5, .7) 100%); }
.movies.thumbs_list .movie.no_thumbnail:nth-child(3n+6) .data { background-image: linear-gradient(-30deg, rgba(155, 0, 85, .2) 0,rgba(25, 185, 235, .7) 100%); }
.movies.thumbs_list .movie.no_thumbnail:nth-child(4n+6) .data { background-image: linear-gradient(-30deg, rgba(115, 5, 235, .2) 0, rgba(55, 180, 5, .7) 100%); }
.movies.thumbs_list .movie.no_thumbnail:nth-child(5n+6) .data { background-image: linear-gradient(-30deg, rgba(35, 15, 215, .2) 0, rgba(135, 215, 115, .7) 100%); }
.movies.thumbs_list .movie.no_thumbnail:nth-child(6n+6) .data { background-image: linear-gradient(-30deg, rgba(35, 15, 215, .2) 0, rgba(135, 15, 115, .7) 100%); }
.movies.thumbs_list .movie:hover .data {
background: rgba(0,0,0,0.9);
}
.movies .data.hide_right {
right: -100%;
}
.movies .movie .check {
@@ -52,143 +187,264 @@
}
.movies.mass_edit_list .movie .check {
float: left;
position: absolute;
left: 0;
top: 0;
display: block;
margin: 7px 0 0 5px;
}
.movies .poster {
float: left;
position: absolute;
left: 0;
width: 120px;
line-height: 0;
overflow: hidden;
height: 180px;
height: 100%;
border-radius: 4px 0 0 4px;
transition: all 0.2s linear;
transition: all .6s cubic-bezier(0.9,0,0.1,1);
}
.movies .list_view .poster, .movies .mass_edit_view .poster {
.movies.thumbs_list .poster {
position: relative;
border-radius: 0;
}
.movies.list_list .movie:not(.details_view) .poster,
.movies.mass_edit_list .poster {
width: 20px;
height: 30px;
border-radius: 1px 0 0 1px;
}
.movies.mass_edit_list .poster {
display: none;
}
.movies.thumbs_list .poster {
width: 100%;
height: 100%;
transition: none;
}
.movies .poster img, .options .poster img {
width: 101%;
height: 101%;
.movies .poster img,
.options .poster img {
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 {
font-size: 30px;
font-size: 28px;
font-weight: bold;
margin-bottom: 10px;
float: left;
width: 90%;
margin-top: 2px;
left: 0;
top: 0;
width: 100%;
padding-right: 80px;
transition: all 0.2s linear;
}
.movies .list_view .info .title, .movies .mass_edit_view .info .title {
.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;
}
.movies.thumbs_list .movie:not(.no_thumbnail) .info {
display: none;
}
.movies.thumbs_list .movie:hover .info {
display: block;
}
.movies.thumbs_list .info .title {
font-size: 21px;
word-wrap: break-word;
padding: 0;
}
.movies .info .year {
font-size: 30px;
margin-bottom: 10px;
float: right;
position: absolute;
color: #bbb;
width: 10%;
right: 0;
top: 1px;
text-align: right;
transition: all 0.2s linear;
font-weight: normal;
}
.movies .list_view .info .year, .movies .mass_edit_view .info .year {
font-size: 16px;
width: 6%;
.movies.list_list .movie:not(.details_view) .info .year,
.movies.mass_edit_list .info .year {
font-size: 1.25em;
right: 10px;
}
.movies.thumbs_list .info .year {
font-size: 23px;
margin: 0;
bottom: 0;
left: 0;
top: auto;
right: auto;
color: #FFF;
}
.movies .info .rating {
font-size: 30px;
margin-bottom: 10px;
color: #444;
float: left;
width: 5%;
padding: 0 0 0 3%;
}
.movies .info .description {
top: 30px;
clear: both;
height: 80px;
bottom: 30px;
overflow: hidden;
position: absolute;
}
.movies .data:hover .description {
overflow: auto;
}
.movies .list_view .info .description, .movies .mass_edit_view .info .description {
.movies.list_list .movie:not(.details_view) .info .description,
.movies.mass_edit_list .info .description,
.movies.thumbs_list .info .description {
display: none;
}
.movies .data .quality {
position: absolute;
bottom: 2px;
display: block;
min-height: 20px;
vertical-align: mid;
}
.movies .data .quality span {
padding: 2px 3px;
font-weight: bold;
opacity: 0.5;
font-size: 10px;
height: 16px;
line-height: 12px;
vertical-align: middle;
display: inline-block;
text-transform: uppercase;
text-shadow: none;
font-weight: normal;
margin: 0 2px;
border-radius: 2px;
background-color: rgba(255,255,255,0.1);
}
.movies .list_view .data .quality, .movies .mass_edit_view .data .quality {
text-align: right;
float: right;
@media all and (max-width: 480px) {
.movies .data .quality {
display: none;
}
}
.movies .data .quality .available, .movies .data .quality .snatched {
opacity: 1;
box-shadow: 1px 1px 0 rgba(0,0,0,0.2);
cursor: pointer;
}
.movies .data .quality .available { background-color: #578bc3; }
.movies .data .quality .snatched { background-color: #369545; }
.movies .data .quality .done {
background-color: #369545;
opacity: 1;
}
.movies .data .quality .finish {
background-image: url('../images/sprite.png');
background-repeat: no-repeat;
background-position: 0 2px;
padding-left: 14px;
background-size: 14px
}
.movies .data .actions {
line-height: 0;
clear: both;
float: right;
margin-top: -25px;
}
.movies .data:hover .action { opacity: 0.6; }
.movies .data:hover .action:hover { opacity: 1; }
.movies.mass_edit_list .data .actions {
.movies .status_suggest .data .quality,
.movies.thumbs_list .data .quality {
display: none;
}
.movies .data .quality span {
padding: 2px 3px;
font-weight: bold;
opacity: 0.5;
font-size: 10px;
height: 16px;
line-height: 12px;
vertical-align: middle;
display: inline-block;
text-transform: uppercase;
font-weight: normal;
margin: 0 4px 0 0;
border-radius: 2px;
background-color: rgba(255,255,255,0.1);
}
.movies.list_list .data .quality,
.movies.mass_edit_list .data .quality {
text-align: right;
right: 0;
margin-right: 60px;
z-index: 1;
}
.movies .data .quality .available,
.movies .data .quality .snatched {
opacity: 1;
box-shadow: 1px 1px 0 rgba(0,0,0,0.2);
cursor: pointer;
}
.movies .data .quality .available { background-color: #578bc3; }
.movies .data .quality .snatched { background-color: #369545; }
.movies .data .quality .done {
background-color: #369545;
opacity: 1;
}
.movies .data .quality .finish {
background-image: url('../images/sprite.png');
background-repeat: no-repeat;
background-position: 0 2px;
padding-left: 14px;
background-size: 14px
}
.movies .data .actions {
position: absolute;
bottom: 20px;
right: 20px;
line-height: 0;
top: 0;
opacity: 0;
display: none;
width: 0;
}
@media all and (max-width: 480px) {
.movies .data .actions {
display: none !important;
}
}
.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;
background-position: center;
@@ -196,13 +452,20 @@
width: 26px;
height: 26px;
padding: 3px;
opacity: 0;
}
.movies.mass_edit_list .movie .data .actions {
display: none;
}
.movies .list_view .data:hover .actions, .movies .mass_edit_view .data:hover .actions {
margin: -34px 2px 0 0;
.movies.list_list .movie:not(.details_view):hover .actions,
.movies.mass_edit_list .movie:hover .actions {
margin: 0;
background: #4e5969;
position: relative;
top: 2px;
bottom: 2px;
right: 5px;
z-index: 3;
}
.movies .delete_container {
@@ -211,7 +474,8 @@
font-size: 20px;
position: absolute;
padding: 70px 0 0;
width: 100%;
left: 120px;
right: 0;
}
.movies .delete_container .cancel {
}
@@ -229,8 +493,8 @@
.movies .options {
position: absolute;
margin-left: 120px;
width: 840px;
right: 0;
left: 120px;
}
.movies .options .form {
@@ -253,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');
@@ -284,9 +547,10 @@
.movies .options .table .provider {
width: 120px;
text-overflow: ellipsis;
overflow: hidden;
}
.movies .options .table .name {
width: 350px;
width: 340px;
overflow: hidden;
text-align: left;
padding: 0 10px;
@@ -320,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;
@@ -335,11 +602,12 @@
padding: 3px 10px;
background: #4e5969;
border-radius: 0 0 2px 2px;
transition: all .6s cubic-bezier(0.9,0,0.1,1) .2s;
}
.movies .movie .hide_trailer.hide {
top: -30px;
transition: all .2s cubic-bezier(0.9,0,0.1,1) .2s;
z-index: 11;
}
.movies .movie .hide_trailer.hide {
top: -30px;
}
.movies .movie .try_container {
padding: 5px 10px;
@@ -366,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;
@@ -379,24 +695,32 @@
.movies .alph_nav {
transition: box-shadow .4s linear;
position: fixed;
z-index: 2;
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;
position: relative;
z-index: 4;
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;
@@ -405,48 +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 {
.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;
@@ -472,7 +810,7 @@
background-position: 3px -95px;
}
.movies .alph_nav .actions li.thumbs span {
.movies .alph_nav .actions li.details span {
background-position: 3px -74px;
}
@@ -530,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;
}
@@ -590,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

@@ -8,7 +8,7 @@ var Movie = new Class({
var self = this;
self.data = data;
self.view = options.view || 'thumbs';
self.view = options.view || 'details';
self.list = list;
self.el = new Element('div.movie.inlay');
@@ -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,22 +60,23 @@ 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();
self.spinner = createSpinner(self.mask);
self.positionMask();
self.mask.fade('in');
}
},
@@ -81,10 +85,9 @@ var Movie = new Class({
var self = this;
self.mask = new Element('div.mask', {
'styles': {
'z-index': '1'
'z-index': 4
}
}).inject(self.el, 'top').fade('hide');
self.positionMask();
},
positionMask: function(){
@@ -103,7 +106,7 @@ var Movie = new Class({
var self = this;
self.data = notification.data;
self.container.destroy();
self.el.empty();
self.profile = Quality.getProfile(self.data.profile_id) || {};
self.create();
@@ -114,52 +117,52 @@ var Movie = new Class({
create: function(){
var self = this;
var s = Status.get(self.get('status_id'));
self.el.addClass('status_'+s.identifier);
self.el.adopt(
self.container = new Element('div.movie_container').adopt(
self.select_checkbox = new Element('input[type=checkbox].inlay', {
'events': {
'change': function(){
self.fireEvent('select')
}
self.select_checkbox = new Element('input[type=checkbox].inlay', {
'events': {
'change': function(){
self.fireEvent('select')
}
}),
self.thumbnail = File.Select.single('poster', self.data.library.files),
self.data_container = new Element('div.data.inlay.light', {
'tween': {
duration: 400,
transition: 'quint:in:out',
onComplete: self.fireEvent.bind(self, 'slideEnd')
}
}).adopt(
self.info_container = new Element('div.info').adopt(
self.title = new Element('div.title', {
}
}),
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(
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
}),
self.description = new Element('div.description', {
'text': self.data.library.plot
}),
self.quality = new Element('div.quality', {
'events': {
'click': function(e){
var releases = self.el.getElement('.actions .releases');
if(releases)
releases.fireEvent('click', [e])
}
}
})
),
self.actions = new Element('div.actions')
)
self.rating = new Element('div.rating.icon', {
'text': self.data.library.rating
}),
self.description = new Element('div.description', {
'text': self.data.library.plot
}),
self.quality = new Element('div.quality', {
'events': {
'click': function(e){
var releases = self.el.getElement('.actions .releases');
if(releases)
releases.fireEvent('click', [e])
}
}
})
),
self.actions = new Element('div.actions')
)
);
self.changeView(self.view);
if(self.thumbnail.empty)
self.el.addClass('no_thumbnail');
//self.changeView(self.view);
self.select_checkbox_class = new Form.Check(self.select_checkbox);
// Add profile
@@ -174,7 +177,7 @@ var Movie = new Class({
});
// Add done releases
// Add releases
self.data.releases.each(function(release){
var q = self.quality.getElement('.q_id'+ release.quality_id),
@@ -241,23 +244,23 @@ var Movie = new Class({
if(direction == 'in'){
self.temp_view = self.view;
self.changeView('thumbs')
self.changeView('details')
self.el.addEvent('outerClick', function(){
self.changeView(self.temp_view)
self.removeView()
self.slide('out')
})
el.show();
self.data_container.tween('right', 0, -840);
self.data_container.addClass('hide_right');
}
else {
self.el.removeEvents('outerClick')
self.addEvent('slideEnd:once', function(){
setTimeout(function(){
self.el.getElements('> :not(.data):not(.poster):not(.movie_container)').hide();
});
}, 600);
self.data_container.tween('right', -840, 0);
self.data_container.removeClass('hide_right');
}
},
@@ -271,6 +274,12 @@ var Movie = new Class({
self.view = new_view;
},
removeView: function(){
var self = this;
self.el.removeClass(self.view+'_view')
},
get: function(attr){
return this.data[attr] || this.data.library[attr]
},
@@ -288,388 +297,4 @@ var Movie = new Class({
return this.el;
}
});
var MovieAction = new Class({
class_name: 'action icon',
initialize: function(movie){
var self = this;
self.movie = movie;
self.create();
if(self.el)
self.el.addClass(self.class_name)
},
create: function(){},
disable: function(){
this.el.addClass('disable')
},
enable: function(){
this.el.removeClass('disable')
},
createMask: function(){
var self = this;
self.mask = new Element('div.mask', {
'styles': {
'z-index': '1'
}
}).inject(self.movie, 'top').fade('hide');
self.positionMask();
},
positionMask: function(){
var self = this,
movie = $(self.movie),
s = movie.getSize()
return;
return self.mask.setStyles({
'width': s.x,
'height': s.y
}).position({
'relativeTo': movie
})
},
toElement: function(){
return this.el || null
}
});
var IMDBAction = new Class({
Extends: MovieAction,
id: null,
create: function(){
var self = this;
self.id = self.movie.get('identifier');
self.el = new Element('a.imdb', {
'title': 'Go to the IMDB page of ' + self.movie.getTitle(),
'href': 'http://www.imdb.com/title/'+self.id+'/',
'target': '_blank'
});
if(!self.id) self.disable();
}
});
var ReleaseAction = new Class({
Extends: MovieAction,
create: function(){
var self = this;
self.el = new Element('a.releases.icon.download', {
'title': 'Show the releases that are available for ' + self.movie.getTitle(),
'events': {
'click': self.show.bind(self)
}
});
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;
}
});
},
show: function(e){
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(
new Element('span.name', {'text': 'Release name'}),
new Element('span.status', {'text': 'Status'}),
new Element('span.quality', {'text': 'Quality'}),
new Element('span.size', {'text': 'Size'}),
new Element('span.age', {'text': 'Age'}),
new Element('span.score', {'text': 'Score'}),
new Element('span.provider', {'text': 'Provider'})
).inject(self.release_container)
self.movie.data.releases.sortBy('-info.score').each(function(release){
var status = Status.get(release.status_id),
quality = Quality.getProfile(release.quality_id) || {},
info = release.info;
release.status = status;
// Create release
new Element('div', {
'class': 'item '+status.identifier,
'id': 'release_'+release.id
}).adopt(
new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}),
new Element('span.status', {'text': status.identifier, 'class': 'release_status '+status.identifier}),
new Element('span.quality', {'text': quality.get('label') || 'n/a'}),
new Element('span.size', {'text': release.info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}),
new Element('span.age', {'text': self.get(release, 'age')}),
new Element('span.score', {'text': self.get(release, 'score')}),
new Element('span.provider', {'text': self.get(release, 'provider')}),
release.info['detail_url'] ? new Element('a.info.icon', {
'href': release.info['detail_url'],
'target': '_blank'
}) : null,
new Element('a.download.icon', {
'events': {
'click': function(e){
(e).preventDefault();
if(!this.hasClass('completed'))
self.download(release);
}
}
}),
new Element('a.delete.icon', {
'events': {
'click': function(e){
(e).preventDefault();
self.ignore(release);
this.getParent('.item').toggleClass('ignored')
}
}
})
).inject(self.release_container)
if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){
if(!self.last_release || (self.last_release && self.last_release.status.identifier != 'snatched' && status.identifier == 'snatched'))
self.last_release = release;
}
else if(!self.next_release && status.identifier == 'available'){
self.next_release = release;
}
});
if(self.last_release){
self.release_container.getElement('#release_'+self.last_release.id).addClass('last_release');
}
if(self.next_release){
self.release_container.getElement('#release_'+self.next_release.id).addClass('next_release');
}
if(self.next_release || self.last_release){
self.trynext_container.adopt(
new Element('span.or', {
'text': 'This movie is snatched, if anything went wrong, download'
}),
self.last_release ? new Element('a.button.orange', {
'text': 'the same release again',
'events': {
'click': self.trySameRelease.bind(self)
}
}) : null,
self.next_release && self.last_release ? new Element('span.or', {
'text': ','
}) : null,
self.next_release ? [new Element('a.button.green', {
'text': self.last_release ? 'another release' : 'the best release',
'events': {
'click': self.tryNextRelease.bind(self)
}
}),
new Element('span.or', {
'text': 'or pick one below'
})] : null
)
}
}
self.movie.slide('in', self.options_container);
},
get: function(release, type){
return release.info[type] || 'n/a'
},
download: function(release){
var self = this;
var release_el = self.release_container.getElement('#release_'+release.id),
icon = release_el.getElement('.download.icon');
icon.addClass('spinner');
Api.request('release.download', {
'data': {
'id': release.id
},
'onComplete': function(json){
icon.removeClass('spinner')
if(json.success)
icon.addClass('completed');
else
icon.addClass('attention').set('title', 'Something went wrong when downloading, please check logs.');
}
});
},
ignore: function(release){
var self = this;
Api.request('release.ignore', {
'data': {
'id': release.id
}
})
},
tryNextRelease: function(movie_id){
var self = this;
if(self.last_release)
self.ignore(self.last_release);
if(self.next_release)
self.download(self.next_release);
},
trySameRelease: function(movie_id){
var self = this;
if(self.last_release)
self.download(self.last_release);
}
});
var TrailerAction = new Class({
Extends: MovieAction,
id: null,
create: function(){
var self = this;
self.el = new Element('a.trailer', {
'title': 'Watch the trailer of ' + self.movie.getTitle(),
'events': {
'click': self.watch.bind(self)
}
});
},
watch: function(offset){
var self = this;
var data_url = 'http://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18'
var url = data_url.substitute({
'title': encodeURI(self.movie.getTitle()),
'year': self.movie.get('year'),
'offset': offset || 1
}),
size = $(self.movie).getSize(),
height = (size.x/16)*9,
id = 'trailer-'+randomString();
self.player_container = new Element('div[id='+id+']');
self.container = new Element('div.hide.trailer_container')
.adopt(self.player_container)
.inject(self.movie.container, 'top');
self.container.setStyle('height', 0);
self.container.removeClass('hide');
self.close_button = new Element('a.hide.hide_trailer', {
'text': 'Hide trailer',
'events': {
'click': self.stop.bind(self)
}
}).inject(self.movie);
setTimeout(function(){
$(self.movie).setStyle('max-height', height);
self.container.setStyle('height', height);
}, 100)
new Request.JSONP({
'url': url,
'onComplete': function(json){
var video_url = json.feed.entry[0].id.$t.split('/'),
video_id = video_url[video_url.length-1];
self.player = new YT.Player(id, {
'height': height,
'width': size.x,
'videoId': video_id,
'playerVars': {
'autoplay': 1,
'showsearch': 0,
'wmode': 'transparent',
'iv_load_policy': 3
}
});
self.close_button.removeClass('hide');
var quality_set = false;
var change_quality = function(state){
if(!quality_set && (state.data == 1 || state.data || 2)){
try {
self.player.setPlaybackQuality('hd720');
quality_set = true;
}
catch(e){
}
}
}
self.player.addEventListener('onStateChange', change_quality);
}
}).send()
},
stop: function(){
var self = this;
self.player.stopVideo();
self.container.addClass('hide');
self.close_button.addClass('hide');
setTimeout(function(){
self.container.destroy()
self.close_button.destroy();
}, 1800)
}
});

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,50 +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;
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 {
border-radius: 3px;
.search_form .mask,
.movie_result .mask {
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', {
@@ -366,7 +353,7 @@ Block.Search.Item = new Class({
loadingMask: function(){
var self = this;
self.mask = new Element('span.mask').inject(self.el).fade('hide')
self.mask = new Element('div.mask').inject(self.el).fade('hide')
createSpinner(self.mask)
self.mask.fade('in')

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

@@ -1,4 +1,5 @@
from couchpotato.core.plugins.renamer.main import Renamer
import os
def start():
return Renamer()
@@ -12,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',
@@ -111,6 +112,24 @@ 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',
'label': 'NTFS Permission',
'type': 'bool',
'hidden': os.name != 'nt',
'description': 'Set permission of moved files to that of destination folder (Windows NTFS only).',
'default': False,
},
],
}, {
'tab': 'renamer',

View File

@@ -2,22 +2,23 @@ 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
import re
import shutil
import time
import traceback
log = CPLog(__name__)
class Renamer(Plugin):
renaming_started = False
@@ -26,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)
@@ -34,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
@@ -59,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')
@@ -78,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:
@@ -170,15 +217,15 @@ class Renamer(Plugin):
replacements['cd_nr'] = cd if multiple else ''
# Naming
final_folder_name = self.doReplace(folder_name, replacements)
final_file_name = self.doReplace(file_name, replacements)
final_folder_name = self.doReplace(folder_name, replacements).lstrip('. ')
final_file_name = self.doReplace(file_name, replacements).lstrip('. ')
replacements['filename'] = final_file_name[:-(len(getExt(final_file_name)) + 1)]
# Meta naming
if file_type is 'trailer':
final_file_name = self.doReplace(trailer_name, replacements, remove_multiple = True)
final_file_name = self.doReplace(trailer_name, replacements, remove_multiple = True).lstrip('. ')
elif file_type is 'nfo':
final_file_name = self.doReplace(nfo_name, replacements, remove_multiple = True)
final_file_name = self.doReplace(nfo_name, replacements, remove_multiple = True).lstrip('. ')
# Seperator replace
if separator:
@@ -275,6 +322,7 @@ class Renamer(Plugin):
for profile_type in movie.profile.types:
if profile_type.quality_id == group['meta_data']['quality']['id'] and profile_type.finish:
movie.status_id = done_status.get('id')
movie.last_edit = int(time.time())
db.commit()
except Exception, e:
log.error('Failed marking movie finished: %s %s', (e, traceback.format_exc()))
@@ -302,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
@@ -316,12 +364,15 @@ class Renamer(Plugin):
log.debug('Marking release as downloaded')
try:
release.status_id = downloaded_status.get('id')
release.last_edit = int(time.time())
except Exception, e:
log.error('Failed marking release as finished: %s %s', (e, traceback.format_exc()))
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)
@@ -346,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:
@@ -371,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)
@@ -422,39 +476,45 @@ 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'))
if os.name == 'nt' and self.conf('ntfs_permission'):
os.popen('icacls "' + dest + '"* /reset /T')
except:
log.error('Failed setting permissions for file: %s, %s', (dest, traceback.format_exc(1)))
@@ -468,7 +528,7 @@ class Renamer(Plugin):
except:
log.error('Couldn\'t move file "%s" to "%s": %s', (old, dest, traceback.format_exc()))
raise Exception
raise
return True
@@ -518,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()
@@ -554,6 +612,7 @@ class Renamer(Plugin):
if rel.movie.status_id == done_status.get('id'):
log.debug('Found a completed movie with a snatched release : %s. Setting release status to ignored...' , default_title)
rel.status_id = ignored_status.get('id')
rel.last_edit = int(time.time())
db.commit()
continue
@@ -564,8 +623,16 @@ class Renamer(Plugin):
found = False
for item in statuses:
if item['name'] == nzbname 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))
@@ -573,15 +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')
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
@@ -598,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
@@ -23,7 +24,7 @@ class Scanner(Plugin):
'media': 314572800, # 300MB
'trailer': 1048576, # 1MB
}
ignored_in_path = ['extracting', '_unpack', '_failed_', '_unknown_', '_exists_', '_failed_remove_', '_failed_rename_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files
ignored_in_path = [os.path.sep + 'extracted' + os.path.sep, 'extracting', '_unpack', '_failed_', '_unknown_', '_exists_', '_failed_remove_', '_failed_rename_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files
ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate']
extensions = {
'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img', 'mdf', 'ts', 'm4v'],
@@ -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

@@ -30,6 +30,7 @@ class Searcher(Plugin):
addEvent('searcher.correct_movie', self.correctMovie)
addEvent('searcher.download', self.download)
addEvent('searcher.try_next_release', self.tryNextRelease)
addEvent('searcher.could_be_released', self.couldBeReleased)
addApiView('searcher.try_next', self.tryNextReleaseView, docs = {
'desc': 'Marks the snatched results as ignored and try the next best release',
@@ -49,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):
@@ -140,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 = []
@@ -156,7 +161,7 @@ class Searcher(Plugin):
ret = False
for quality_type in movie['profile']['types']:
if not self.couldBeReleased(quality_type['quality']['identifier'], release_dates, pre_releases):
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
@@ -208,6 +213,7 @@ class Searcher(Plugin):
db.add(rls)
else:
[db.delete(old_info) for old_info in rls.info]
rls.last_edit = int(time.time())
db.commit()
@@ -283,16 +289,28 @@ 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()
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
if rls:
rls.status_id = snatched_status.get('id')
renamer_enabled = Env.setting('enabled', 'renamer')
done_status = fireEvent('status.get', 'done', single = True)
rls.status_id = done_status.get('id') if not renamer_enabled else snatched_status.get('id')
# Save download-id info if returned
if isinstance(download_result, dict):
for key in download_result:
rls_info = ReleaseInfo(
identifier = 'download_%s' % key,
value = toUnicode(download_result.get(key))
)
rls.info.append(rls_info)
db.commit()
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
@@ -300,33 +318,35 @@ class Searcher(Plugin):
log.info(snatch_message)
fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict())
# If renamer isn't used, mark movie done
if not Env.setting('enabled', 'renamer'):
active_status = fireEvent('status.get', 'active', single = True)
done_status = fireEvent('status.get', 'done', single = True)
try:
if movie['status_id'] == active_status.get('id'):
for profile_type in movie['profile']['types']:
if rls and profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
log.info('Renamer disabled, marking movie as finished: %s', log_movie)
# If renamer isn't used, mark movie done
if not renamer_enabled:
active_status = fireEvent('status.get', 'active', single = True)
done_status = fireEvent('status.get', 'done', single = True)
try:
if movie['status_id'] == active_status.get('id'):
for profile_type in movie['profile']['types']:
if profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
log.info('Renamer disabled, marking movie as finished: %s', log_movie)
# Mark release done
rls.status_id = done_status.get('id')
db.commit()
# Mark release done
rls.status_id = done_status.get('id')
rls.last_edit = int(time.time())
db.commit()
# Mark movie done
mvie = db.query(Movie).filter_by(id = movie['id']).first()
mvie.status_id = done_status.get('id')
db.commit()
except:
log.error('Failed marking movie finished, renamer disabled: %s', traceback.format_exc())
# Mark movie done
mvie = db.query(Movie).filter_by(id = movie['id']).first()
mvie.status_id = done_status.get('id')
mvie.last_edit = int(time.time())
db.commit()
except:
log.error('Failed marking movie finished, renamer disabled: %s', traceback.format_exc())
except:
log.error('Failed marking movie finished: %s', traceback.format_exc())
return True
log.info('Tried to download, but none of the "%s" downloaders are enabled', (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
@@ -350,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')
@@ -363,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, '&')
@@ -374,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
@@ -526,7 +552,7 @@ class Searcher(Plugin):
return False
def couldBeReleased(self, wanted_quality, dates, pre_releases):
def couldBeReleased(self, is_pre_release, dates):
now = int(time.time())
@@ -538,7 +564,7 @@ class Searcher(Plugin):
if dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0:
return True
if wanted_quality in pre_releases:
if is_pre_release:
# Prerelease 1 week before theaters
if dates.get('theater') - 604800 < now:
return True

View File

@@ -23,14 +23,16 @@ class StatusPlugin(Plugin):
'deleted': 'Deleted',
'ignored': 'Ignored',
'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',
@@ -66,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

@@ -1,6 +1,22 @@
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.plugins.base import Plugin
class Suggestion(Plugin):
pass
def __init__(self):
addApiView('suggestion.view', self.getView)
def getView(self):
limit_offset = getParam('limit_offset', None)
total_movies, movies = fireEvent('movie.list', status = 'suggest', limit_offset = limit_offset, single = True)
return jsonified({
'success': True,
'empty': len(movies) == 0,
'total': total_movies,
'movies': movies,
})

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',
@@ -82,7 +67,7 @@ Page.Wizard = new Class({
'target': self.el
},
'onComplete': function(){
window.location = App.createUrl();
window.location = App.createUrl('wanted');
}
});
}
@@ -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,28 @@
from .main import Goodfilms
def start():
return Goodfilms()
config = [{
'name': 'goodfilms',
'groups': [
{
'tab': 'automation',
'list': 'watchlist_providers',
'name': 'goodfilms_automation',
'label': 'Goodfilms',
'description': 'import movies from your <a href="http://goodfil.ms">Goodfilms</a> queue',
'options': [
{
'name': 'automation_enabled',
'default': False,
'type': 'enabler',
},
{
'name': 'automation_username',
'label': 'Username',
},
],
},
],
}]

View File

@@ -0,0 +1,36 @@
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation
from bs4 import BeautifulSoup
log = CPLog(__name__)
class Goodfilms(Automation):
url = 'http://goodfil.ms/%s/queue'
def getIMDBids(self):
if not self.conf('automation_username'):
log.error('Please fill in your username')
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):
url = self.url % self.conf('automation_username')
soup = BeautifulSoup(self.getHTMLData(url))
movies = []
for movie in soup.find_all('div', attrs = { 'class': 'movie', 'data-film-title': True }):
movies.append({ 'title': movie['data-film-title'], 'year': movie['data-film-year'] })
return movies

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

@@ -46,7 +46,8 @@ class Provider(Plugin):
def getJsonData(self, url, **kwargs):
data = self.getCache(md5(url), url, **kwargs)
cache_key = '%s%s' % (md5(url), md5('%s' % kwargs.get('params', {})))
data = self.getCache(cache_key, url, **kwargs)
if data:
try:
@@ -58,7 +59,8 @@ class Provider(Plugin):
def getRSSData(self, url, item_path = 'channel/item', **kwargs):
data = self.getCache(md5(url), url, **kwargs)
cache_key = '%s%s' % (md5(url), md5('%s' % kwargs.get('params', {})))
data = self.getCache(cache_key, url, **kwargs)
if data:
try:
@@ -70,7 +72,9 @@ class Provider(Plugin):
return []
def getHTMLData(self, url, **kwargs):
return self.getCache(md5(url), url, **kwargs)
cache_key = '%s%s' % (md5(url), md5('%s' % kwargs.get('params', {})))
return self.getCache(cache_key, url, **kwargs)
class YarrProvider(Provider):
@@ -100,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,12 +2,12 @@ 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 flask.helpers import json
from couchpotato.environment import Env
import time
import traceback
log = CPLog(__name__)
@@ -17,70 +17,78 @@ class CouchPotatoApi(MovieProvider):
urls = {
'search': 'https://couchpota.to/api/search/%s/',
'info': 'https://couchpota.to/api/info/%s/',
'is_movie': 'https://couchpota.to/api/ismovie/%s/',
'eta': 'https://couchpota.to/api/eta/%s/',
'suggest': 'https://couchpota.to/api/suggest/%s/%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())
cache_key = 'cpapi.cache.%s' % q
cached = self.getCache(cache_key, self.urls['search'] % tryUrlencode(q), headers = self.getRequestHeaders())
def isMovie(self, identifier = None):
if cached:
try:
movies = json.loads(cached)
return movies
except:
log.error('Failed parsing search results: %s', traceback.format_exc())
if not identifier:
return
return []
data = self.getJsonData(self.urls['is_movie'] % identifier, headers = self.getRequestHeaders())
if data:
return data.get('is_movie', True)
return True
def getInfo(self, identifier = None):
if not identifier:
return
cache_key = 'cpapi.cache.info.%s' % identifier
cached = self.getCache(cache_key, self.urls['info'] % identifier, headers = self.getRequestHeaders())
if cached:
try:
movie = json.loads(cached)
return movie
except:
log.error('Failed parsing info results: %s', traceback.format_exc())
result = self.getJsonData(self.urls['info'] % identifier, headers = self.getRequestHeaders())
if result: return result
return {}
def getReleaseDate(self, identifier = None):
if identifier is None: return {}
try:
data = self.urlopen(self.urls['eta'] % identifier, headers = self.getRequestHeaders())
dates = json.loads(data)
log.debug('Found ETA for %s: %s', (identifier, dates))
return dates
except Exception, e:
log.error('Error getting ETA for %s: %s', (identifier, e))
return {}
dates = self.getJsonData(self.urls['eta'] % identifier, headers = self.getRequestHeaders())
log.debug('Found ETA for %s: %s', (identifier, dates))
return dates
def suggest(self, movies = [], ignore = []):
try:
data = self.urlopen(self.urls['suggest'] % (','.join(movies), ','.join(ignore)))
suggestions = json.loads(data)
log.info('Found Suggestions for %s', (suggestions))
except Exception, e:
log.error('Error getting suggestions for %s: %s', (movies, e))
suggestions = self.getJsonData(self.urls['suggest'], params = {
'movies': ','.join(movies),
#'ignore': ','.join(ignore),
})
log.info('Found Suggestions for %s', (suggestions))
return suggestions
@@ -108,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
@@ -69,13 +69,14 @@ class Newznab(NZBProvider, RSS):
results.append({
'id': nzb_id,
'provider_extra': host['host'],
'provider_extra': urlparse(host['host']).hostname or host['host'],
'name': self.getTextElement(nzb, 'title'),
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024,
'url': (self.getUrl(host['host'], self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host),
'detail_url': '%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

@@ -0,0 +1,48 @@
from .main import IPTorrents
def start():
return IPTorrents()
config = [{
'name': 'iptorrents',
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'list': 'torrent_providers',
'name': 'IPTorrents',
'description': 'See <a href="http://www.iptorrents.com">IPTorrents</a>',
'wizard': True,
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
{
'name': 'username',
'default': '',
},
{
'name': 'password',
'default': '',
'type': 'password',
},
{
'name': 'freeleech',
'default': 0,
'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

@@ -0,0 +1,83 @@
from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider
import traceback
log = CPLog(__name__)
class IPTorrents(TorrentProvider):
urls = {
'test' : 'http://www.iptorrents.com/',
'base_url' : 'http://www.iptorrents.com',
'login' : 'http://www.iptorrents.com/torrents/',
'search' : 'http://www.iptorrents.com/torrents/?l%d=1%s&q=%s&qf=ti',
}
cat_ids = [
([48], ['720p', '1080p', 'bd50']),
([72], ['cam', 'ts', 'tc', 'r5', 'scr']),
([7], ['dvdrip', 'brrip']),
([6], ['dvdr']),
]
http_time_between_calls = 1 #seconds
cat_backup_id = None
def _searchOnTitle(self, title, movie, quality, results):
freeleech = '' if not self.conf('freeleech') else '&free=on'
url = self.urls['search'] % (self.getCatId(quality['identifier'])[0], freeleech, tryUrlencode('%s %s' % (title.replace(':', ''), movie['library']['year'])))
data = self.getHTMLData(url, opener = self.login_opener)
if data:
html = BeautifulSoup(data)
try:
result_table = html.find('table', attrs = {'class' : 'torrents'})
if not result_table or 'nothing found!' in data.lower():
return
entries = result_table.find_all('tr')
for result in entries[1:]:
torrent = result.find_all('td')[1].find('a')
torrent_id = torrent['href'].replace('/details.php?id=', '')
torrent_name = torrent.string
torrent_download_url = self.urls['base_url'] + (result.find_all('td')[3].find('a'))['href'].replace(' ', '.')
torrent_details_url = self.urls['base_url'] + torrent['href']
torrent_size = self.parseSize(result.find_all('td')[5].string)
torrent_seeders = tryInt(result.find('td', attrs = {'class' : 'ac t_seeders'}).string)
torrent_leechers = tryInt(result.find('td', attrs = {'class' : 'ac t_leechers'}).string)
results.append({
'id': torrent_id,
'name': torrent_name,
'url': torrent_download_url,
'detail_url': torrent_details_url,
'download': self.loginDownload,
'size': torrent_size,
'seeders': torrent_seeders,
'leechers': torrent_leechers,
})
except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
def loginSuccess(self, output):
return 'don\'t have an account' not in output.lower()
def getLoginParams(self):
return tryUrlencode({
'username': self.conf('username'),
'password': self.conf('password'),
'login': 'submit',
})

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.',
}
],
}

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