Compare commits

...

180 Commits

Author SHA1 Message Date
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
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
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
1cd8040692 One up 2013-03-17 16:39:09 +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 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
89bff73431 Decode torrent hash for magnets also 2013-03-09 18:15:06 +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
eed0382b41 Host to 0.0.0.0 2013-02-14 23:01:34 +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
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
9e260a89af One up 2013-01-26 14:51:39 +01:00
Ruud
d233e4d22e Merge branch 'refs/heads/develop' into desktop 2013-01-26 13:54:56 +01:00
Ruud
a60e9dc4c3 Encode nzbname before sending it to NZBGet. fix #1321 2013-01-25 21:29:25 +01:00
Ruud
b168c1364d Blackhole error on manual download. fix #1351 2013-01-25 21:08:19 +01:00
Ruud
23893dbcb9 Merge branch 'refs/heads/develop' into desktop 2013-01-25 20:13:58 +01:00
Ruud
14fffda3ff Don't add signal handlers when daemonized. fix #1346 2013-01-25 15:26:06 +01:00
Ruud
51364a3c25 Transmission params not set properly. fix #1344 2013-01-25 13:12:10 +01:00
Ruud
c6642ffeb7 Allow 1080p in brrip releases. fixes #1339 2013-01-25 12:57:41 +01:00
Ruud
9fe9ccf0ad Open browser didn't work. 2013-01-24 23:51:11 +01:00
Ruud
cb92b00534 Manage setting instead of getting folders. fix #1307 2013-01-24 23:33:48 +01:00
Ruud
7d3780133f Missing download function. fix #1337 2013-01-24 23:02:36 +01:00
Ruud
f23b9d7cb9 Use host from config again. fix #1329 2013-01-24 22:56:33 +01:00
Ruud
506871b506 One up 2013-01-23 23:10:55 +01:00
Ruud
6115917660 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2013-01-23 22:57:07 +01:00
Ruud
44b78f8d2f Version up 2013-01-23 22:56:23 +01:00
Ruud
21df8819d3 Merge branch 'refs/heads/develop' into desktop 2013-01-23 22:55:09 +01:00
Ruud
cad9bfae9f Transmission: Don't use ratio when not filled in. 2013-01-23 22:54:02 +01:00
Ruud
749075b4cb Setting cleanup 2013-01-23 22:50:31 +01:00
Ruud
0456a1e820 Typo 2013-01-23 22:29:02 +01:00
ikkemaniac
35a9739ec5 Improve debugging for email notifications 2013-01-23 22:26:09 +01:00
ikkemaniac
2a451c255e Rename var for naming consistency 2013-01-23 22:26:09 +01:00
Ruud
7c38ad1c00 Remove non-int backup folders. closes #1298 2013-01-23 22:24:09 +01:00
Ruud
647159e549 Fix and cleanup wizard. fix #1324 2013-01-23 22:16:04 +01:00
Ruud
7cc55c21b6 Shutdown cleanly on quit process 2013-01-22 23:43:15 +01:00
Ruud
89c38f5aa4 Use host from config again. fix #1278 2013-01-22 23:24:09 +01:00
Ruud
5f428649c3 Writing larger files fails on Windows. fix #1281 2013-01-22 23:20:55 +01:00
Ruud
8ed2a99830 Don't try to parse None on IMDB watchlist 2013-01-22 22:55:06 +01:00
Ruud
1a89d551dc Contribute update 2013-01-22 22:41:17 +01:00
Ruud
9d633910f6 NZBsRus fixes 2013-01-22 22:35:44 +01:00
Ruud
54ea22e9b6 Only remove available status releases when refreshing 2013-01-22 22:02:23 +01:00
Ruud
fb3f3e11f6 Merge branch 'refs/heads/develop' into desktop 2013-01-22 21:40:40 +01:00
Ruud
f84b23eecc Only fire enabled downloader failed event 2013-01-22 21:35:11 +01:00
Ruud
6ea045ddd3 Remove failed wrong parameters 2013-01-21 22:37:58 +01:00
Ruud
f8b4e75b74 If data is empty, assume correct type for downloaders 2013-01-20 21:11:36 +01:00
Ruud
faaf351662 Group providers together 2013-01-20 20:50:50 +01:00
Ruud
f41fc794c1 Don't search full disk when no manage folders are filled. fix #1304 2013-01-19 01:30:56 +01:00
Ruud
0f789b5b40 NZBsRus rss different item path. fix #1301 2013-01-19 01:17:57 +01:00
Ruud
d2496d768d Don't reorder based on omdb 2013-01-19 01:04:24 +01:00
Ruud
b93488f025 Use default timeout for CP calls 2013-01-19 00:56:03 +01:00
Ruud
d4de68ef86 Add page nr after 2013-01-19 00:51:20 +01:00
Ruud
61a0bb8ec6 Don't use quality identifier in title searches 2013-01-19 00:45:08 +01:00
Ruud
fe52ac7203 Use default title as email subject 2013-01-16 19:45:39 +01:00
Cybertinus
4447b7611e Added the e-mail notifier 2013-01-15 21:15:17 +01:00
Ruud
178c8942c3 Merge branch 'refs/heads/develop' into desktop 2013-01-14 19:54:22 +01:00
Ruud
4fe9f9e42f Log subtitle search 2013-01-13 10:08:59 +01:00
Ruud
71b22345bc Make sure downloaders and providers match
Remove releases not found anymore on new searches
2013-01-12 16:07:11 +01:00
Ruud
a0dc5c075a Remove print 2013-01-12 16:05:54 +01:00
Ruud
a264c75f8c Remove releases that aren't found in latest search 2013-01-11 22:31:19 +01:00
Ruud
fcc8a71eae NZBget version check 2013-01-11 21:45:25 +01:00
Ruud
cdd681ad48 XBMC icon image 2013-01-11 21:02:22 +01:00
Ruud
36e5c49147 TorrentDay: decoding error. fix #1260 2013-01-11 20:50:38 +01:00
Ruud
300f4738a0 Randomize wanted search. fix #1261 2013-01-11 20:12:12 +01:00
Ruud
9447833653 RT cleanup 2013-01-10 07:42:15 +01:00
Ruud
df53d0c578 Rename RT folder 2013-01-10 07:29:48 +01:00
Kris Kater
17eaba3e2a Added rottentomatoes automation 2013-01-10 07:28:39 +01:00
Ruud
0f389f18cb NZBGet: Don't use priority when set to normal. 2013-01-09 23:41:47 +01:00
Prinz23
28ce083f48 nzbget priority support 2013-01-09 23:34:03 +01:00
Ruud
cfaffe2bcb SSL support 2013-01-09 23:29:54 +01:00
Ruud
432852cf5d Enable added combined list by default 2013-01-09 21:20:27 +01:00
Ruud
3c728608e9 FTDWorld use simple login
Add size
2013-01-09 20:42:33 +01:00
Ruud
8892ace3c2 OMGWTF proper link in settings 2013-01-09 20:26:02 +01:00
Ruud
87574a1810 Allow spotweb url without rewriterule. fix #1248 2013-01-08 20:28:59 +01:00
Ruud
14e0219e62 Urlencode spotweb id. fix #1213 2013-01-07 23:11:45 +01:00
Ruud
51e747049d One up 2013-01-07 23:10:42 +01:00
Ruud
0582f7d694 Urlencode spotweb id. fix #1213 2013-01-07 23:10:06 +01:00
Ruud
fa7cac7538 Merge branch 'refs/heads/develop' into desktop 2013-01-07 22:41:55 +01:00
Ruud
ec857a9b3d FTDWorld: Check for login success 2013-01-07 22:31:42 +01:00
Ruud
4d32b0b16d Use FTDWorld temp api. closes #1243 2013-01-07 22:21:44 +01:00
Ruud
ca08287cff Ignore Growl timeout. fixes #1240 2013-01-07 20:54:21 +01:00
Ruud
36fee69843 XBMC notifier for Frodo & Eden 2013-01-06 23:06:38 +01:00
ikkemaniac
c5cae5ab9b add XBMC v11 Eden notifications support
This is my approach on working with Eden, maybe a little late since Frodo is almost released, but better late then never.

- First detect the JSON-RPC version XBMC is running (once per boot of CouchPotatoServer on the first notification, except for sending test message then the JSON version is always checked).
- Set a variable indicating whether or not to use JSON (or normal http).
- If JSON should be used, proceed as before this commit.
- If normal-http should be used, use 'notifyXBMCnoJSON' func
- 'notifyXBMCnoJSON' just opens a specific XBMC api url, unfortunately importing urllib for this was necessary to escape the message strings.

TODO: support multiple XBMC hosts, right now the last host in the hosts array will set the 'useJSONnotifications' var.

Conflicts:

	couchpotato/core/notifications/xbmc/main.py

Conflicts:
	couchpotato/core/notifications/xbmc/main.py
2013-01-06 22:52:59 +01:00
ikkemaniac
9bd5688fb9 Remove services that are not required for couchpotato to run 2013-01-06 22:35:35 +01:00
ikkemaniac
1993c2b6cb Redo FreeBSD init script completely.
Use rc.subr functions and proper rc.conf variables.
2013-01-06 22:35:35 +01:00
ikkemaniac
acc8ed2092 Acutally use config_file variable 2013-01-06 22:35:35 +01:00
ikkemaniac
7b4924dd7a Don't influence the PATH variable in FreeBSD rc script
Don't prepend the PATH variable, it's ugly, unwanted and unnecessary. Call binaries with their full path.
2013-01-06 22:35:35 +01:00
ikkemaniac
3a2861f72a fix FreeBSD init script
-add actual start command
-fix verify_couchpotato_pid function, 'ps' command failed if PID var was empty
-fix verify_couchpotato_pid usage, acutally use the return of verify_couchpotato_pid in the 'stop' routine
2013-01-06 22:35:35 +01:00
Ruud
4779265b43 Change xbmc description 2013-01-06 11:52:05 +01:00
ikkemaniac
f8a46ebe6d clearly state XBMC version dependency for notifications 2013-01-06 11:33:53 +01:00
ikkemaniac
383ec7e6f5 check for XBMC JSON-RPC version and improve logging info 2013-01-06 11:33:53 +01:00
Ruud
dd9118292d Newznab log error 2013-01-06 11:20:13 +01:00
Ruud
4d0f8eb4ac Default add top25 to itunes automation 2013-01-04 20:26:36 +01:00
Ruud
637b21cc68 iTunes automation cleanup 2013-01-04 20:20:52 +01:00
Joseph Gardner
da429f0cb8 Adding itunes automation provider 2013-01-04 20:15:30 +01:00
Ruud
41c2845328 Toasty cleanup 2013-01-04 20:14:25 +01:00
Travis La Marr
c2453bb070 Added Windows Phone SuperToasty Notifier 2013-01-04 19:05:58 +01:00
Ruud
a3a2c8da8e Typo 2013-01-04 19:03:36 +01:00
Ruud
a1d4bab793 NZBVortex: Delete failed option 2013-01-02 14:11:40 +01:00
Ruud
d314a9b5b3 Also check status on manual 2013-01-02 14:11:11 +01:00
Ruud
9a60f6001a Check snatched on startup 2013-01-02 14:10:41 +01:00
Ruud
96a39dbf60 Link to downloaders 2013-01-02 13:52:59 +01:00
Ruud
015675750c Properly use imdb_results 2013-01-02 13:43:24 +01:00
Ruud
bf4dc62f54 NZBVortex support. closes #1204 2013-01-02 13:31:14 +01:00
Ruud
c2382ade05 Use provider for downloader 2013-01-02 13:29:44 +01:00
Ruud
2f65545086 Extend opener with multipart 2013-01-02 13:29:28 +01:00
Ruud
3aea2cd968 Simpler CP tag regex 2013-01-02 13:29:08 +01:00
Ruud
f30cb9185c Add nzbgeek to the defaults of newznab 2013-01-02 10:40:08 +01:00
Ruud
615468e8e6 Make nzbget password a password field. closes #1205 2012-12-31 21:31:11 +01:00
Ruud
0cbee01024 Don't use unicode when not needed in urlopen 2012-12-31 13:10:11 +01:00
Ruud
c29cb39797 Automation cleanup 2012-12-30 21:43:13 +01:00
Kris Kater
580ff38136 Added moviemeter.nl automation 2012-12-30 20:51:47 +01:00
Sander Boele
6b8bca5491 added path to the freebsd init script 2012-12-30 20:51:24 +01:00
Ruud
e92b5d95ca IOLoop cleanup 2012-12-30 18:40:38 +01:00
Ruud
611a32d110 Add randomstring to each internal api 2012-12-30 18:39:16 +01:00
Ruud
74e4b015a9 Module update: Tornade 2012-12-30 18:38:52 +01:00
Ruud
1e0267cdb5 Change OMGWTF to .org 2012-12-30 11:21:23 +01:00
Ruud
041a206fb4 Rename to OMDBapi 2012-12-29 23:45:21 +01:00
Ruud
12a4d6a995 Send proper user-agent with nzbx.co 2012-12-29 21:23:15 +01:00
Ruud
b14a6c1e63 nzbx description 2012-12-29 20:22:42 +01:00
spion06
7fa08ef9b6 Update init/freebsd to not use perl
When using couchpotato on slimmed down versions of freebsd (freenas for example) sometimes perl is not available. Since the previous parsing of the INI required a "key = value" format it is pretty simple to use awk for this.
2012-12-29 19:24:12 +01:00
203 changed files with 6123 additions and 2989 deletions

View File

@@ -62,7 +62,6 @@ class Loader(object):
self.log.logger.addHandler(hdlr)
def addSignals(self):
signal.signal(signal.SIGINT, self.onExit)
signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1))
@@ -74,7 +73,7 @@ class Loader(object):
def onExit(self, signal, frame):
from couchpotato.core.event import fireEvent
fireEvent('app.crappy_shutdown', single = True)
fireEvent('app.shutdown', single = True)
def run(self):

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

@@ -5,9 +5,10 @@
* Search through the existing (and closed) issues first. See if you can get your answer there.
* Double check the result manually, because it could be an external issue.
* Post logs! Without seeing what is going on, I can't reproduce the error.
* What are you settings for the specific problem
* What providers are you using. (While your logs include these, scanning through hundred of lines of log isn't my hobby)
* Give me a short step by step of how to reproduce
* What is the movie + quality you are searching for.
* What are you settings for the specific problem.
* What providers are you using. (While your logs include these, scanning through hundred of lines of log isn't my hobby).
* Give me a short step by step of how to reproduce.
* What hardware / OS are you using and what are the limits? NAS can be slow and maybe have a different python installed then when you use CP on OSX or Windows for example.
* I will mark issues with the "can't reproduce" tag. Don't go asking me "why closed" if it clearly says the issue in the tag ;)

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

@@ -1,6 +1,5 @@
from flask.blueprints import Blueprint
from flask.helpers import url_for
from tornado.ioloop import IOLoop
from tornado.web import RequestHandler, asynchronous
from werkzeug.utils import redirect
@@ -11,7 +10,11 @@ api_nonblock = {}
class NonBlockHandler(RequestHandler):
stoppers = []
def __init__(self, application, request, **kwargs):
cls = NonBlockHandler
cls.stoppers = []
super(NonBlockHandler, self).__init__(application, request, **kwargs)
@asynchronous
def get(self, route):

View File

@@ -23,20 +23,22 @@ config = [{
'default': '',
'type': 'password',
},
{
'name': 'host',
'advanced': True,
'default': '0.0.0.0',
'hidden': True,
'label': 'IP',
'description': 'Host that I should listen to. "0.0.0.0" listens to all ips.',
},
{
'name': 'port',
'default': 5050,
'type': 'int',
'description': 'The port I should listen to.',
},
{
'name': 'ssl_cert',
'description': 'Path to SSL server.crt',
'advanced': True,
},
{
'name': 'ssl_key',
'description': 'Path to SSL server.key',
'advanced': True,
},
{
'name': 'launch_browser',
'default': True,

View File

@@ -152,7 +152,7 @@ class Core(Plugin):
def createBaseUrl(self):
host = Env.setting('host')
if host == '0.0.0.0':
if host == '0.0.0.0' or host == '':
host = 'localhost'
port = Env.setting('port')
@@ -176,8 +176,10 @@ class Core(Plugin):
})
def signalHandler(self):
if Env.get('daemonized'): return
def signal_handler(signal, frame):
fireEvent('app.do_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.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
from minify.cssmin import cssmin
from minify.jsmin import jsmin
import 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/prefix_free.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,66 @@ class ClientScript(Plugin):
addEvent('clientscript.get_styles', self.getStyles)
addEvent('clientscript.get_scripts', self.getScripts)
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 = cssmin(f)
data = data.replace('../images/', '../static/images/')
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, 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 +140,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

@@ -2,7 +2,6 @@ from apscheduler.scheduler import Scheduler as Sched
from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
import logging
log = CPLog(__name__)

View File

@@ -0,0 +1,13 @@
config = {
'name': 'download_providers',
'groups': [
{
'label': 'Downloaders',
'description': 'You can select different downloaders for each type (usenet / torrent)',
'type': 'list',
'name': 'download_providers',
'tab': 'downloaders',
'options': [],
},
],
}

View File

@@ -1,16 +1,17 @@
from base64 import b32decode, b16encode
from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.providers.base import Provider
import random
import re
log = CPLog(__name__)
class Downloader(Plugin):
class Downloader(Provider):
type = []
http_time_between_calls = 0
torrent_sources = [
'http://torrage.com/torrent/%s.torrent',
@@ -32,18 +33,44 @@ class Downloader(Plugin):
]
def __init__(self):
addEvent('download', self.download)
addEvent('download.status', self.getAllDownloadStatus)
addEvent('download.remove_failed', self.removeFailed)
addEvent('download', self._download)
addEvent('download.enabled', self._isEnabled)
addEvent('download.enabled_types', self.getEnabledDownloadType)
addEvent('download.status', self._getAllDownloadStatus)
addEvent('download.remove_failed', self._removeFailed)
def download(self, data = {}, movie = {}, manual = False, filedata = None):
pass
def getEnabledDownloadType(self):
for download_type in self.type:
if self.isEnabled(manual = True, data = {'type': download_type}):
return self.type
return []
def _download(self, data = {}, movie = {}, manual = False, filedata = None):
if self.isDisabled(manual, data):
return
return self.download(data = data, movie = movie, filedata = filedata)
def _getAllDownloadStatus(self):
if self.isDisabled(manual = True, data = {}):
return
return self.getAllDownloadStatus()
def getAllDownloadStatus(self):
return
def _removeFailed(self, item):
if self.isDisabled(manual = True, data = {}):
return
if self.conf('delete_failed', default = True):
return self.removeFailed(item)
return False
def removeFailed(self, name = {}, nzo_id = {}):
return False
def removeFailed(self, item):
return
def isCorrectType(self, item_type):
is_correct = item_type in self.type
@@ -76,9 +103,16 @@ class Downloader(Plugin):
log.error('Failed converting magnet url to torrent: %s', (torrent_hash))
return False
def isDisabled(self, manual):
return not self.isEnabled(manual)
def isDisabled(self, manual, data):
return not self.isEnabled(manual, data)
def isEnabled(self, manual):
def _isEnabled(self, manual, data = {}):
if not self.isEnabled(manual, data):
return
return True
def isEnabled(self, manual, data = {}):
d_manual = self.conf('manual', default = False)
return super(Downloader, self).isEnabled() and ((d_manual and manual) or (d_manual is False))
return super(Downloader, self).isEnabled() and \
((d_manual and manual) or (d_manual is False)) and \
(not data or self.isCorrectType(data.get('type')))

View File

@@ -10,6 +10,7 @@ config = [{
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'blackhole',
'label': 'Black hole',
'description': 'Download the NZB/Torrent to a specific folder.',

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
@@ -10,11 +11,7 @@ class Blackhole(Downloader):
type = ['nzb', 'torrent', 'torrent_magnet']
def download(self, data = {}, movie = {}, manual = False, filedata = None):
if self.isDisabled(manual) or \
(not self.isCorrectType(data.get('type')) or \
(not self.conf('use_for') in ['both', 'torrent' if 'torrent' in data.get('type') else data.get('type')])):
return
def download(self, data = {}, movie = {}, filedata = None):
directory = self.conf('directory')
if not directory or not os.path.isdir(directory):
@@ -40,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)
@@ -52,4 +50,23 @@ class Blackhole(Downloader):
except:
log.info('Failed to download file %s: %s', (data.get('name'), traceback.format_exc()))
return False
return False
def getEnabledDownloadType(self):
if self.conf('use_for') == 'both':
return super(Blackhole, self).getEnabledDownloadType()
elif self.conf('use_for') == 'torrent':
return ['torrent', 'torrent_magnet']
else:
return ['nzb']
def isEnabled(self, manual, data = {}):
for_type = ['both']
if data and 'torrent' in data.get('type'):
for_type.append('torrent')
elif data:
for_type.append(data.get('type'))
return super(Blackhole, self).isEnabled(manual, data) and \
((self.conf('use_for') in for_type))

View File

@@ -8,9 +8,10 @@ config = [{
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'nzbget',
'label': 'NZBGet',
'description': 'Send NZBs to your NZBGet installation.',
'description': 'Use <a href="http://nzbget.sourceforge.net/Main_Page" target="_blank">NZBGet</a> to download NZBs.',
'options': [
{
'name': 'enabled',
@@ -25,6 +26,7 @@ config = [{
},
{
'name': 'password',
'type': 'password',
'description': 'Default NZBGet password is <i>tegbzn6789</i>',
},
{
@@ -32,6 +34,13 @@ config = [{
'default': 'Movies',
'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
},
{
'name': 'priority',
'default': '0',
'type': 'dropdown',
'values': [('Very Low', -100), ('Low', -50), ('Normal', 0), ('High', 50), ('Very High', 100)],
'description': 'Only change this if you are using NZBget 9.0 or higher',
},
{
'name': 'manual',
'default': 0,

View File

@@ -1,7 +1,9 @@
from base64 import standard_b64encode
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from inspect import isfunction
import re
import socket
import traceback
import xmlrpclib
@@ -14,10 +16,7 @@ class NZBGet(Downloader):
url = 'http://nzbget:%(password)s@%(host)s/xmlrpc'
def download(self, data = {}, movie = {}, manual = False, filedata = None):
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
return
def download(self, data = {}, movie = {}, filedata = None):
if not filedata:
log.error('Unable to get NZB file: %s', traceback.format_exc())
@@ -26,7 +25,7 @@ class NZBGet(Downloader):
log.info('Sending "%s" to NZBGet.', data.get('name'))
url = self.url % {'host': self.conf('host'), 'password': self.conf('password')}
nzb_name = '%s.nzb' % self.createNzbName(data, movie)
nzb_name = ss('%s.nzb' % self.createNzbName(data, movie))
rpc = xmlrpclib.ServerProxy(url)
try:
@@ -44,7 +43,12 @@ class NZBGet(Downloader):
log.error('Protocol Error: %s', e)
return False
if rpc.append(nzb_name, self.conf('category'), False, standard_b64encode(filedata.strip())):
if re.search(r"^0", rpc.version()):
xml_response = rpc.append(nzb_name, self.conf('category'), False, standard_b64encode(filedata.strip()))
else:
xml_response = rpc.append(nzb_name, self.conf('category'), tryInt(self.conf('priority')), False, standard_b64encode(filedata.strip()))
if xml_response:
log.info('NZB sent successfully to NZBGet')
return True
else:

View File

@@ -0,0 +1,47 @@
from .main import NZBVortex
def start():
return NZBVortex()
config = [{
'name': 'nzbvortex',
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'nzbvortex',
'label': 'NZBVortex',
'description': 'Use <a href="http://www.nzbvortex.com/landing/" target="_blank">NZBVortex</a> to download NZBs.',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
'radio_group': 'nzb',
},
{
'name': 'host',
'default': 'https://localhost:4321',
},
{
'name': 'api_key',
'label': 'Api Key',
},
{
'name': 'manual',
'default': False,
'type': 'bool',
'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
},
{
'name': 'delete_failed',
'default': True,
'type': 'bool',
'description': 'Delete a release after the download has failed.',
},
],
}
],
}]

View File

@@ -0,0 +1,170 @@
from base64 import b64encode
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import tryUrlencode, ss
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog
from urllib2 import URLError
from uuid import uuid4
import hashlib
import httplib
import json
import socket
import ssl
import sys
import traceback
import urllib2
log = CPLog(__name__)
class NZBVortex(Downloader):
type = ['nzb']
api_level = None
session_id = None
def download(self, data = {}, movie = {}, filedata = None):
# Send the nzb
try:
nzb_filename = self.createFileName(data, filedata, movie)
self.call('nzb/add', params = {'file': (ss(nzb_filename), filedata)}, multipart = True)
return True
except:
log.error('Something went wrong sending the NZB file: %s', traceback.format_exc())
return False
def getAllDownloadStatus(self):
raw_statuses = self.call('nzb')
statuses = []
for item in raw_statuses.get('nzbs', []):
# Check status
status = 'busy'
if item['state'] == 20:
status = 'completed'
elif item['state'] in [21, 22, 24]:
status = 'failed'
statuses.append({
'id': item['id'],
'name': item['uiTitle'],
'status': status,
'original_status': item['state'],
'timeleft':-1,
})
return statuses
def removeFailed(self, item):
log.info('%s failed downloading, deleting...', item['name'])
try:
self.call('nzb/%s/cancel' % item['id'])
except:
log.error('Failed deleting: %s', traceback.format_exc(0))
return False
return True
def login(self):
nonce = self.call('auth/nonce', auth = False).get('authNonce')
cnonce = uuid4().hex
hashed = b64encode(hashlib.sha256('%s:%s:%s' % (nonce, cnonce, self.conf('api_key'))).digest())
params = {
'nonce': nonce,
'cnonce': cnonce,
'hash': hashed
}
login_data = self.call('auth/login', parameters = params, auth = False)
# Save for later
if login_data.get('loginResult') == 'successful':
self.session_id = login_data.get('sessionID')
return True
log.error('Login failed, please check you api-key')
return False
def call(self, call, parameters = {}, repeat = False, auth = True, *args, **kwargs):
# Login first
if not self.session_id and auth:
self.login()
# Always add session id to request
if self.session_id:
parameters['sessionid'] = self.session_id
params = tryUrlencode(parameters)
url = cleanHost(self.conf('host')) + 'api/' + call
url_opener = urllib2.build_opener(HTTPSHandler())
try:
data = self.urlopen('%s?%s' % (url, params), opener = url_opener, *args, **kwargs)
if data:
return json.loads(data)
except URLError, e:
if hasattr(e, 'code') and e.code == 403:
# Try login and do again
if not repeat:
self.login()
return self.call(call, parameters = parameters, repeat = True, *args, **kwargs)
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
return {}
def getApiLevel(self):
if not self.api_level:
url = cleanHost(self.conf('host')) + 'api/app/apilevel'
url_opener = urllib2.build_opener(HTTPSHandler())
try:
data = self.urlopen(url, opener = url_opener, show_error = False)
self.api_level = float(json.loads(data).get('apilevel'))
except URLError, e:
if hasattr(e, 'code') and e.code == 403:
log.error('This version of NZBVortex isn\'t supported. Please update to 2.8.6 or higher')
else:
log.error('NZBVortex doesn\'t seem to be running or maybe the remote option isn\'t enabled yet: %s', traceback.format_exc(1))
return self.api_level
def isEnabled(self, manual, data):
return super(NZBVortex, self).isEnabled(manual, data) and self.getApiLevel()
class HTTPSConnection(httplib.HTTPSConnection):
def __init__(self, *args, **kwargs):
httplib.HTTPSConnection.__init__(self, *args, **kwargs)
def connect(self):
sock = socket.create_connection((self.host, self.port), self.timeout)
if sys.version_info < (2, 6, 7):
if hasattr(self, '_tunnel_host'):
self.sock = sock
self._tunnel()
else:
if self._tunnel_host:
self.sock = sock
self._tunnel()
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version = ssl.PROTOCOL_TLSv1)
class HTTPSHandler(urllib2.HTTPSHandler):
def https_open(self, req):
return self.do_open(HTTPSConnection, req)

View File

@@ -9,9 +9,10 @@ config = [{
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'pneumatic',
'label': 'Pneumatic',
'description': 'Download the .strm file to a specific folder.',
'description': 'Use <a href="http://forum.xbmc.org/showthread.php?tid=97657" target="_blank">Pneumatic</a> to download .strm files.',
'options': [
{
'name': 'enabled',

View File

@@ -11,9 +11,7 @@ class Pneumatic(Downloader):
type = ['nzb']
strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s'
def download(self, data = {}, movie = {}, manual = False, filedata = None):
if self.isDisabled(manual) or (not self.isCorrectType(data.get('type'))):
return
def download(self, data = {}, movie = {}, filedata = None):
directory = self.conf('directory')
if not directory or not os.path.isdir(directory):

View File

@@ -8,9 +8,10 @@ config = [{
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'sabnzbd',
'label': 'Sabnzbd',
'description': 'Send NZBs to your Sabnzbd installation.',
'description': 'Use <a href="http://sabnzbd.org/" target="_blank">SABnzbd</a> to download NZBs.',
'wizard': True,
'options': [
{

View File

@@ -1,7 +1,8 @@
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import tryUrlencode
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 urllib2 import URLError
import json
import traceback
@@ -12,10 +13,7 @@ class Sabnzbd(Downloader):
type = ['nzb']
def download(self, data = {}, movie = {}, manual = False, filedata = None):
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
return
def download(self, data = {}, movie = {}, filedata = None):
log.info('Sending "%s" to SABnzbd.', data.get('name'))
@@ -41,9 +39,9 @@ class Sabnzbd(Downloader):
try:
if params.get('mode') is 'addfile':
sab = self.urlopen(url, timeout = 60, params = {'nzbfile': (nzb_filename, filedata)}, multipart = True, show_error = False)
sab = self.urlopen(url, timeout = 60, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True, show_error = False, headers = {'User-Agent': Env.getIdentifier()})
else:
sab = self.urlopen(url, timeout = 60, show_error = False)
sab = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()})
except URLError:
log.error('Failed sending release, probably wrong HOST: %s', traceback.format_exc(0))
return False
@@ -65,8 +63,6 @@ class Sabnzbd(Downloader):
return False
def getAllDownloadStatus(self):
if self.isDisabled(manual = False):
return False
log.debug('Checking SABnzbd download status.')
@@ -122,9 +118,6 @@ class Sabnzbd(Downloader):
def removeFailed(self, item):
if not self.conf('delete_failed', default = True):
return False
log.info('%s failed downloading, deleting...', item['name'])
try:
@@ -147,7 +140,7 @@ class Sabnzbd(Downloader):
'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()})
if use_json:
d = json.loads(data)
if d.get('error'):

View File

@@ -8,9 +8,10 @@ config = [{
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'synology',
'label': 'Synology',
'description': 'Send torrents to Synology\'s Download Station.',
'description': 'Use <a href="http://www.synology.com/dsm/home_home_applications_download_station.php" target="_blank">Synology Download Station</a> to download.',
'wizard': True,
'options': [
{

View File

@@ -14,10 +14,7 @@ class Synology(Downloader):
type = ['torrent_magnet']
log = CPLog(__name__)
def download(self, data, movie, manual = False, filedata = None):
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
return
def download(self, data, movie, filedata = None):
log.error('Sending "%s" (%s) to Synology.', (data.get('name'), data.get('type')))

View File

@@ -8,9 +8,10 @@ config = [{
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'transmission',
'label': 'Transmission',
'description': 'Send torrents to Transmission.',
'description': 'Use <a href="http://www.transmissionbt.com/" target="_blank">Transmission</a> to download torrents.',
'wizard': True,
'options': [
{

View File

@@ -16,10 +16,7 @@ class Transmission(Downloader):
type = ['torrent', 'torrent_magnet']
log = CPLog(__name__)
def download(self, data, movie, manual = False, filedata = None):
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
return
def download(self, data, movie, filedata = None):
log.debug('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type')))
@@ -41,10 +38,12 @@ class Transmission(Downloader):
'download-dir': folder_path
}
torrent_params = {
'seedRatioLimit': self.conf('ratio'),
'seedRatioMode': (0 if self.conf('ratio') else 1)
}
torrent_params = {}
if self.conf('ratio'):
torrent_params = {
'seedRatioLimit': self.conf('ratio'),
'seedRatioMode': self.conf('ratio')
}
if not filedata and data.get('type') == 'torrent':
log.error('Failed sending torrent, no data')
@@ -60,7 +59,8 @@ class Transmission(Downloader):
remote_torrent = trpc.add_torrent_file(b64encode(filedata), arguments = params)
# Change settings of added torrents
trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
if torrent_params:
trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
return True
except Exception, err:

View File

@@ -8,9 +8,10 @@ config = [{
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'utorrent',
'label': 'uTorrent',
'description': 'Send torrents to uTorrent.',
'description': 'Use <a href="http://www.utorrent.com/" target="_blank">uTorrent</a> to download torrents.',
'wizard': True,
'options': [
{

View File

@@ -1,11 +1,13 @@
from base64 import b16encode, b32decode
from bencode import bencode, bdecode
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import isInt
from couchpotato.core.helpers.encoding import isInt, ss
from couchpotato.core.logger import CPLog
from hashlib import sha1
from multipartpost import MultipartPostHandler
import cookielib
import httplib
import json
import re
import time
import urllib
@@ -20,10 +22,7 @@ class uTorrent(Downloader):
type = ['torrent', 'torrent_magnet']
utorrent_api = None
def download(self, data, movie, manual = False, filedata = None):
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
return
def download(self, data, movie, filedata = None):
log.debug('Sending "%s" (%s) to uTorrent.', (data.get('name'), data.get('type')))
@@ -40,6 +39,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)
@@ -48,6 +48,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:
@@ -67,6 +71,59 @@ class uTorrent(Downloader):
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 = []
# 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'
statuses.append({
'id': item[0],
'name': item[2],
'status': status,
'original_status': item[1],
'timeleft': item[10],
})
return statuses
class uTorrentAPI(object):
@@ -97,9 +154,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)
@@ -125,7 +180,7 @@ class uTorrentAPI(object):
def add_torrent_file(self, filename, filedata):
action = "action=add-file"
return self._request(action, {"torrent_file": (filename, filedata)})
return self._request(action, {"torrent_file": (ss(filename), filedata)})
def set_torrent(self, hash, params):
action = "action=setprops&hash=%s" % hash
@@ -136,3 +191,7 @@ 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)

View File

@@ -104,6 +104,8 @@ def fireEvent(name, *args, **kwargs):
# Merge
if options['merge'] and len(results) > 0:
results.reverse() # Priority 1 is higher then 100
# Dict
if isinstance(results[0], dict):
merged = {}
@@ -115,7 +117,8 @@ def fireEvent(name, *args, **kwargs):
elif isinstance(results[0], list):
merged = []
for result in results:
merged += result
if result not in merged:
merged += result
results = merged

View File

@@ -168,4 +168,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

@@ -67,6 +67,18 @@ class Loader(object):
def addFromDir(self, plugin_type, priority, module, dir_name):
# Load dir module
try:
m = __import__(module)
splitted = module.split('.')
for sub in splitted[1:]:
m = getattr(m, sub)
if hasattr(m, 'config'):
fireEvent('settings.options', splitted[-1] + '_config', getattr(m, 'config'))
except:
raise
for cur_file in glob.glob(os.path.join(dir_name, '*')):
name = os.path.basename(cur_file)
if os.path.isdir(os.path.join(dir_name, name)):

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

@@ -0,0 +1,13 @@
config = {
'name': 'notification_providers',
'groups': [
{
'label': 'Notifications',
'description': 'Notify when movies are done or snatched',
'type': 'list',
'name': 'notification_providers',
'tab': 'notifications',
'options': [],
},
],
}

View File

@@ -8,6 +8,7 @@ config = [{
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'boxcar',
'options': [
{

View File

@@ -178,11 +178,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 +194,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

@@ -0,0 +1,56 @@
from .main import Email
def start():
return Email()
config = [{
'name': 'email',
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'email',
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
},
{
'name': 'from',
'label': 'Send e-mail from',
},
{
'name': 'to',
'label': 'Send e-mail to',
},
{
'name': 'smtp_server',
'label': 'SMTP server',
},
{
'name': 'ssl',
'label': 'Enable SSL',
'default': 0,
'type': 'bool',
},
{
'name': 'smtp_user',
'label': 'SMTP user',
},
{
'name': 'smtp_pass',
'label': 'SMTP password',
'type': 'password',
},
{
'name': 'on_snatch',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Also send message when movie is snatched.',
},
],
}
],
}]

View File

@@ -0,0 +1,55 @@
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
import smtplib
import traceback
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')
to_address = self.conf('to')
ssl = self.conf('ssl')
smtp_server = self.conf('smtp_server')
smtp_user = self.conf('smtp_user')
smtp_pass = self.conf('smtp_pass')
# Make the basic message
message = MIMEText(toUnicode(message))
message['Subject'] = self.default_title
message['From'] = from_address
message['To'] = to_address
try:
# Open the SMTP connection, via SSL if requested
log.debug("SMTP over SSL %s", ("enabled" if ssl == 1 else "disabled"))
mailserver = smtplib.SMTP_SSL(smtp_server) if ssl == 1 else smtplib.SMTP(smtp_server)
# Check too see if an login attempt should be attempted
if len(smtp_user) > 0:
log.debug("Logging on to SMTP server using username \'%s\'%s", (smtp_user, " and a password" if len(smtp_pass) > 0 else ""))
mailserver.login(smtp_user, smtp_pass)
# Send the e-mail
log.debug("Sending the email")
mailserver.sendmail(from_address, splitString(to_address), message.as_string())
# Close the SMTP connection
mailserver.quit()
log.info('Email notification sent')
return True
except:
log.error('E-mail failed: %s', traceback.format_exc())
return False
return False

View File

@@ -8,6 +8,7 @@ config = [{
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'growl',
'description': 'Version 1.4+',
'options': [

View File

@@ -37,8 +37,11 @@ class Growl(Notification):
)
self.growl.register()
self.registered = True
except:
log.error('Failed register of growl: %s', traceback.format_exc())
except Exception, e:
if 'timed out' in str(e):
self.registered = True
else:
log.error('Failed register of growl: %s', traceback.format_exc())
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return

View File

@@ -8,6 +8,7 @@ config = [{
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'nmj',
'label': 'NMJ',
'options': [

View File

@@ -8,6 +8,7 @@ config = [{
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'notifo',
'description': 'Keep in mind that Notifo service will end soon.',
'options': [

View File

@@ -8,6 +8,7 @@ config = [{
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'notifymyandroid',
'label': 'Notify My Android',
'options': [

View File

@@ -8,8 +8,9 @@ config = [{
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'notifymywp',
'label': 'Notify My Windows Phone',
'label': 'Windows Phone',
'options': [
{
'name': 'enabled',

View File

@@ -8,6 +8,7 @@ config = [{
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'plex',
'options': [
{

View File

@@ -8,6 +8,7 @@ config = [{
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'prowl',
'options': [
{

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,37 @@
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):
if self.isDisabled(): return
data = {
'AuthorizationToken': self.conf('auth_token'),
'Title': self.default_title,
'Body': toUnicode(message),
'LinkTitle': toUnicode("CouchPotato"),
'link': toUnicode("https://couchpota.to/"),
'IsImportant': self.conf('important'),
'IsSilent': self.conf('silent'),
}
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

@@ -8,6 +8,7 @@ config = [{
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'pushover',
'options': [
{

View File

@@ -8,6 +8,7 @@ config = [{
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'synoindex',
'description': 'Automaticly adds index to Synology Media Server.',
'options': [

View File

@@ -0,0 +1,33 @@
from .main import Toasty
def start():
return Toasty()
config = [{
'name': 'toasty',
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'toasty',
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
},
{
'name': 'api_key',
'label': 'Device ID',
},
{
'name': 'on_snatch',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Also send message when movie is snatched.',
},
],
}
],
}]

View File

@@ -0,0 +1,30 @@
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
import traceback
log = CPLog(__name__)
class Toasty(Notification):
urls = {
'api': 'http://api.supertoasty.com/notify/%s?%s'
}
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
data = {
'title': self.default_title,
'text': toUnicode(message),
'sender': toUnicode("CouchPotato"),
'image': 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/homescreen.png',
}
try:
self.urlopen(self.urls['api'] % (self.conf('api_key'), tryUrlencode(data)), show_error = False)
return True
except:
log.error('Toasty failed: %s', traceback.format_exc())
return False

View File

@@ -8,6 +8,7 @@ config = [{
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'twitter',
'options': [
{

View File

@@ -8,8 +8,10 @@ config = [{
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'xbmc',
'label': 'XBMC',
'description': 'v11 (Eden) and v12 (Frodo)',
'options': [
{
'name': 'enabled',

View File

@@ -4,6 +4,7 @@ from couchpotato.core.notifications.base import Notification
from flask.helpers import json
import base64
import traceback
import urllib
log = CPLog(__name__)
@@ -11,27 +12,148 @@ log = CPLog(__name__)
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'))
successful = 0
for host in hosts:
response = self.request(host, [
('GUI.ShowNotification', {"title":"CouchPotato", "message":message}),
('VideoLibrary.Scan', {}),
])
if self.use_json_notifications.get(host) is None:
self.getXBMCJSONversion(host, message = message)
if self.use_json_notifications.get(host):
response = self.request(host, [
('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.couch_logo_url}),
('VideoLibrary.Scan', {}),
])
else:
response = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message})
response += self.request(host, [('VideoLibrary.Scan', {})])
try:
for result in response:
if result['result'] == "OK":
if (result.get('result') and result['result'] == 'OK'):
successful += 1
elif (result.get('error')):
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
except:
log.error('Failed parsing results: %s', traceback.format_exc())
return successful == len(hosts) * 2
def getXBMCJSONversion(self, host, message = ''):
success = False
# XBMC JSON-RPC version request
response = self.request(host, [
('JSONRPC.Version', {})
])
for result in response:
if (result.get('result') and type(result['result']['version']).__name__ == 'int'):
# only v2 and v4 return an int object
# v6 (as of XBMC v12(Frodo)) is required to send notifications
xbmc_rpc_version = str(result['result']['version'])
log.debug('XBMC JSON-RPC Version: %s ; Notifications by JSON-RPC only supported for v6 [as of XBMC v12(Frodo)]', xbmc_rpc_version)
# disable JSON use
self.use_json_notifications[host] = False
# send the text message
resp = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message})
for result in resp:
if (result.get('result') and result['result'] == 'OK'):
log.debug('Message delivered successfully!')
success = True
break
elif (result.get('error')):
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
break
elif (result.get('result') and type(result['result']['version']).__name__ == 'dict'):
# XBMC JSON-RPC v6 returns an array object containing
# major, minor and patch number
xbmc_rpc_version = str(result['result']['version']['major'])
xbmc_rpc_version += '.' + str(result['result']['version']['minor'])
xbmc_rpc_version += '.' + str(result['result']['version']['patch'])
log.debug('XBMC JSON-RPC Version: %s', xbmc_rpc_version)
# ok, XBMC version is supported
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})])
for result in resp:
if (result.get('result') and result['result'] == 'OK'):
log.debug('Message delivered successfully!')
success = True
break
elif (result.get('error')):
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
break
# error getting version info (we do have contact with XBMC though)
elif (result.get('error')):
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
log.debug('Use JSON notifications: %s ', self.use_json_notifications)
return success
def notifyXBMCnoJSON(self, host, data):
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))
server += cmd
# I have no idea what to set to, just tried text/plain and seems to be working :)
headers = {
'Content-Type': 'text/plain',
}
# authentication support
if self.conf('password'):
base64string = base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password'))).replace('\n', '')
headers['Authorization'] = 'Basic %s' % base64string
try:
log.debug('Sending non-JSON-type request to %s: %s', (host, data))
# response wil either be 'OK':
# <html>
# <li>OK
# </html>
#
# or 'Error':
# <html>
# <li>Error:<message>
# </html>
#
response = self.urlopen(server, headers = headers)
if 'OK' in response:
log.debug('Returned from non-JSON-type request %s: %s', (host, response))
# manually fake expected response array
return [{'result': 'OK'}]
else:
log.error('Returned from non-JSON-type request %s: %s', (host, response))
# manually fake expected response array
return [{'result': 'Error'}]
except:
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc())
return [{'result': 'Error'}]
def request(self, host, requests):
server = 'http://%s/jsonrpc' % host

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')
@@ -78,7 +78,7 @@ class Plugin(object):
self.makeDir(os.path.dirname(path))
try:
f = open(path, 'w' if not binary else 'wb')
f = open(path, 'w+' if not binary else 'w+b')
f.write(content)
f.close()
os.chmod(path, Env.getPermission('file'))
@@ -98,6 +98,7 @@ class Plugin(object):
# http request
def urlopen(self, url, timeout = 30, params = None, headers = None, opener = None, multipart = False, show_error = True):
url = ss(url)
if not headers: headers = {}
if not params: params = {}
@@ -129,8 +130,11 @@ class Plugin(object):
log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
request = urllib2.Request(url, params, headers)
cookies = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), MultipartPostHandler)
if opener:
opener.add_handler(MultipartPostHandler())
else:
cookies = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), MultipartPostHandler)
response = opener.open(request, timeout = timeout)
else:
@@ -236,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

@@ -71,7 +71,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: continue
file_path = os.path.join(root, filename)
f = db.query(File).filter(File.path == toUnicode(file_path)).first()
if not f:

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

@@ -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 = {}):
@@ -135,7 +138,6 @@ class Manage(Plugin):
already_used = used_files.get(release_file['path'])
if already_used:
print already_used, release['id']
if already_used < release['id']:
fireEvent('release.delete', release['id'], single = True) # delete this one
else:
@@ -193,15 +195,19 @@ 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
def directories(self):
try:
return splitString(self.conf('library', default = ''), '::')
if self.conf('library', default = '').strip():
return splitString(self.conf('library', default = ''), '::')
except:
return []
pass
return []
def scanFilesToLibrary(self, folder = None, files = None):
@@ -212,3 +218,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,34 @@ 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 = fireEvent('status.get', 'done', single = True)
available_status = fireEvent('status.get', 'available', single = True)
snatched_status = fireEvent('status.get', '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 +152,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 +194,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 +209,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 +228,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 +237,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 +258,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 +292,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,
@@ -283,7 +345,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 +354,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)
@@ -307,7 +368,9 @@ class MoviePlugin(Plugin):
# Status
status_active = fireEvent('status.add', 'active', single = True)
status_snatched = fireEvent('status.add', 'snatched', single = True)
snatched_status = fireEvent('status.add', 'snatched', single = True)
ignored_status = fireEvent('status.add', 'ignored', single = True)
downloaded_status = fireEvent('status.add', 'downloaded', single = True)
default_profile = fireEvent('profile.default', single = True)
@@ -319,7 +382,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 +394,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')]:
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 +409,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 +516,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

View File

@@ -5,6 +5,7 @@ var MovieList = new Class({
options: {
navigation: true,
limit: 50,
load_more: true,
menu: [],
add_new: false
},
@@ -12,25 +13,37 @@ var MovieList = new Class({
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
);
self.changeView(self.getSavedView() || self.options.view || 'details');
self.getMovies();
App.addEvent('movie.added', self.movieAdded.bind(self))
@@ -70,22 +83,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 +101,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();
}
@@ -121,18 +126,14 @@ var MovieList = new Class({
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,7 +144,7 @@ var MovieList = new Class({
var self = this;
var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
self.current_view = self.getSavedView();
self.current_view = self.getSavedView() || 'details';
self.el.addClass(self.current_view+'_list')
self.navigation = new Element('div.alph_nav').adopt(
@@ -216,7 +217,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': {
@@ -398,11 +399,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 +424,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,9 +470,12 @@ 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...');
}
Api.request(self.options.api_call || 'movie.list', {
'data': Object.merge({
'status': self.options.status,
'limit_offset': self.options.limit + ',' + self.offset
@@ -478,8 +483,10 @@ var MovieList = new Class({
'onComplete': function(json){
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()
}
@@ -502,7 +509,13 @@ 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[is_empty ? 'hide' : 'show']()
if(is_empty && self.options.on_empty_element){
self.el.grab(self.options.on_empty_element);

View File

@@ -0,0 +1,699 @@
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 {
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,
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
)
}
}
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);
}
});
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,7 +1,33 @@
.movies {
padding: 60px 0 20px;
position: relative;
z-index: 3;
}
.movies h2 {
margin-bottom: 20px;
}
.movies > .description {
position: absolute;
top: 30px;
right: 0;
font-style: italic;
text-shadow: none;
opacity: 0.8;
}
.movies:hover > .description {
opacity: 1;
}
.movies.thumbs_list {
padding: 20px 0 20px;
}
.home .movies {
padding-top: 6px;
}
.movies.mass_edit_list {
padding-top: 90px;
}
@@ -12,33 +38,58 @@
margin: 10px 0;
overflow: hidden;
width: 100%;
height: 180px;
transition: all 0.2s linear;
}
.movies .movie.list_view, .movies .movie.mass_edit_view {
.movies.list_list .movie:not(.details_view),
.movies.mass_edit_list .movie {
height: 32px;
}
.movies.thumbs_list .movie {
width: 153px;
height: 230px;
display: inline-block;
margin: 0 8px 0 0;
}
.movies.thumbs_list .movie:nth-child(6n+6) {
margin: 0;
}
.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;
height: 100%;
width: 840px;
position: relative;
float: right;
position: absolute;
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;
@@ -46,79 +97,148 @@
border: 0;
background: none;
}
.movies.thumbs_list .data {
left: 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 {
display: none;
}
.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.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%;
}
.movies .poster img, .options .poster img {
.movies .poster img,
.options .poster img {
width: 101%;
height: 101%;
}
.movies .info {
position: relative;
height: 100%;
}
.movies .info .title {
font-size: 30px;
display: inline;
position: absolute;
font-size: 28px;
font-weight: bold;
margin-bottom: 10px;
float: left;
left: 0;
top: 0;
width: 90%;
transition: all 0.2s linear;
}
.movies .list_view .info .title, .movies .mass_edit_view .info .title {
.movies.list_list .movie:not(.details_view) .info .title,
.movies.mass_edit_list .info .title {
font-size: 16px;
font-weight: normal;
text-overflow: ellipsis;
width: auto;
overflow: hidden;
}
.movies.thumbs_list .movie:not(.no_thumbnail) .info {
display: none;
}
.movies.thumbs_list .movie:hover .info {
display: block;
}
.movies.thumbs_list .info .title {
font-size: 21px;
text-shadow: 0 0 10px #000;
word-wrap: break-word;
}
.movies .info .year {
position: absolute;
font-size: 30px;
margin-bottom: 10px;
float: right;
color: #bbb;
width: 10%;
right: 0;
top: 0;
text-align: right;
transition: all 0.2s linear;
}
.movies .list_view .info .year, .movies .mass_edit_view .info .year {
.movies.list_list .movie:not(.details_view) .info .year,
.movies.mass_edit_list .info .year {
font-size: 16px;
width: 6%;
right: 10px;
}
.movies.thumbs_list .info .year {
font-size: 23px;
margin: 0;
bottom: 0;
left: 0;
top: auto;
right: auto;
color: #FFF;
text-shadow: none;
text-shadow: 0 0 6px #000;
}
.movies .info .rating {
font-size: 30px;
margin-bottom: 10px;
color: #444;
float: left;
width: 5%;
padding: 0 0 0 3%;
}
.movies .info .description {
position: absolute;
top: 30px;
clear: both;
height: 80px;
overflow: hidden;
@@ -126,63 +246,82 @@
.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: 0;
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;
.movies .status_suggest .data .quality,
.movies.thumbs_list .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 .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_list .data .quality,
.movies.mass_edit_list .data .quality {
text-align: right;
right: 0;
margin-right: 50px;
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;
clear: both;
float: right;
margin-top: -25px;
}
.movies.thumbs_list .data .actions {
bottom: 8px;
right: 10px;
}
.movies .data:hover .action { opacity: 0.6; }
.movies .data:hover .action:hover { opacity: 1; }
.movies.mass_edit_list .data .actions {
@@ -199,10 +338,14 @@
opacity: 0;
}
.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) .data:hover .actions,
.movies.mass_edit_list .data:hover .actions {
margin: 0;
background: #4e5969;
position: relative;
top: 2px;
bottom: 2px;
right: 5px;
z-index: 3;
}
.movies .delete_container {
@@ -284,6 +427,7 @@
.movies .options .table .provider {
width: 120px;
text-overflow: ellipsis;
overflow: hidden;
}
.movies .options .table .name {
width: 350px;
@@ -335,11 +479,11 @@
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;
}
.movies .movie .hide_trailer.hide {
top: -30px;
}
.movies .movie .try_container {
padding: 5px 10px;
@@ -380,7 +524,7 @@
.movies .alph_nav {
transition: box-shadow .4s linear;
position: fixed;
z-index: 2;
z-index: 4;
top: 0;
padding: 100px 60px 7px;
width: 1080px;
@@ -409,7 +553,8 @@
text-align: center;
}
.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;
@@ -472,7 +617,7 @@
background-position: 3px -95px;
}
.movies .alph_nav .actions li.thumbs span {
.movies .alph_nav .actions li.details span {
background-position: 3px -74px;
}

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');
@@ -72,7 +72,6 @@ var Movie = new Class({
else if(!self.spinner) {
self.createMask();
self.spinner = createSpinner(self.mask);
self.positionMask();
self.mask.fade('in');
}
},
@@ -81,10 +80,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 +101,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 +112,50 @@ 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', {
'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.thumbnail = File.Select.single('poster', self.data.library.files),
self.data_container = new Element('div.data.inlay.light').adopt(
self.info_container = new Element('div.info').adopt(
self.title = new Element('div.title', {
'text': self.getTitle() || 'n/a'
}),
self.year = new Element('div.year', {
'text': self.data.library.year || 'n/a'
}),
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.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 +170,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 +237,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 +267,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 +290,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

@@ -191,6 +191,8 @@
.movie_result .info h2 {
margin: 0;
font-size: 17px;
line-height: 20px;
}
.movie_result .info h2 span {
@@ -200,7 +202,8 @@
.movie_result .info h2 span:before { content: "("; }
.movie_result .info h2 span:after { content: ")"; }
.search_form .mask {
.search_form .mask,
.movie_result .mask {
border-radius: 3px;
position: absolute;
height: 100%;

View File

@@ -366,7 +366,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

@@ -21,7 +21,7 @@ class QualityPlugin(Plugin):
{'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': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['avi']},
{'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']},

View File

@@ -1,4 +1,5 @@
from couchpotato.core.plugins.renamer.main import Renamer
import os
def start():
return Renamer()
@@ -111,6 +112,15 @@ config = [{
'label': 'Separator',
'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.',
},
{
'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

@@ -13,6 +13,7 @@ import errno
import os
import re
import shutil
import time
import traceback
log = CPLog(__name__)
@@ -33,6 +34,7 @@ class Renamer(Plugin):
addEvent('renamer.check_snatched', self.checkSnatched)
addEvent('app.load', self.scan)
addEvent('app.load', self.checkSnatched)
if self.conf('run_every') > 0:
fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'))
@@ -169,15 +171,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:
@@ -274,6 +276,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()))
@@ -315,8 +318,10 @@ 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
@@ -454,6 +459,8 @@ class Renamer(Plugin):
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)))
@@ -467,7 +474,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
@@ -553,6 +560,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
@@ -563,7 +571,7 @@ class Renamer(Plugin):
found = False
for item in statuses:
if item['name'] == nzbname or getImdb(item['name']) == movie_dict['library']['identifier']:
if item['name'] == nzbname or rel_dict['info']['name'] in item['name'] or getImdb(item['name']) == movie_dict['library']['identifier']:
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))
@@ -577,6 +585,7 @@ class Renamer(Plugin):
fireEvent('searcher.try_next_release', movie_id = rel.movie_id)
else:
rel.status_id = failed_status.get('id')
rel.last_edit = int(time.time())
db.commit()
elif item['status'] == 'completed':
log.info('Download of %s completed!', item['name'])

View File

@@ -23,7 +23,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'],
@@ -89,7 +89,7 @@ class Scanner(Plugin):
'()([ab])(\.....?)$' #*a.mkv
]
cp_imdb = '(\.cp\((?P<id>tt[0-9{7}]+)\))'
cp_imdb = '(.cp.(?P<id>tt[0-9{7}]+).)'
def __init__(self):
@@ -341,7 +341,7 @@ class Scanner(Plugin):
group['files']['movie'] = self.getMediaFiles(group['unsorted_files'])
if len(group['files']['movie']) == 0:
log.error('Couldn\t find any movie files for %s', identifier)
log.error('Couldn\'t find any movie files for %s', identifier)
continue
log.debug('Getting metadata for %s', identifier)

View File

@@ -12,6 +12,7 @@ from couchpotato.environment import Env
from inspect import ismethod, isfunction
from sqlalchemy.exc import InterfaceError
import datetime
import random
import re
import time
import traceback
@@ -29,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',
@@ -83,37 +85,51 @@ class Searcher(Plugin):
movies = db.query(Movie).filter(
Movie.status.has(identifier = 'active')
).all()
random.shuffle(movies)
self.in_progress = {
'total': len(movies),
'to_go': len(movies),
}
for movie in movies:
movie_dict = movie.to_dict({
'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files':{}},
'files': {}
})
try:
search_types = self.getSearchTypes()
try:
self.single(movie_dict)
except IndexError:
log.error('Forcing library update for %s, if you see this often, please report: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
fireEvent('library.update', movie_dict['library']['identifier'], force = True)
except:
log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
for movie in movies:
movie_dict = movie.to_dict({
'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files':{}},
'files': {}
})
self.in_progress['to_go'] -= 1
try:
self.single(movie_dict, search_types)
except IndexError:
log.error('Forcing library update for %s, if you see this often, please report: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
fireEvent('library.update', movie_dict['library']['identifier'], force = True)
except:
log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
# Break if CP wants to shut down
if self.shuttingDown():
break
self.in_progress['to_go'] -= 1
# Break if CP wants to shut down
if self.shuttingDown():
break
except SearchSetupError:
pass
self.in_progress = False
def single(self, movie):
def single(self, movie, search_types = None):
# Find out search type
try:
if not search_types:
search_types = self.getSearchTypes()
except SearchSetupError:
return
done_status = fireEvent('status.get', 'done', single = True)
@@ -128,6 +144,8 @@ class Searcher(Plugin):
available_status = fireEvent('status.get', 'available', single = True)
ignored_status = fireEvent('status.get', 'ignored', single = True)
found_releases = []
default_title = getTitle(movie['library'])
if not default_title:
log.error('No proper info found for movie, removing it from library to cause it from having more issues.')
@@ -136,9 +154,10 @@ class Searcher(Plugin):
fireEvent('notify.frontend', type = 'searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title)
ret = False
for quality_type in movie['profile']['types']:
if not self.couldBeReleased(quality_type['quality']['identifier'], release_dates, pre_releases):
if 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
@@ -155,7 +174,11 @@ class Searcher(Plugin):
log.info('Search for %s in %s', (default_title, quality_type['quality']['label']))
quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True)
results = fireEvent('yarr.search', movie, quality, merge = True)
results = []
for search_type in search_types:
type_results = fireEvent('%s.search' % search_type, movie, quality, merge = True)
if type_results:
results += type_results
sorted_results = sorted(results, key = lambda k: k['score'], reverse = True)
if len(sorted_results) == 0:
@@ -172,10 +195,13 @@ class Searcher(Plugin):
# Add them to this movie releases list
for nzb in sorted_results:
rls = db.query(Release).filter_by(identifier = md5(nzb['url'])).first()
nzb_identifier = md5(nzb['url'])
found_releases.append(nzb_identifier)
rls = db.query(Release).filter_by(identifier = nzb_identifier).first()
if not rls:
rls = Release(
identifier = md5(nzb['url']),
identifier = nzb_identifier,
movie_id = movie.get('id'),
quality_id = quality_type.get('quality_id'),
status_id = available_status.get('id')
@@ -183,6 +209,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()
@@ -223,6 +250,12 @@ class Searcher(Plugin):
break
elif downloaded != 'try_next':
break
# Remove releases that aren't found anymore
for release in movie.get('releases', []):
if release.get('status_id') == available_status.get('id') and release.get('identifier') not in found_releases:
fireEvent('release.delete', release.get('id'), single = True)
else:
log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title))
fireEvent('movie.restatus', movie['id'])
@@ -238,61 +271,92 @@ class Searcher(Plugin):
def download(self, data, movie, manual = False):
snatched_status = fireEvent('status.get', 'snatched', single = True)
# Test to see if any downloaders are enabled for this type
downloader_enabled = fireEvent('download.enabled', manual, data, single = True)
# Download movie to temp
filedata = None
if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))):
filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
if filedata == 'try_next':
return filedata
if downloader_enabled:
successful = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
snatched_status = fireEvent('status.get', 'snatched', single = True)
if successful:
# Download movie to temp
filedata = None
if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))):
filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
if filedata == 'try_next':
return filedata
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')
db.commit()
successful = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
log.info(snatch_message)
fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict())
if successful:
# 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)
try:
# Mark release as snatched
db = get_session()
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
if rls:
renamer_enabled = Env.setting('enabled', 'renamer')
# Mark release done
rls.status_id = done_status.get('id')
db.commit()
done_status = fireEvent('status.get', 'done', single = True)
rls.status_id = done_status.get('id') if not renamer_enabled else snatched_status.get('id')
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())
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
log.info(snatch_message)
fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict())
except:
log.error('Failed marking movie finished: %s', traceback.format_exc())
# 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)
return True
# Mark release done
rls.status_id = done_status.get('id')
rls.last_edit = int(time.time())
db.commit()
# Mark movie done
mvie = db.query(Movie).filter_by(id = movie['id']).first()
mvie.status_id = done_status.get('id')
mvie.last_edit = int(time.time())
db.commit()
except:
log.error('Failed marking movie finished, renamer disabled: %s', traceback.format_exc())
except:
log.error('Failed marking movie finished: %s', traceback.format_exc())
return True
log.info('Tried to download, but none of the "%s" downloaders are enabled', (data.get('type', '')))
log.info('Tried to download, but none of the downloaders are enabled')
return False
def getSearchTypes(self):
download_types = fireEvent('download.enabled_types', merge = True)
provider_types = fireEvent('provider.enabled_types', merge = True)
if download_types and len(list(set(provider_types) & set(download_types))) == 0:
log.error('There aren\'t any providers enabled for your downloader (%s). Check your settings.', ','.join(download_types))
raise NoProviders
for useless_provider in list(set(provider_types) - set(download_types)):
log.debug('Provider for "%s" enabled, but no downloader.', useless_provider)
search_types = download_types
if len(search_types) == 0:
log.error('There aren\'t any downloaders enabled. Please pick one in settings.')
raise NoDownloaders
return search_types
def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs):
imdb_results = kwargs.get('imdb_results', False)
@@ -469,7 +533,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())
@@ -481,7 +545,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
@@ -536,3 +600,12 @@ class Searcher(Plugin):
except:
log.error('Failed searching for next release: %s', traceback.format_exc())
return False
class SearchSetupError(Exception):
pass
class NoDownloaders(SearchSetupError):
pass
class NoProviders(SearchSetupError):
pass

View File

@@ -23,6 +23,7 @@ class StatusPlugin(Plugin):
'deleted': 'Deleted',
'ignored': 'Ignored',
'available': 'Available',
'suggest': 'Suggest',
}
def __init__(self):

View File

@@ -49,6 +49,7 @@ class Subtitle(Plugin):
available_languages = sum(group['subtitle_language'].itervalues(), [])
downloaded = []
files = [toUnicode(x) for x in group['files']['movie']]
log.debug('Searching for subtitles for: %s', files)
for lang in self.getLanguages():
if lang not in available_languages:
@@ -57,6 +58,7 @@ class Subtitle(Plugin):
downloaded.extend(download[subtitle])
for d_sub in downloaded:
log.info('Found subtitle (%s): %s', (d_sub.language.alpha2, files))
group['files']['subtitle'].add(d_sub.path)
group['subtitle_language'][d_sub.path] = [d_sub.language.alpha2]

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

@@ -1,8 +1,13 @@
.page.wizard .uniForm {
width: 80%;
margin: 0 auto 30px;
}
.page.wizard h1 {
padding: 10px 30px;
margin: 0;
display: block;
font-size: 40px;
font-size: 30px;
margin-top: 80px;
}

View File

@@ -41,8 +41,7 @@ Page.Wizard = new Class({
},
'providers': {
'title': 'Are you registered at any of these sites?',
'description': 'CP uses these sites to search for movies. A few free are enabled by default, but it\'s always better to have a few more. Check settings for the full list of available providers.',
'include': ['nzb_providers', 'torrent_providers']
'description': 'CP uses these sites to search for movies. A few free are enabled by default, but it\'s always better to have a few more. Check settings for the full list of available providers.'
},
'renamer': {
'title': 'Move & rename the movies after downloading?',
@@ -83,7 +82,7 @@ Page.Wizard = new Class({
'target': self.el
},
'onComplete': function(){
window.location = App.createUrl();
window.location = App.createUrl('wanted');
}
});
}
@@ -213,8 +212,6 @@ Page.Wizard = new Class({
// Hide retention
self.el.getElement('.tab_searcher').hide();
self.el.getElement('.t_searcher').hide();
self.el.getElement('.t_nzb_providers').hide();
self.el.getElement('.t_torrent_providers').hide();
// Add pointer
new Element('.tab_wrapper').wraps(tabs).adopt(

View File

@@ -0,0 +1,21 @@
config = {
'name': 'automation_providers',
'groups': [
{
'label': 'Watchlists',
'description': 'Check watchlists for new movies',
'type': 'list',
'name': 'watchlist_providers',
'tab': 'automation',
'options': [],
},
{
'label': 'Automated',
'description': 'Uses minimal requirements',
'type': 'list',
'name': 'automation_providers',
'tab': 'automation',
'options': [],
},
],
}

View File

@@ -1,13 +1,13 @@
from couchpotato.core.event import addEvent, fireEvent
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
import time
log = CPLog(__name__)
class Automation(Plugin):
class Automation(Provider):
enabled_option = 'automation_enabled'
@@ -19,6 +19,9 @@ class Automation(Plugin):
def _getMovies(self):
if self.isDisabled():
return
if not self.canCheck():
log.debug('Just checked, skipping %s', self.getName())
return []

View File

@@ -8,6 +8,7 @@ config = [{
'groups': [
{
'tab': 'automation',
'list': 'automation_providers',
'name': 'bluray_automation',
'label': 'Blu-ray.com',
'description': 'Imports movies from blu-ray.com. (uses minimal requirements)',

View File

@@ -1,8 +1,7 @@
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import md5, tryInt
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation
import xml.etree.ElementTree as XMLTree
log = CPLog(__name__)
@@ -14,32 +13,24 @@ class Bluray(Automation, RSS):
def getIMDBids(self):
if self.isDisabled():
return
movies = []
cache_key = 'bluray.%s' % md5(self.rss_url)
rss_data = self.getCache(cache_key, self.rss_url)
data = XMLTree.fromstring(rss_data)
rss_movies = self.getRSSData(self.rss_url)
if data is not None:
rss_movies = self.getElements(data, 'channel/item')
for movie in rss_movies:
name = self.getTextElement(movie, 'title').lower().split('blu-ray')[0].strip('(').rstrip()
year = self.getTextElement(movie, 'description').split('|')[1].strip('(').strip()
for movie in rss_movies:
name = self.getTextElement(movie, "title").lower().split("blu-ray")[0].strip("(").rstrip()
year = self.getTextElement(movie, "description").split("|")[1].strip("(").strip()
if not name.find('/') == -1: # make sure it is not a double movie release
continue
if not name.find("/") == -1: # make sure it is not a double movie release
continue
if tryInt(year) < self.getMinimal('year'):
continue
if tryInt(year) < self.getMinimal('year'):
continue
imdb = self.search(name, year)
imdb = self.search(name, year)
if imdb:
if self.isMinimalMovie(imdb):
movies.append(imdb['imdb'])
if imdb:
if self.isMinimalMovie(imdb):
movies.append(imdb['imdb'])
return movies

View File

@@ -8,7 +8,4 @@ class CP(Automation):
def getMovies(self):
if self.isDisabled():
return
return []

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

@@ -8,6 +8,7 @@ config = [{
'groups': [
{
'tab': 'automation',
'list': 'watchlist_providers',
'name': 'imdb_automation',
'label': 'IMDB',
'description': 'From any <strong>public</strong> IMDB watchlists. Url should be the RSS link.',

View File

@@ -1,5 +1,5 @@
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import md5, getImdb, splitString, tryInt
from couchpotato.core.helpers.variable import getImdb, splitString, tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation
import traceback
@@ -13,9 +13,6 @@ class IMDB(Automation, RSS):
def getIMDBids(self):
if self.isDisabled():
return
movies = []
enablers = [tryInt(x) for x in splitString(self.conf('automation_urls_use'))]
@@ -29,9 +26,8 @@ class IMDB(Automation, RSS):
continue
try:
cache_key = 'imdb.rss.%s' % md5(url)
rss_data = self.getCache(cache_key, url)
imdbs = getImdb(rss_data, multiple = True)
rss_data = self.getHTMLData(url)
imdbs = getImdb(rss_data, multiple = True) if rss_data else []
for imdb in imdbs:
movies.append(imdb)

View File

@@ -0,0 +1,36 @@
from .main import ITunes
def start():
return ITunes()
config = [{
'name': 'itunes',
'groups': [
{
'tab': 'automation',
'list': 'automation_providers',
'name': 'itunes_automation',
'label': 'iTunes',
'description': 'From any <a href="http://itunes.apple.com/rss">iTunes</a> Store feed. Url should be the RSS link. (uses minimal requirements)',
'options': [
{
'name': 'automation_enabled',
'default': False,
'type': 'enabler',
},
{
'name': 'automation_urls_use',
'label': 'Use',
'default': ',',
},
{
'name': 'automation_urls',
'label': 'url',
'type': 'combined',
'combine': ['automation_urls_use', 'automation_urls'],
'default': 'https://itunes.apple.com/rss/topmovies/limit=25/xml,',
},
],
},
],
}]

View File

@@ -0,0 +1,63 @@
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import md5, splitString, tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation
from xml.etree.ElementTree import QName
import datetime
import traceback
import xml.etree.ElementTree as XMLTree
log = CPLog(__name__)
class ITunes(Automation, RSS):
interval = 1800
def getIMDBids(self):
if self.isDisabled():
return
movies = []
enablers = [tryInt(x) for x in splitString(self.conf('automation_urls_use'))]
urls = splitString(self.conf('automation_urls'))
namespace = 'http://www.w3.org/2005/Atom'
namespaceIM = 'http://itunes.apple.com/rss'
index = -1
for url in urls:
index += 1
if not enablers[index]:
continue
try:
cache_key = 'itunes.rss.%s' % md5(url)
rss_data = self.getCache(cache_key, url)
data = XMLTree.fromstring(rss_data)
if data is not None:
entry_tag = str(QName(namespace, 'entry'))
rss_movies = self.getElements(data, entry_tag)
for movie in rss_movies:
name_tag = str(QName(namespaceIM, 'name'))
name = self.getTextElement(movie, name_tag)
releaseDate_tag = str(QName(namespaceIM, 'releaseDate'))
releaseDateText = self.getTextElement(movie, releaseDate_tag)
year = datetime.datetime.strptime(releaseDateText, '%Y-%m-%dT00:00:00-07:00').strftime("%Y")
imdb = self.search(name, year)
if imdb and self.isMinimalMovie(imdb):
movies.append(imdb['imdb'])
except:
log.error('Failed loading iTunes rss feed: %s %s', (url, traceback.format_exc()))
return movies

View File

@@ -8,6 +8,7 @@ config = [{
'groups': [
{
'tab': 'automation',
'list': 'automation_providers',
'name': 'kinepolis_automation',
'label': 'Kinepolis',
'description': 'Imports movies from the current top 10 of kinepolis. (uses minimal requirements)',

View File

@@ -1,9 +1,7 @@
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import md5
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation
import datetime
import xml.etree.ElementTree as XMLTree
log = CPLog(__name__)
@@ -15,25 +13,17 @@ class Kinepolis(Automation, RSS):
def getIMDBids(self):
if self.isDisabled():
return
movies = []
cache_key = 'kinepolis.%s' % md5(self.rss_url)
rss_data = self.getCache(cache_key, self.rss_url)
data = XMLTree.fromstring(rss_data)
rss_movies = self.getRSSData(self.rss_url)
if data is not None:
rss_movies = self.getElements(data, 'channel/item')
for movie in rss_movies:
name = self.getTextElement(movie, 'title')
year = datetime.datetime.now().strftime('%Y')
for movie in rss_movies:
name = self.getTextElement(movie, "title")
year = datetime.datetime.now().strftime("%Y")
imdb = self.search(name, year)
imdb = self.search(name, year)
if imdb and self.isMinimalMovie(imdb):
movies.append(imdb['imdb'])
if imdb and self.isMinimalMovie(imdb):
movies.append(imdb['imdb'])
return movies

View File

@@ -0,0 +1,24 @@
from .main import Moviemeter
def start():
return Moviemeter()
config = [{
'name': 'moviemeter',
'groups': [
{
'tab': 'automation',
'list': 'automation_providers',
'name': 'moviemeter_automation',
'label': 'Moviemeter',
'description': 'Imports movies from the current top 10 of moviemeter.nl. (uses minimal requirements)',
'options': [
{
'name': 'automation_enabled',
'default': False,
'type': 'enabler',
},
],
},
],
}]

View File

@@ -0,0 +1,28 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation
log = CPLog(__name__)
class Moviemeter(Automation, RSS):
interval = 1800
rss_url = 'http://www.moviemeter.nl/rss/cinema'
def getIMDBids(self):
movies = []
rss_movies = self.getRSSData(self.rss_url)
for movie in rss_movies:
name_year = fireEvent('scanner.name_year', self.getTextElement(movie, 'title'), single = True)
imdb = self.search(name_year.get('name'), name_year.get('year'))
if imdb and self.isMinimalMovie(imdb):
movies.append(imdb['imdb'])
return movies

View File

@@ -8,6 +8,7 @@ config = [{
'groups': [
{
'tab': 'automation',
'list': 'watchlist_providers',
'name': 'moviesio',
'label': 'Movies.IO',
'description': 'Imports movies from <a href="http://movies.io">Movies.io</a> RSS watchlists',

View File

@@ -1,11 +1,8 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import md5
from couchpotato.core.helpers.variable import tryInt, splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation
from xml.etree.ElementTree import ParseError
import traceback
import xml.etree.ElementTree as XMLTree
log = CPLog(__name__)
@@ -16,39 +13,27 @@ class MoviesIO(Automation, RSS):
def getIMDBids(self):
if self.isDisabled():
return
movies = []
enablers = self.conf('automation_urls_use').split(',')
enablers = [tryInt(x) for x in splitString(self.conf('automation_urls_use'))]
index = -1
for rss_url in self.conf('automation_urls').split(','):
for rss_url in splitString(self.conf('automation_urls')):
index += 1
if not enablers[index]:
continue
try:
cache_key = 'imdb.rss.%s' % md5(rss_url)
rss_movies = self.getRSSData(rss_url, headers = {'Referer': ''})
rss_data = self.getCache(cache_key, rss_url, headers = {'Referer': ''})
data = XMLTree.fromstring(rss_data)
rss_movies = self.getElements(data, 'channel/item')
for movie in rss_movies:
for movie in rss_movies:
nameyear = fireEvent('scanner.name_year', self.getTextElement(movie, 'title'), single = True)
imdb = self.search(nameyear.get('name'), nameyear.get('year'), imdb_only = True)
nameyear = fireEvent('scanner.name_year', self.getTextElement(movie, "title"), single = True)
imdb = self.search(nameyear.get('name'), nameyear.get('year'), imdb_only = True)
if not imdb:
continue
if not imdb:
continue
movies.append(imdb)
except ParseError:
log.debug('Failed loading Movies.io watchlist, probably empty: %s', (rss_url))
except:
log.error('Failed loading Movies.io watchlist: %s %s', (rss_url, traceback.format_exc()))
movies.append(imdb)
return movies

View File

@@ -0,0 +1,29 @@
from .main import Rottentomatoes
def start():
return Rottentomatoes()
config = [{
'name': 'rottentomatoes',
'groups': [
{
'tab': 'automation',
'list': 'automation_providers',
'name': 'rottentomatoes_automation',
'label': 'Rottentomatoes',
'description': 'Imports movies from the rottentomatoes "in theaters"-feed.',
'options': [
{
'name': 'automation_enabled',
'default': False,
'type': 'enabler',
},
{
'name': 'tomatometer_percent',
'default': '80',
'label': 'Tomatometer'
}
],
},
],
}]

View File

@@ -0,0 +1,48 @@
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation
from xml.etree.ElementTree import QName
import datetime
import re
log = CPLog(__name__)
class Rottentomatoes(Automation, RSS):
interval = 1800
urls = {
'namespace': 'http://www.rottentomatoes.com/xmlns/rtmovie/',
'theater': 'http://www.rottentomatoes.com/syndication/rss/in_theaters.xml',
}
def getIMDBids(self):
movies = []
rss_movies = self.getRSSData(self.urls['theater'])
rating_tag = str(QName(self.urls['namespace'], 'tomatometer_percent'))
for movie in rss_movies:
value = self.getTextElement(movie, "title")
result = re.search('(?<=%\s).*', value)
if result:
log.info2('Something smells...')
rating = tryInt(self.getTextElement(movie, rating_tag))
name = result.group(0)
if rating < tryInt(self.conf('tomatometer_percent')):
log.info2('%s seems to be rotten...' % name)
else:
log.info2('Found %s fresh enough movies, enqueuing: %s' % (rating, name))
year = datetime.datetime.now().strftime("%Y")
imdb = self.search(name, year)
if imdb:
movies.append(imdb['imdb'])
return movies

View File

@@ -8,6 +8,7 @@ config = [{
'groups': [
{
'tab': 'automation',
'list': 'watchlist_providers',
'name': 'trakt_automation',
'label': 'Trakt',
'description': 'import movies from your own watchlist',

View File

@@ -1,9 +1,8 @@
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.variable import md5, sha1
from couchpotato.core.helpers.variable import sha1
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation
import base64
import json
log = CPLog(__name__)
@@ -25,9 +24,6 @@ class Trakt(Automation):
def getIMDBids(self):
if self.isDisabled():
return
movies = []
for movie in self.getWatchlist():
movies.append(movie.get('imdb_id'))
@@ -38,22 +34,11 @@ class Trakt(Automation):
method = (self.urls['watchlist'] % self.conf('automation_api_key')) + self.conf('automation_username')
return self.call(method)
def call(self, method_url):
try:
if self.conf('automation_password'):
headers = {
'Authorization': 'Basic %s' % base64.encodestring('%s:%s' % (self.conf('automation_username'), self.conf('automation_password')))[:-1]
}
else:
headers = {}
headers = {}
if self.conf('automation_password'):
headers['Authorization'] = 'Basic %s' % base64.encodestring('%s:%s' % (self.conf('automation_username'), self.conf('automation_password')))[:-1]
cache_key = 'trakt.%s' % md5(method_url)
json_string = self.getCache(cache_key, self.urls['base'] + method_url, headers = headers)
if json_string:
return json.loads(json_string)
except:
log.error('Failed to get data from trakt, check your login.')
return []
data = self.getJsonData(self.urls['base'] + method_url, headers = headers)
return data if data else []

View File

@@ -44,6 +44,38 @@ class Provider(Plugin):
return self.is_available.get(host, False)
def getJsonData(self, url, **kwargs):
cache_key = '%s%s' % (md5(url), md5('%s' % kwargs.get('params', {})))
data = self.getCache(cache_key, url, **kwargs)
if data:
try:
return json.loads(data)
except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
return []
def getRSSData(self, url, item_path = 'channel/item', **kwargs):
cache_key = '%s%s' % (md5(url), md5('%s' % kwargs.get('params', {})))
data = self.getCache(cache_key, url, **kwargs)
if data:
try:
data = XMLTree.fromstring(data)
return self.getElements(data, item_path)
except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
return []
def getHTMLData(self, url, **kwargs):
cache_key = '%s%s' % (md5(url), md5('%s' % kwargs.get('params', {})))
return self.getCache(cache_key, url, **kwargs)
class YarrProvider(Provider):
@@ -56,8 +88,16 @@ class YarrProvider(Provider):
login_opener = None
def __init__(self):
addEvent('provider.enabled_types', self.getEnabledProviderType)
addEvent('provider.belongs_to', self.belongsTo)
addEvent('yarr.search', self.search)
addEvent('%s.search' % self.type, self.search)
def getEnabledProviderType(self):
if self.isEnabled():
return self.type
else:
return []
def login(self):
@@ -67,15 +107,20 @@ class YarrProvider(Provider):
urllib2.install_opener(opener)
log.info2('Logging into %s', self.urls['login'])
f = opener.open(self.urls['login'], self.getLoginParams())
f.read()
output = f.read()
f.close()
self.login_opener = opener
return True
if self.loginSuccess(output):
self.login_opener = opener
return True
except:
log.error('Failed to login %s: %s', (self.getName(), traceback.format_exc()))
return False
def loginSuccess(self, output):
return True
def loginDownload(self, url = '', nzb_id = ''):
try:
if not self.login_opener and not self.login():
@@ -106,11 +151,11 @@ class YarrProvider(Provider):
return []
# Create result container
imdb_result = hasattr(self, '_search')
results = ResultList(self, movie, quality, imdb_result = imdb_result)
imdb_results = hasattr(self, '_search')
results = ResultList(self, movie, quality, imdb_results = imdb_results)
# Do search based on imdb id
if imdb_result:
if imdb_results:
self._search(movie, quality, results)
# Search possible titles
else:
@@ -165,34 +210,6 @@ class YarrProvider(Provider):
return [self.cat_backup_id]
def getJsonData(self, url, **kwargs):
data = self.getCache(md5(url), url, **kwargs)
if data:
try:
return json.loads(data)
except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
return []
def getRSSData(self, url, **kwargs):
data = self.getCache(md5(url), url, **kwargs)
if data:
try:
data = XMLTree.fromstring(data)
return self.getElements(data, 'channel/item')
except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
return []
def getHTMLData(self, url, **kwargs):
return self.getCache(md5(url), url, **kwargs)
class ResultList(list):

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