Compare commits

...

442 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
Ruud
9a314cfbc4 One up 2012-12-29 00:03:45 +01:00
Ruud
5941d0bf77 Add version to update url 2012-12-29 00:03:36 +01:00
Ruud
d326c1c25c Merge branch 'refs/heads/master' into desktop
Conflicts:
	version.py
2012-12-28 23:31:08 +01:00
Ruud
7e6234298d Merge branch 'refs/heads/develop' 2012-12-28 23:25:40 +01:00
Ruud
5cf4b8b4d3 Binsearch provider 2012-12-28 23:01:35 +01:00
Ruud
6e56072250 Don't migrate when in development 2012-12-28 17:44:02 +01:00
Ruud
917c5552a4 Simplified providers 2012-12-27 19:53:12 +01:00
Ruud
73c5b90232 TorrentDay support. closes #1161 2012-12-25 18:38:37 +01:00
Ruud
fd53ba0637 SceneAccess: Don't add quality to query 2012-12-25 17:43:47 +01:00
Ruud
0ef3906b3d Cleanup 2012-12-25 17:15:09 +01:00
Ruud
5ab0d7a97b Cleanup torrent providers 2012-12-25 14:56:27 +01:00
Ruud
dbbbbb2f84 Module update: dateutil 2012-12-25 11:38:49 +01:00
Ruud
1bfe948a45 Newznab didn't return results 2012-12-24 19:21:43 +01:00
Ruud
0d2dcff7f0 NZB Provider cleanup 2012-12-23 02:48:36 +01:00
Ruud
d4da206f93 Merge branch 'refs/heads/develop' 2012-12-22 16:33:47 +01:00
Ruud
439cda8b63 Newznab age wrong. fix #1171 2012-12-22 16:33:28 +01:00
Ruud
bbe8362b08 Show updating screen instantly. closes #1167 2012-12-21 23:54:41 +01:00
Ruud
985a168724 Merge branch 'refs/heads/develop' 2012-12-21 23:18:00 +01:00
Ruud
5e6aea97f7 Score providers 2012-12-21 23:17:50 +01:00
Ruud
6c7c4c7aba Use same api call for all qualities. closes #1164 2012-12-21 23:17:42 +01:00
Ruud
e2f59f5ff4 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2012-12-21 22:15:18 +01:00
Ruud
b225980ce7 Use pubDate and enclosure length for newznab 2012-12-21 22:14:37 +01:00
Ruud
b8e86b378f NZBx provider 2012-12-20 15:45:04 +01:00
Ruud
031a186d71 NZBx fixes 2012-12-20 15:19:40 +01:00
Ruud
3c04eed218 Added nzbx option 2012-12-20 15:18:46 +01:00
Ruud
17e01689d9 Remove torrage.ws. fix #1157 2012-12-19 16:13:40 +01:00
Ruud
173c6194ed Merge branch 'refs/heads/develop' 2012-12-19 11:12:26 +01:00
Ruud
95c2e992b0 Use trailer naming from settings. closes #936 2012-12-19 11:11:12 +01:00
Ruud
4bffb299af Catch urlerrors. closes #1154 2012-12-19 08:01:35 +01:00
Ruud
a2c4119508 Change PublicHD to .se TLD 2012-12-18 23:36:13 +01:00
Ruud
4e9472f8ee Encode path properly before using it in walk. close #978 2012-12-18 23:18:54 +01:00
Ruud
f7911fe9f3 Remove release on new scan 2012-12-18 14:14:06 +01:00
Ruud
8ffa6a8392 Quality by id 2012-12-18 13:54:13 +01:00
Ruud
382d49f895 Delete release if it has no files 2012-12-17 22:41:56 +01:00
Ruud
570b79a67e Use height with margin to check quality. fix #582 2012-12-17 22:27:07 +01:00
Ruud
e7aafc406f Check if identifier exists before adding release. fix #1048 2012-12-17 21:10:30 +01:00
Ruud
2dcc1e096e Make path safe first 2012-12-17 21:10:04 +01:00
Ruud
9f0746a668 Encoding issues. fix #974 2012-12-17 20:50:58 +01:00
Ruud
d9c437bd7f Fix some torrentleech stuff. closes #1149 2012-12-17 19:55:27 +01:00
Ruud
7079647f87 Also try and find movie name between [] 2012-12-17 18:49:37 +01:00
Ruud
65570ba479 Improve name searching. closes #1137 2012-12-17 18:22:12 +01:00
Ruud
a57ba9026d Year match only 1900-2099 2012-12-17 18:21:15 +01:00
Ruud
63246256ee Don't remove stuff from python cache 2012-12-17 17:10:53 +01:00
Ruud
1ac0dc3bbf Don't show Environment vars when developing 2012-12-17 16:40:15 +01:00
Ruud
bcd23ad10c Merge branch 'refs/heads/develop' 2012-12-17 15:13:00 +01:00
Ruud
342d31b48a Remove ignored words which are part of title. close #1123 2012-12-17 14:08:44 +01:00
Ruud
ea7904ed9a Typo on seeders check. fix #1142 2012-12-17 13:56:11 +01:00
Ruud
ca37c2f018 Merge branch 'develop-renamer' of https://github.com/clinton-hall/CouchPotatoServer into develop 2012-12-17 13:18:23 +01:00
Ruud
5aa2146614 OMGWTFNZBs support. closes #1130
ZOMG BBQ SAUCAGES NOMNOMNOM
2012-12-17 13:07:01 +01:00
Ruud
0fd49a2c67 FTDWorld returned wrong backup category 2012-12-17 13:03:33 +01:00
Ruud
b680d84cba Don't use handler when in desktop build 2012-12-17 12:00:42 +01:00
Ruud
898e6f487d Merge branch 'refs/heads/develop' 2012-12-16 23:52:06 +01:00
Ruud
96472a9a8f One up 2012-12-16 23:51:58 +01:00
Ruud
27252561e2 Merge branch 'refs/heads/develop' into desktop 2012-12-16 23:51:24 +01:00
Ruud
24b341005e Extended Newznab description 2012-12-16 22:07:02 +01:00
Ruud
749cf550ec FTDWorld provider 2012-12-16 17:37:35 +01:00
Ruud
650177803b Movie login to yarrprovider class 2012-12-16 17:34:50 +01:00
clinton-hall
bb7b4cbbed Added try: except for two common errors
Does not fix the errors, but prevents the renamer being stuck as "in progress"
Allows next instance to run.
2012-12-13 19:45:13 -08:00
Ruud
6618c3927c Merge branch 'refs/heads/develop' 2012-12-11 23:15:06 +01:00
Ruud
003db92c9b Label depending on list. closes #1131 2012-12-11 22:22:31 +01:00
Arsecroft
b2b396bf17 Fixed PtP provider login as per link included
http://passthepopcorn.me/forums.php?action=viewthread&threadid=15602&post=68#post535328
2012-12-11 21:49:48 +01:00
Ruud
f1a1db8d5b Revert "Remove NZBsRus"
This reverts commit f515cd2477.
2012-12-09 21:50:07 +01:00
Ruud
f515cd2477 Remove NZBsRus 2012-12-09 16:21:36 +01:00
Ruud
65bb1bec27 Remove nzbmatrix 2012-12-09 15:49:47 +01:00
Ruud
cc84532824 Typo 2012-12-09 15:42:15 +01:00
clinton-hall
5530fbf792 Remove ignore words. URL too long
Most people are experiencing 401 errors with nzbclub
2012-12-05 22:07:33 +01:00
Ruud
5658a85f61 Use splitstring when possible. 2012-12-04 23:18:13 +01:00
Ruud
0c5206f01b Placeholder for settings 2012-12-04 23:04:48 +01:00
clinton-hall
4bffce637e Fixed double-up in wording 2012-12-04 22:49:20 +01:00
clinton-hall
9f2941a45c Changed description for required words 2012-12-04 22:49:20 +01:00
clinton-hall
f452106bfc Added comparison of sets of required words
Can now match sets of words containing words separated by & with each set separated by ,
This only requires any set of word(s) to be matched, but within each set ALL words must match.
2012-12-04 22:49:20 +01:00
Ruud
da3055be30 Return nothing on disabled calls. 2012-12-03 14:29:59 +01:00
Ruud
f9b65e7216 Add some better name guessing to searcher. fix #1091 2012-12-03 14:20:20 +01:00
Ruud
07e2c56095 Create empty download folder for Transmission. fix #1055 2012-12-03 13:58:46 +01:00
Ruud
9a6cfe3a21 Wait for wasn't used. fix #1104 2012-12-03 13:55:02 +01:00
Ruud
802338a934 Update Tornado lib 2012-12-03 13:31:34 +01:00
Ruud
f0a3358561 Use debug for Tornado errors 2012-12-03 13:11:08 +01:00
Ruud
1c4c69211b Change shutdown event name 2012-12-02 00:18:11 +01:00
Ruud
c9e732651f One up 2012-12-01 12:16:58 +01:00
Ruud
7849e7170d Uninstall only create files, no wildcard *.* 2012-12-01 12:16:51 +01:00
Ruud
087894eb4e Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2012-12-01 11:50:08 +01:00
Ruud
4b58b40226 Merge branch 'refs/heads/develop' 2012-12-01 11:48:54 +01:00
Ruud
77d57f5a09 Removed stopped providers 2012-11-30 22:49:22 +01:00
Ruud
618845a021 Code formatting 2012-11-30 22:37:23 +01:00
iamnos
3aabcbf8f1 Update init/fedora
The config file should be read after the defaults are set, or the options set in the config file are never used.
2012-11-30 21:53:35 +01:00
Ruud
929c6fe3f9 Don't run schedules when it's 0 2012-11-30 21:42:18 +01:00
Ruud
c852949591 To lower case 2012-11-30 21:36:33 +01:00
Guillaume Bienkowski
e36c8ec3ab Fixed typo 2012-11-30 21:32:20 +01:00
Guillaume BIENKOWSKI
afea12c7c0 Log out when finished 2012-11-30 21:32:20 +01:00
Guillaume BIENKOWSKI
c29a8b47d6 Basic support of Synology DownloadStation API
VERY basic. We're not logging out at the moment, and we keep the
session open.
No verification of the protocol version either (I assume DSM 4.1).

Very basic, indeed
2012-11-30 21:32:20 +01:00
Alexej Haak
fdd0826b4f Added custom categories
less big api requests this way and better results (the cats include
audio quality filter)
2012-11-30 21:29:45 +01:00
Alexej Haak
81b7ebaf51 Kere.ws Implementation from Pheelee
Implementation by Pheelee
he closed the pull request some time ago and I dont know why.. Using
this for some time now with no problems at all
2012-11-30 21:29:45 +01:00
Ruud
eafc3db74d Add HDTS as alternative tag. #1097 2012-11-30 21:11:34 +01:00
Ruud
3464435a5c Imdb url only in XBMC meta data 2012-11-24 16:53:38 +01:00
Randall Ma
9f19902221 fixed typo of "successfully" 2012-11-24 16:35:22 +01:00
Ruud
2ed72c9098 Don't show button bar when there aren't any releases 2012-11-20 21:15:43 +01:00
Ruud
723f720280 Ignore header in TPB results 2012-11-20 21:15:22 +01:00
Ruud
daaa2154e5 Return when no table is returned. fix #1053 2012-11-20 21:07:21 +01:00
Ruud
95c5db2d17 Use css for search animations 2012-11-20 20:36:52 +01:00
Ruud
e53a9ed30a Try next on failed download from provider 2012-11-15 18:30:45 +01:00
Ruud
2b49a4b5d6 Merge branch 'develop' of https://github.com/clinton-hall/CouchPotatoServer into clinton-hall-develop 2012-11-14 13:54:27 +01:00
Ruud
4224a25e54 Sabnzbd, show what is in filedata. 2012-11-14 13:44:26 +01:00
Ruud
3635da1f59 Add new api for upcoming extension 2012-11-14 13:43:38 +01:00
clinton-hall
71cca6b87f Removed excess tags from title
Now only adds "Source", "Video Format", "Audio Format", and "Language"
2012-11-13 18:43:31 -08:00
Ruud
68c0496f8e Don't hide partial keyword log. closes #1043 2012-11-13 20:24:58 +01:00
Ruud
6dc3c8d69d Filmweb.pl userscript. closes #1029
Thanks @kempniu
2012-11-11 22:22:38 +01:00
Ruud
3ecc826629 Merge branch 'refs/heads/develop'
Conflicts:
	version.py
2012-11-11 22:06:48 +01:00
Ruud
b03012e4aa Customizable check snatched. closes #920
Thanks to @clinton-hall
2012-11-11 22:01:19 +01:00
Ruud
5a1f05df8e Transmission, add folder name to download dir 2012-11-11 21:47:02 +01:00
Ruud
62a5909856 Transmission add trackers 2012-11-11 21:03:13 +01:00
Ruud
813c078db0 uTorrent cleanup
Add trackers when adding via magnet_link
2012-11-11 20:45:50 +01:00
Ruud
904d1ea4f7 Merge branch 'utorrent_downloader' of https://github.com/EchelonFour/CouchPotatoServer into EchelonFour-utorrent_downloader 2012-11-11 17:40:14 +01:00
Ruud
20b773bc3b Typos. closes #1022
Thanks to @demonbane
2012-11-11 17:36:42 +01:00
Ruud
be56b96bd0 Try and find groupname more accurately. fix #612 2012-11-11 16:47:00 +01:00
Ruud
655e847aeb Catch renamer.after event errors. fix #1032 2012-11-10 10:10:05 +01:00
Ruud
f3fd0afb42 Only do dvdr match when no quality is found. fix #1030 2012-11-09 17:30:32 +01:00
Ruud
3782ad7f98 Added dksubs to ignore list 2012-11-07 22:26:59 +01:00
Frank Fenton
6f5031fa7c Add torrent file support to utorrent downloader 2012-11-08 03:14:52 +11:00
Frank Fenton
93604a45e5 Add uTorrent downloader 2012-11-07 23:13:44 +11:00
Ruud
28f4169e44 Allow csv file from imdb.com. closes #1017 2012-11-05 17:56:44 +01:00
Ruud
2361057e4c Allow multiple getImdb 2012-11-05 17:55:45 +01:00
Ruud
5caa40bd81 Mapped audio codecs to renamer. closes #993
Thanks @clinton-hall
2012-11-04 00:23:36 +01:00
Ruud
a22bd4abd4 First start improvements 2012-11-03 22:36:25 +01:00
Ruud
a32ba7a763 Disable nzbclub by default 2012-11-03 22:01:13 +01:00
Ruud
5fe645cc11 Return boolean properly 2012-11-03 21:47:37 +01:00
Ruud
f333d85907 Added initscript for ffpstick closes #992
Thanks to @MariusRugan
2012-11-02 18:41:10 +01:00
Ruud
3ec2df5780 Fedora init fix #1009 2012-11-02 18:37:42 +01:00
Ruud
25f1b8c7a7 Fedora init fix #1009 2012-11-02 18:32:15 +01:00
Ruud
e71da1f14d Use proper description for binary build. fix #1005 2012-11-02 18:24:13 +01:00
Ruud
212d64143c Ignore extracting folder. fix #1004 2012-11-02 18:16:03 +01:00
Ruud
51f9b5c673 Clear queu tasks. fix #997 2012-11-02 17:28:39 +01:00
Ruud
2215c000b7 Return everything but None 2012-10-30 22:42:11 +01:00
Ruud
14797249ff NZBClub RSS doesn't support https. fix #991 2012-10-30 20:01:32 +01:00
Ruud
49e2607f5d One up 2012-10-29 21:04:38 +01:00
Ruud
938b14ba18 One up installer 2012-10-29 20:45:17 +01:00
Ruud
c893d5bbb8 import cleanup 2012-10-29 16:36:02 +01:00
Ruud
c4adab69cb Just use basename of __file__ when restarting. fix #982 2012-10-29 16:32:35 +01:00
Ruud
2c9af74f7f Add newznab host to found log 2012-10-29 16:23:55 +01:00
Ruud
7eb15c1a53 Add less important info log 2012-10-29 16:07:18 +01:00
Ruud
a02257a906 Enable both nzb and torrent by default
Use default user download dir and enable blackhole
2012-10-29 16:06:32 +01:00
Ruud
667075a006 Ctrl+c proper shutdown 2012-10-29 11:29:08 +01:00
Ruud
b0f6f9b2ea Update Flask 2012-10-29 10:54:38 +01:00
Ruud
c0900cfe94 Open up browser in readme 2012-10-27 19:14:00 +02:00
bfagundez
24a4810919 Modified the default url to highlight 2012-10-27 19:10:54 +02:00
bfagundez
70b15a5696 Added the url to open after installation, to avoid searching for 10 minutes the default port. 2012-10-27 19:10:54 +02:00
Ruud
d6522d8f38 One up installer 2012-10-27 18:49:44 +02:00
Ruud
78eab890e7 Merge branch 'refs/heads/develop' into desktop 2012-10-27 18:25:36 +02:00
Ruud
35c0356734 Let esky manage it's own restart 2012-10-27 18:25:27 +02:00
Ruud
1a56191f83 Don't unzip 2012-10-27 18:22:50 +02:00
Ruud
41c0f34d95 Properly restart 2012-10-27 18:22:40 +02:00
Ruud
37bf205d7a Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2012-10-27 11:56:57 +02:00
Ruud
b66af0b6c6 One up 2012-10-27 11:52:59 +02:00
Ruud
d1e798323c SAB error doesn't show anything. Add 1 more line 2012-10-26 23:50:59 +02:00
Ruud
32fe3796e4 Merge branch 'refs/heads/develop' 2012-10-26 22:22:47 +02:00
Ruud
7420785eaf Show branch on about page 2012-10-26 22:14:48 +02:00
Ruud
a86522a810 Don't download next when the release isn't found in downloaded. fix #924 2012-10-26 22:02:34 +02:00
Ruud
359d1aaafa Merge branch 'refs/heads/develop' 2012-10-26 14:54:12 +02:00
Ruud
d636314971 Contributing.md 2012-10-26 14:53:35 +02:00
Ruud
fb5d336351 Merge branch 'refs/heads/develop' 2012-10-26 14:36:04 +02:00
Ruud
e918e6b12f Check watchlist adds in automation plugin, not the providers. fix #838 2012-10-23 00:20:04 +02:00
Ruud
b71f003ad8 Use secure connections when available. 2012-10-22 22:22:52 +02:00
Ruud
a432ad4f5a Use new kat.ph url. fix #959
Also use https while we're at it.
2012-10-22 21:42:42 +02:00
Ruud
ac04121dd3 Just use q as name in imdbapi search. 2012-10-21 20:49:54 +02:00
Ruud
61a3a0386e Search after added all movies. fix #702 2012-10-21 20:02:24 +02:00
Ruud
7b1f17c062 Limit threads per event 2012-10-21 20:02:06 +02:00
Ruud
6f7b565103 Use multiple tag when renaming extras. fix #652 2012-10-21 18:07:14 +02:00
Ruud
4fb7467e97 Send 2 tweets when message is above 140 chars. fix #441 2012-10-21 16:14:11 +02:00
Ruud
bcdc633a5e Use urlopen for Prowl notifier. fix #932 2012-10-21 15:26:29 +02:00
Ruud
19f74e398f Commit before adding more info 2012-10-21 14:38:58 +02:00
Ruud
09f723bda5 Use jsonrpc for xbmc request. fix #927 #945 2012-10-21 13:37:45 +02:00
Ruud
fbeadb8d9e Don't show "add your first movie" when searching. fix #937 2012-10-21 12:11:37 +02:00
Ruud
5bda44d419 Add api for file types 2012-10-20 11:30:10 +02:00
Ruud
84eccbf9cf Chmod metadata. fix #928 2012-10-16 22:18:37 +02:00
Ruud
9ebc4dbf38 Reworked some download code. #924 2012-10-15 23:50:05 +02:00
Ruud
907f821e50 Some more logging for SABNZBd 2012-10-14 20:42:31 +02:00
Ruud
9dc1843f25 Also use data param on notifier 2012-10-14 20:42:12 +02:00
Ruud
ad0a1b1efe Pass all the renamed files when adding new release 2012-10-14 20:26:10 +02:00
Ruud
8bfad087e1 Add podnapisi to subtitle providers 2012-10-14 20:17:13 +02:00
Ruud
67c87444de Subliminal update 2012-10-14 17:58:09 +02:00
Ruud
4dfd8b4cd5 Don't show trailer 404 errors 2012-10-14 17:32:45 +02:00
Ruud
3ffc6e122e Double genres because of trailing spaces 2012-10-14 15:28:26 +02:00
Ruud
f204309ed7 Remove unused file before counting them 2012-10-14 14:37:02 +02:00
Ruud
d3ebe531d5 Don't use 2 events after rename 2012-10-14 14:13:10 +02:00
Ruud
6106fd4e82 Only scroll back to top when added 2012-10-13 21:19:56 +02:00
Ruud
981ba61458 Progress bars for manage updates 2012-10-13 21:17:42 +02:00
Ruud
a5534c4bd2 Create file types on load 2012-10-13 21:16:05 +02:00
Ruud
2cd887b70a Empty list styling 2012-10-13 12:00:47 +02:00
Ruud
19ddd03204 Improved movie year matching 2012-10-13 11:23:13 +02:00
Ruud
d0d9ac07a6 CSS gradient "fix" 2012-10-13 10:52:35 +02:00
Ruud
eb30dff986 Merge branch 'refs/heads/develop' 2012-10-13 00:00:44 +02:00
Ruud
0773d6e6ad Merge branch 'develop' of https://github.com/clinton-hall/CouchPotatoServer into clinton-hall-develop 2012-10-12 23:35:15 +02:00
Ruud
1011e2e9b8 Show "how to" when wanted list is empty 2012-10-12 23:34:39 +02:00
Ruud
84f5dcc134 Wizard fix 2012-10-12 23:34:13 +02:00
clinton-hall
fef3eb1b84 Added replacement of uid and key for nzbsrus
Prevent people posting log files with their account information
2012-10-12 13:29:41 -07:00
Ruud
ef6d0e04c0 Use event value 2012-10-12 21:04:49 +02:00
Ruud
269e98b049 Removed all commented out db.close 2012-10-12 19:09:57 +02:00
Ruud
378d1ccd1c Check if db exists before loading plugins 2012-10-12 19:03:44 +02:00
Ruud
f3e3632dd3 Use less db.commits when adding quality and profiles. 2012-10-12 18:52:44 +02:00
Ruud
27635caa1d Simpler options for events 2012-10-12 18:50:57 +02:00
Ruud
4836a9ffdc Merge branch 'fix_scoring' of https://github.com/dersphere/CouchPotatoServer into dersphere-fix_scoring 2012-10-12 13:21:07 +02:00
Ruud
8874bd4e2b Wrongly assuming quality when no quality in the name. fix #901 2012-10-12 13:15:40 +02:00
Tristan Fischer
799b665f15 fix audio scoring
The elements of name_scores are compared to a lower-cased version of the release name so they need to be also lower cased.
2012-10-08 23:30:10 +02:00
Ruud
7f90135947 checksnatched debug code leftover. fix #892 2012-10-02 22:16:21 +02:00
Ruud
50a2bca459 Don't open releases when all are ignored 2012-09-30 17:34:09 +02:00
Ruud
a3b3b9c218 Don't use cached in_wanted when re-adding movie 2012-09-30 17:33:51 +02:00
Ruud
d38bd03422 Use finish by default when adding new type 2012-09-30 17:15:47 +02:00
Ruud
9184a97fcd Don't loop over releases when download doesn't support check snatched. fix #887 2012-09-30 15:56:51 +02:00
Ruud
ce0bf7b51a Standardized checking snatched 2012-09-28 13:10:58 +02:00
Ruud
151b100573 Merge branch 'develop' of https://github.com/clinton-hall/CouchPotatoServer into clinton-hall-develop 2012-09-28 08:35:32 +02:00
Ruud
0e23413069 Mark movie snatched on manual download 2012-09-27 08:39:07 +02:00
Ruud
2ac2b0ff06 Remove debug sleep 2012-09-26 21:12:14 +02:00
Ruud
86bf08cbd4 Add documentation to progress api 2012-09-26 21:01:48 +02:00
Ruud
ed0e54d64d Add force full search options to wanted list 2012-09-26 20:59:03 +02:00
Ruud
3da0b1a804 Let user know to report weird errors 2012-09-26 20:12:03 +02:00
Ruud
39c2567d5a Also listen to search ended per movie. 2012-09-26 20:11:19 +02:00
Ruud
95c5d16991 Don't check retention when it's 0 2012-09-26 19:35:59 +02:00
Ruud
08b450fc0a Remove trailer feature not implemented yet. 2012-09-26 17:10:34 +02:00
clinton-hall
dc63796e48 Added nzbname
previously defined in Downloader. I forgot to bring this across.
2012-09-26 17:12:19 +09:30
clinton-hall
c6cba2f6e5 Only request SABnzbd status once
Performs the checking of queue and history here. Requests delete of failed downlaods via download.remove event.
2012-09-26 16:59:36 +09:30
clinton-hall
ef945597d2 Removed checking of status results from here
getDownloadStatus is only called once from renamer and all results are passed back. Def remove is added so that renamer can request a failed downlaod to be deleted from SABnzbd if enabled.
2012-09-26 16:51:51 +09:30
clinton-hall
ba36c738c7 Added def remove
Allows renamer to request deletion of failed downloads
2012-09-26 16:46:10 +09:30
Ruud
3a3a4fb1f3 Cleanup javascript events on movie delete 2012-09-25 23:11:27 +02:00
Ruud
3fa352e7c8 Firefox fix for directory input 2012-09-25 19:02:28 +02:00
Ruud
08ef153bbf remove dropdown arrow 2012-09-25 16:03:31 +02:00
Ruud
7e3a6eeb83 Make sure top quality is always checked. 2012-09-25 15:47:50 +02:00
Ruud
24ad975917 Movie providers in their own subgroup 2012-09-25 15:09:35 +02:00
Ruud
952f29918e Hide git command when not using git
Re-order automation
2012-09-25 14:30:35 +02:00
Ruud
08ae51dbe6 Improved folder select 2012-09-25 14:22:48 +02:00
Ruud
d40ad1ddf2 Improved wizard 2012-09-25 12:43:22 +02:00
Ruud
0132012276 Hide ip change 2012-09-25 11:10:49 +02:00
Ruud
0f1e8eeff9 Make sure to always search for old movies 2012-09-25 10:43:46 +02:00
Ruud
8eee2af49b Renamer fileinput. fix #861 2012-09-24 22:23:15 +02:00
Ruud
3d26a53fbd Don't capitalize labels in settings 2012-09-24 21:59:36 +02:00
Ruud
d4600635e1 Use sudo in readme. Thanks jbillo 2012-09-24 17:35:13 +02:00
Ruud
9312336962 Merge branch 'refs/heads/develop' 2012-09-24 09:36:59 +02:00
Ruud
6fc9d383de Add some error handling to sabnzbd statuscheck 2012-09-22 09:20:37 +02:00
Ruud
5776b2caad Convert torrent hash to uppercase 2012-09-22 09:05:44 +02:00
Ruud
f82e2a3e6e Limit sabnzbd history check. 2012-09-21 12:38:38 +02:00
Ruud
a5fa503970 Copy paste error. 2012-09-21 12:33:22 +02:00
Ruud
6f7d2caa9b Properly update release dates. 2012-09-21 12:16:36 +02:00
Ruud
c0012c9243 Properly use newer_than. fix #850 2012-09-20 11:25:38 +02:00
Ruud
5cc7250528 Image optimize 2012-09-20 09:16:26 +02:00
Ruud
aa1fa3eb9a Add description 2012-09-19 15:42:33 +02:00
Ruud
f474962225 Just check for synoindex file on test. 2012-09-19 15:31:27 +02:00
Ruud
0e2f8a612c Extract zip after build, for testing 2012-09-19 15:29:07 +02:00
Ruud
ad7de32e70 Send time with API requests 2012-09-19 13:23:40 +02:00
Ruud
d295b881af Use new CP url for api 2012-09-16 21:58:58 +02:00
Ruud
ade4338ea6 Merge branch 'refs/heads/develop' 2012-09-16 21:32:16 +02:00
Ruud
20f81d06c0 Send headers with CPAPI 2012-09-16 20:44:10 +02:00
Ruud
8fb24bb101 Couldn't test Synoindex. fix #814 2012-09-16 20:20:47 +02:00
Ruud
ce3efd3a3c Cleanup cache on startup 2012-09-16 20:02:15 +02:00
Ruud
ef7fc62c66 Download fanart en bigger poster when needed 2012-09-16 19:25:53 +02:00
Ruud
4be8d02cbb Added CP search & info provider 2012-09-16 17:47:14 +02:00
Ruud
7ce8a4fc45 Show proper date in update info 2012-09-16 13:21:01 +02:00
Ruud
55b20324c0 Merge branch 'refs/heads/develop' 2012-09-16 12:36:48 +02:00
Ruud
c0fb28301d Merge branch 'refs/heads/develop'
Conflicts:
	version.py
2012-09-16 10:46:39 +02:00
Ruud
f9c2503f81 Merge branch 'refs/heads/develop' 2012-09-14 13:15:35 +02:00
Ruud
5b4cdf05b1 Merge branch 'refs/heads/develop' 2012-09-14 13:06:56 +02:00
Ruud
6f25a6bdfd Merge branch 'refs/heads/develop' 2012-09-03 10:32:09 +02:00
Ruud
23427e95f7 Merge branch 'refs/heads/develop' 2012-08-26 23:09:51 +02:00
Ruud
90a09e573b Merge branch 'refs/heads/develop'
Conflicts:
	couchpotato/core/_base/updater/main.py
2012-08-05 16:15:53 +02:00
Ruud
e1d7440b9d Wrong branch in master 2012-07-15 00:23:44 +02:00
562 changed files with 35805 additions and 7632 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):
@@ -100,7 +99,7 @@ class Loader(object):
logging.shutdown()
time.sleep(3)
args = [sys.executable] + [os.path.join(base_path, __file__)] + sys.argv[1:]
args = [sys.executable] + [os.path.join(base_path, os.path.basename(__file__))] + sys.argv[1:]
subprocess.Popen(args)
except:
self.log.critical(traceback.format_exc())

View File

@@ -1,12 +1,12 @@
from esky.util import appdir_from_executable #@UnresolvedImport
from threading import Thread
from version import VERSION
from wx.lib.softwareupdate import SoftwareUpdate
import os
import sys
import time
import webbrowser
import wx
import subprocess
# Include proper dirs
if hasattr(sys, 'frozen'):
@@ -166,8 +166,8 @@ class CouchPotatoApp(wx.App, SoftwareUpdate):
def OnInit(self):
# Updater
base_url = 'http://couchpota.to/updates/'
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)
@@ -214,5 +214,18 @@ if __name__ == '__main__':
time.sleep(1)
if app.restart:
args = [sys.executable] + [os.path.join(base_path, 'Desktop.py')] + sys.argv[1:]
subprocess.Popen(args)
def appexe_from_executable(exepath):
appdir = appdir_from_executable(exepath)
exename = os.path.basename(exepath)
if sys.platform == "darwin":
if os.path.isdir(os.path.join(appdir, "Contents", "MacOS")):
return os.path.join(appdir, "Contents", "MacOS", exename)
return os.path.join(appdir, exename)
exe = appexe_from_executable(sys.executable)
os.chdir(os.path.dirname(exe))
os.execv(exe, [exe] + sys.argv[1:])

View File

@@ -17,6 +17,7 @@ Windows, see [the CP forum](http://couchpota.to/forum/showthread.php?tid=14) for
* Open up `Git Bash` (or CMD) and go to the folder you want to install CP. Something like Program Files.
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`.
* You can now start CP via `CouchPotatoServer\CouchPotato.py` to start
* Your browser should open up, but if it doesn't go to: `http://localhost:5050/`
OSx:
@@ -26,6 +27,7 @@ OSx:
* Go to your App folder `cd /Applications`
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
* Then do `python CouchPotatoServer/CouchPotato.py`
* Your browser should open up, but if it doesn't go to: `http://localhost:5050/`
Linux (ubuntu / debian):
@@ -33,7 +35,8 @@ Linux (ubuntu / debian):
* 'cd' to the folder of your choosing.
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
* Then do `python CouchPotatoServer/CouchPotato.py` to start
* To run on boot copy the init script. `cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato`
* Change the paths inside the init script. `nano /etc/init.d/couchpotato`
* Make it executable. `chmod +x /etc/init.d/couchpotato`
* To run on boot copy the init script. `sudo cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato`
* Change the paths inside the init script. `sudo nano /etc/init.d/couchpotato`
* Make it executable. `sudo chmod +x /etc/init.d/couchpotato`
* Add it to defaults. `sudo update-rc.d couchpotato defaults`
* Open your browser and go to: `http://localhost:5050/`

15
contributing.md Normal file
View File

@@ -0,0 +1,15 @@
#So you feel like posting a bug, sending me a pull request or just telling me how awesome I am. No problem!
##Just make sure you think of the following things:
* 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 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 ;)
**If I don't get enough info, the change of the issue getting closed is a lot bigger ;)**

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,19 +23,22 @@ config = [{
'default': '',
'type': 'password',
},
{
'name': 'host',
'advanced': True,
'default': '0.0.0.0',
'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

@@ -9,6 +9,7 @@ from tornado.ioloop import IOLoop
from uuid import uuid4
import os
import platform
import signal
import time
import traceback
import webbrowser
@@ -51,6 +52,9 @@ class Core(Plugin):
addEvent('setting.save.core.password', self.md5Password)
addEvent('setting.save.core.api_key', self.checkApikey)
# Make sure we can close-down with ctrl+c properly
if not Env.get('desktop'):
self.signalHandler()
def md5Password(self, value):
return md5(value.encode(Env.get('encoding'))) if value else ''
@@ -66,7 +70,7 @@ class Core(Plugin):
def available(self):
return jsonified({
'succes': True
'success': True
})
def shutdown(self):
@@ -98,7 +102,7 @@ class Core(Plugin):
self.shutdown_started = True
fireEvent('app.shutdown')
fireEvent('app.do_shutdown')
log.debug('Every plugin got shutdown event')
loop = True
@@ -148,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')
@@ -170,3 +174,12 @@ class Core(Plugin):
return jsonified({
'version': self.version()
})
def signalHandler(self):
if Env.get('daemonized'): return
def signal_handler(signal, frame):
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

@@ -1,4 +1,6 @@
from .main import Updater
from couchpotato.environment import Env
import os
def start():
return Updater()
@@ -33,6 +35,7 @@ config = [{
{
'name': 'git_command',
'default': 'git',
'hidden': not os.path.isdir(os.path.join(Env.get('app_dir'), '.git')),
'advanced': True
},
],

View File

@@ -106,6 +106,10 @@ class Updater(Plugin):
if success:
fireEventAsync('app.restart')
# Assume the updater handles things
if not success:
success = True
return jsonified({
'success': success
})
@@ -396,6 +400,7 @@ class DesktopUpdater(BaseUpdater):
self.update_failed = True
self.desktop._esky.auto_update(callback = do_restart)
return
except:
self.update_failed = True
@@ -406,7 +411,7 @@ class DesktopUpdater(BaseUpdater):
'last_check': self.last_check,
'update_version': self.update_version,
'version': self.getVersion(),
'branch': 'desktop_build',
'branch': self.branch,
}
def check(self):

View File

@@ -90,17 +90,18 @@ var UpdaterBase = new Class({
doUpdate: function(){
var self = this;
App.blockPage('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
Api.request('updater.update', {
'onComplete': function(json){
if(json.success){
if(json.success)
self.updating();
}
else
App.unBlockPage()
}
});
},
updating: function(){
App.blockPage('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
App.checkAvailable.delay(500, App, [1000, function(){
window.location.reload();
}]);

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,51 +1,76 @@
from base64 import b32decode, b16encode
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toSafeString
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
import os
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',
'http://torrage.ws/torrent/%s.torrent',
'http://torcache.net/torrent/%s.torrent',
]
torrent_trackers = [
'http://tracker.publicbt.com/announce',
'udp://tracker.istole.it:80/announce',
'udp://fr33domtracker.h33t.com:3310/announce',
'http://tracker.istole.it/announce',
'http://tracker.ccc.de/announce',
'udp://tracker.publicbt.com:80/announce',
'udp://tracker.ccc.de:80/announce',
'http://exodus.desync.com/announce',
'http://exodus.desync.com:6969/announce',
'http://tracker.publichd.eu/announce',
'http://tracker.openbittorrent.com/announce',
]
def __init__(self):
addEvent('download', self.download)
addEvent('download.status', self.getDownloadStatus)
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)
def getDownloadStatus(self, data = {}, movie = {}):
return False
def createNzbName(self, data, movie):
tag = self.cpTag(movie)
return '%s%s' % (toSafeString(data.get('name')[:127 - len(tag)]), tag)
def createFileName(self, data, filedata, movie):
name = os.path.join(self.createNzbName(data, movie))
if data.get('type') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata:
return '%s.%s' % (name, 'rar')
return '%s.%s' % (name, data.get('type'))
def cpTag(self, movie):
if Env.setting('enabled', 'renamer'):
return '.cp(' + movie['library'].get('identifier') + ')' if movie['library'].get('identifier') else ''
return ''
def removeFailed(self, item):
return
def isCorrectType(self, item_type):
is_correct = item_type in self.type
@@ -56,7 +81,7 @@ class Downloader(Plugin):
return is_correct
def magnetToTorrent(self, magnet_link):
torrent_hash = re.findall('urn:btih:([\w]{32,40})', magnet_link)[0]
torrent_hash = re.findall('urn:btih:([\w]{32,40})', magnet_link)[0].upper()
# Convert base 32 to hex
if len(torrent_hash) == 32:
@@ -78,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

@@ -1,4 +1,5 @@
from .main import Blackhole
from couchpotato.core.helpers.variable import getDownloadDir
def start():
return Blackhole()
@@ -9,6 +10,7 @@ config = [{
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'blackhole',
'label': 'Black hole',
'description': 'Download the NZB/Torrent to a specific folder.',
@@ -16,7 +18,7 @@ config = [{
'options': [
{
'name': 'enabled',
'default': 0,
'default': True,
'type': 'enabler',
'radio_group': 'nzb,torrent',
},
@@ -24,6 +26,7 @@ config = [{
'name': 'directory',
'type': 'directory',
'description': 'Directory where the .nzb (or .torrent) file is saved to.',
'default': getDownloadDir()
},
{
'name': 'use_for',

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,10 +8,10 @@ config = [{
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'nzbget',
'label': 'NZBGet',
'description': 'Send NZBs to your NZBGet installation.',
'wizard': True,
'description': 'Use <a href="http://nzbget.sourceforge.net/Main_Page" target="_blank">NZBGet</a> to download NZBs.',
'options': [
{
'name': 'enabled',
@@ -26,6 +26,7 @@ config = [{
},
{
'name': 'password',
'type': 'password',
'description': 'Default NZBGet password is <i>tegbzn6789</i>',
},
{
@@ -33,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,10 +9,10 @@ config = [{
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'pneumatic',
'label': 'Pneumatic',
'description': 'Download the .strm file to a specific folder.',
'wizard': True,
'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,9 +1,11 @@
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.helpers.encoding import tryUrlencode, ss
from couchpotato.core.helpers.variable import cleanHost, mergeDicts
from couchpotato.core.logger import CPLog
import traceback
from couchpotato.environment import Env
from urllib2 import URLError
import json
import traceback
log = CPLog(__name__)
@@ -11,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'))
@@ -27,7 +26,7 @@ class Sabnzbd(Downloader):
if filedata:
if len(filedata) < 50:
log.error('No proper nzb available!')
log.error('No proper nzb available: %s', (filedata))
return False
# If it's a .rar, it adds the .rar extension, otherwise it stays .nzb
@@ -36,126 +35,119 @@ class Sabnzbd(Downloader):
else:
params['name'] = data.get('url')
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(params)
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
except:
log.error('Failed sending release: %s', traceback.format_exc())
log.error('Failed sending release, use API key, NOT the NZB key: %s', traceback.format_exc(0))
return False
result = sab.strip()
if not result:
log.error("SABnzbd didn't return anything.")
log.error('SABnzbd didn\'t return anything.')
return False
log.debug("Result text from SAB: " + result[:40])
if result == "ok":
log.info("NZB sent to SAB successfully.")
log.debug('Result text from SAB: %s', result[:40])
if result[:2] == 'ok':
log.info('NZB sent to SAB successfully.')
return True
elif result == "Missing authentication":
log.error("Incorrect username/password.")
return False
else:
log.error("Unknown error: " + result[:40])
log.error(result[:40])
return False
def getDownloadStatus(self, data = {}, movie = {}):
if self.isDisabled(manual = True) or not self.isCorrectType(data.get('type')):
return
def getAllDownloadStatus(self):
nzbname = self.createNzbName(data, movie)
log.info('Checking download status of "%s" at SABnzbd.', nzbname)
log.debug('Checking SABnzbd download status.')
# Go through Queue
params = {
'apikey': self.conf('api_key'),
'mode': 'queue',
'output': 'json'
}
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
try:
sab = self.urlopen(url, timeout = 60, show_error = False)
queue = self.call({
'mode': 'queue',
})
except:
log.error('Failed checking status: %s', traceback.format_exc())
log.error('Failed getting queue: %s', traceback.format_exc(1))
return False
try:
history = json.loads(sab)
except:
log.debug("Result text from SAB: " + sab[:40])
log.error('Failed parsing json status: %s', traceback.format_exc())
return False
for slot in history['queue']['slots']:
log.debug('Found %s in SabNZBd queue, which is %s, with %s left', (slot['filename'], slot['status'], slot['timeleft']))
if slot['filename'] == nzbname:
return slot['status'].lower()
# Go through history items
params = {
'apikey': self.conf('api_key'),
'mode': 'history',
'output': 'json'
}
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
try:
history = self.call({
'mode': 'history',
'limit': 15,
})
except:
log.error('Failed getting history json: %s', traceback.format_exc(1))
return False
statuses = []
# Get busy releases
for item in queue.get('slots', []):
statuses.append({
'id': item['nzo_id'],
'name': item['filename'],
'status': 'busy',
'original_status': item['status'],
'timeleft': item['timeleft'] if not queue['paused'] else -1,
})
# Get old releases
for item in history.get('slots', []):
status = 'busy'
if item['status'] == 'Failed' or (item['status'] == 'Completed' and item['fail_message'].strip()):
status = 'failed'
elif item['status'] == 'Completed':
status = 'completed'
statuses.append({
'id': item['nzo_id'],
'name': item['name'],
'status': status,
'original_status': item['status'],
'timeleft': 0,
})
return statuses
def removeFailed(self, item):
log.info('%s failed downloading, deleting...', item['name'])
try:
sab = self.urlopen(url, timeout = 60, show_error = False)
self.call({
'mode': 'history',
'name': 'delete',
'del_files': '1',
'value': item['id']
}, use_json = False)
except:
log.error('Failed getting history: %s', traceback.format_exc())
return
log.error('Failed deleting: %s', traceback.format_exc(0))
return False
try:
history = json.loads(sab)
except:
log.debug("Result text from SAB: " + sab[:40])
log.error('Failed parsing history json: %s', traceback.format_exc())
return
return True
for slot in history['history']['slots']:
log.debug('Found %s in SabNZBd history, which has %s', (slot['name'], slot['status']))
if slot['name'] == nzbname:
# Note: if post process even if failed is on in SabNZBd, it will complete with a fail message
if slot['status'] == 'Failed' or (slot['status'] == 'Completed' and slot['fail_message'].strip()):
def call(self, params, use_json = True):
# Delete failed download
if self.conf('delete_failed', default = True):
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(params, {
'apikey': self.conf('api_key'),
'output': 'json'
}))
log.info('%s failed downloading, deleting...', slot['name'])
params = {
'apikey': self.conf('api_key'),
'mode': 'history',
'name': 'delete',
'del_files': '1',
'value': slot['nzo_id']
}
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
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'):
log.error('Error getting data from SABNZBd: %s', d.get('error'))
return {}
try:
sab = self.urlopen(url, timeout = 60, show_error = False)
except:
log.error('Failed deleting: %s', traceback.format_exc())
return False
return d[params['mode']]
else:
return data
result = sab.strip()
if not result:
log.error("SABnzbd didn't return anything.")
log.debug("Result text from SAB: " + result[:40])
if result == "ok":
log.info('SabNZBd deleted failed release %s successfully.', slot['name'])
elif result == "Missing authentication":
log.error("Incorrect username/password or API?.")
else:
log.error("Unknown error: " + result[:40])
return 'failed'
else:
return slot['status'].lower()
return 'not_found'

View File

@@ -0,0 +1,45 @@
from .main import Synology
def start():
return Synology()
config = [{
'name': 'synology',
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'synology',
'label': 'Synology',
'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': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
'radio_group': 'torrent',
},
{
'name': 'host',
'default': 'localhost:5000',
'description': 'Hostname with port. Usually <strong>localhost:5000</strong>',
},
{
'name': 'username',
},
{
'name': 'password',
'type': 'password',
},
{
'name': 'manual',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
},
],
}
],
}]

View File

@@ -0,0 +1,105 @@
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import isInt
from couchpotato.core.logger import CPLog
import httplib
import json
import urllib
import urllib2
log = CPLog(__name__)
class Synology(Downloader):
type = ['torrent_magnet']
log = CPLog(__name__)
def download(self, data, movie, filedata = None):
log.error('Sending "%s" (%s) to Synology.', (data.get('name'), data.get('type')))
# 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
if data.get('type') == 'torrent':
log.error('Can\'t add binary torrent file')
return False
try:
# Send request to Transmission
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
remote_torrent = srpc.add_torrent_uri(data.get('url'))
log.info('Response: %s', remote_torrent)
return remote_torrent['success']
except Exception, err:
log.error('Exception while adding torrent: %s', err)
return False
class SynologyRPC(object):
'''SynologyRPC lite library'''
def __init__(self, host = 'localhost', port = 5000, username = None, password = None):
super(SynologyRPC, self).__init__()
self.download_url = 'http://%s:%s/webapi/DownloadStation/task.cgi' % (host, port)
self.auth_url = 'http://%s:%s/webapi/auth.cgi' % (host, port)
self.username = username
self.password = password
self.session_name = 'DownloadStation'
def _login(self):
if self.username and self.password:
args = {'api': 'SYNO.API.Auth', 'account': self.username, 'passwd': self.password, 'version': 2,
'method': 'login', 'session': self.session_name, 'format': 'sid'}
response = self._req(self.auth_url, args)
if response['success'] == True:
self.sid = response['data']['sid']
log.debug('Sid=%s', self.sid)
return response
elif self.username or self.password:
log.error('User or password missing, not using authentication.')
return False
def _logout(self):
args = {'api':'SYNO.API.Auth', 'version':1, 'method':'logout', 'session':self.session_name, '_sid':self.sid}
return self._req(self.auth_url, args)
def _req(self, url, args):
req_url = url + '?' + urllib.urlencode(args)
try:
req_open = urllib2.urlopen(req_url)
response = json.loads(req_open.read())
if response['success'] == True:
log.info('Synology action successfull')
return response
except httplib.InvalidURL, err:
log.error('Invalid Transmission host, check your config %s', err)
return False
except urllib2.HTTPError, err:
log.error('SynologyRPC HTTPError: %s', err)
return False
except urllib2.URLError, err:
log.error('Unable to connect to Synology %s', err)
return False
def add_torrent_uri(self, torrent):
log.info('Adding torrent URL %s', torrent)
response = {}
# login
login = self._login()
if len(login) > 0 and login['success'] == True:
log.info('Login success, adding torrent')
args = {'api':'SYNO.DownloadStation.Task', 'version':1, 'method':'create', 'uri':torrent, '_sid':self.sid}
response = self._req(self.download_url, args)
self._logout()
else:
log.error('Couldn\'t login to Synology, %s', login)
return response

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')))
@@ -30,15 +27,23 @@ class Transmission(Downloader):
return False
# Set parameters for Transmission
folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1]
folder_path = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep)
# Create the empty folder to download too
self.makeDir(folder_path)
params = {
'paused': self.conf('paused', default = 0),
'download-dir': self.conf('directory', default = '').rstrip(os.path.sep)
'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')
@@ -49,11 +54,13 @@ class Transmission(Downloader):
trpc = TransmissionRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
if data.get('type') == 'torrent_magnet':
remote_torrent = trpc.add_torrent_uri(data.get('url'), arguments = params)
torrent_params['trackerAdd'] = self.torrent_trackers
else:
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

@@ -0,0 +1,55 @@
from .main import uTorrent
def start():
return uTorrent()
config = [{
'name': 'utorrent',
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'utorrent',
'label': 'uTorrent',
'description': 'Use <a href="http://www.utorrent.com/" target="_blank">uTorrent</a> to download torrents.',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
'radio_group': 'torrent',
},
{
'name': 'host',
'default': 'localhost:8000',
'description': 'Hostname with port. Usually <strong>localhost:8000</strong>',
},
{
'name': 'username',
},
{
'name': 'password',
'type': 'password',
},
{
'name': 'label',
'description': 'Label to add torrent as.',
},
{
'name': 'paused',
'type': 'bool',
'default': False,
'description': 'Add the torrent paused.',
},
{
'name': 'manual',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
},
],
}
],
}]

View File

@@ -0,0 +1,197 @@
from base64 import b16encode, b32decode
from bencode import bencode, bdecode
from couchpotato.core.downloaders.base import Downloader
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
import urllib2
log = CPLog(__name__)
class uTorrent(Downloader):
type = ['torrent', 'torrent_magnet']
utorrent_api = None
def download(self, data, movie, filedata = None):
log.debug('Sending "%s" (%s) to uTorrent.', (data.get('name'), data.get('type')))
# 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
torrent_params = {}
if self.conf('label'):
torrent_params['label'] = self.conf('label')
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)
else:
info = bdecode(filedata)["info"]
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:
self.utorrent_api = uTorrentAPI(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
if data.get('type') == 'torrent_magnet':
self.utorrent_api.add_torrent_uri(data.get('url'))
else:
self.utorrent_api.add_torrent_file(torrent_filename, filedata)
# Change settings of added torrents
self.utorrent_api.set_torrent(torrent_hash, torrent_params)
if self.conf('paused', default = 0):
self.utorrent_api.pause_torrent(torrent_hash)
return True
except Exception, err:
log.error('Failed to send torrent to uTorrent: %s', err)
return False
def getAllDownloadStatus(self):
log.debug('Checking uTorrent download status.')
# Load host from config and split out port.
host = self.conf('host').split(':')
if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.')
return False
try:
self.utorrent_api = uTorrentAPI(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
except Exception, err:
log.error('Failed to get uTorrent object: %s', err)
return False
data = ''
try:
data = self.utorrent_api.get_status()
queue = json.loads(data)
if queue.get('error'):
log.error('Error getting data from uTorrent: %s', queue.get('error'))
return False
except Exception, err:
log.error('Failed to get status from uTorrent: %s', err)
return False
if queue.get('torrents', []) == []:
log.debug('Nothing in queue')
return False
statuses = []
# 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):
def __init__(self, host = 'localhost', port = 8000, username = None, password = None):
super(uTorrentAPI, self).__init__()
self.url = 'http://' + str(host) + ':' + str(port) + '/gui/'
self.token = ''
self.last_time = time.time()
cookies = cookielib.CookieJar()
self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), MultipartPostHandler)
self.opener.addheaders = [('User-agent', 'couchpotato-utorrent-client/1.0')]
if username and password:
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
password_manager.add_password(realm = None, uri = self.url, user = username, passwd = password)
self.opener.add_handler(urllib2.HTTPBasicAuthHandler(password_manager))
self.opener.add_handler(urllib2.HTTPDigestAuthHandler(password_manager))
elif username or password:
log.debug('User or password missing, not using authentication.')
self.token = self.get_token()
def _request(self, action, data = None):
if time.time() > self.last_time + 1800:
self.last_time = time.time()
self.token = self.get_token()
request = urllib2.Request(self.url + "?token=" + self.token + "&" + action, data)
try:
open_request = self.opener.open(request)
response = open_request.read()
if response:
return response
else:
log.debug('Unknown failure sending command to uTorrent. Return text is: %s', response)
except httplib.InvalidURL, err:
log.error('Invalid uTorrent host, check your config %s', err)
except urllib2.HTTPError, err:
if err.code == 401:
log.error('Invalid uTorrent Username or Password, check your config')
else:
log.error('uTorrent HTTPError: %s', err)
except urllib2.URLError, err:
log.error('Unable to connect to uTorrent %s', err)
return False
def get_token(self):
request = self.opener.open(self.url + "token.html")
token = re.findall("<div.*?>(.*?)</", request.read())[0]
return token
def add_torrent_uri(self, torrent):
action = "action=add-url&s=%s" % urllib.quote(torrent)
return self._request(action)
def add_torrent_file(self, filename, filedata):
action = "action=add-file"
return self._request(action, {"torrent_file": (ss(filename), filedata)})
def set_torrent(self, hash, params):
action = "action=setprops&hash=%s" % hash
for k, v in params.iteritems():
action += "&s=%s&v=%s" % (k, v)
return self._request(action)
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

@@ -12,14 +12,14 @@ def runHandler(name, handler, *args, **kwargs):
return handler(*args, **kwargs)
except:
from couchpotato.environment import Env
log.error('Error in event "%s", that wasn\'t caught: %s%s', (name, traceback.format_exc(), Env.all()))
log.error('Error in event "%s", that wasn\'t caught: %s%s', (name, traceback.format_exc(), Env.all() if not Env.get('dev') else ''))
def addEvent(name, handler, priority = 100):
if events.get(name):
e = events[name]
else:
e = events[name] = Event(name = name, threads = 20, exc_info = True, traceback = True, lock = threading.RLock())
e = events[name] = Event(name = name, threads = 10, exc_info = True, traceback = True, lock = threading.RLock())
def createHandle(*args, **kwargs):
@@ -46,49 +46,40 @@ def fireEvent(name, *args, **kwargs):
#log.debug('Firing event %s', name)
try:
# Fire after event
is_after_event = False
try:
del kwargs['is_after_event']
is_after_event = True
except: pass
options = {
'is_after_event': False, # Fire after event
'on_complete': False, # onComplete event
'single': False, # Return single handler
'merge': False, # Merge items
'in_order': False, # Fire them in specific order, waits for the other to finish
}
# onComplete event
on_complete = False
try:
on_complete = kwargs['on_complete']
del kwargs['on_complete']
except: pass
# Return single handler
single = False
try:
del kwargs['single']
single = True
except: pass
# Merge items
merge = False
try:
del kwargs['merge']
merge = True
except: pass
# Merge items
in_order = False
try:
del kwargs['in_order']
in_order = True
except: pass
# Do options
for x in options:
try:
val = kwargs[x]
del kwargs[x]
options[x] = val
except: pass
e = events[name]
if not in_order: e.lock.acquire()
e.asynchronous = False
e.in_order = in_order
result = e(*args, **kwargs)
if not in_order: e.lock.release()
if single and not merge:
# Lock this event
e.lock.acquire()
e.asynchronous = False
# Make sure only 1 event is fired at a time when order is wanted
kwargs['event_order_lock'] = threading.RLock() if options['in_order'] or options['single'] else None
kwargs['event_return_on_result'] = options['single']
# Fire
result = e(*args, **kwargs)
# Release lock for this event
e.lock.release()
if options['single'] and not options['merge']:
results = None
# Loop over results, stop when first not None result is found.
@@ -112,19 +103,22 @@ def fireEvent(name, *args, **kwargs):
errorHandler(r[1])
# Merge
if merge and len(results) > 0:
if options['merge'] and len(results) > 0:
results.reverse() # Priority 1 is higher then 100
# Dict
if type(results[0]) == dict:
if isinstance(results[0], dict):
merged = {}
for result in results:
merged = mergeDicts(merged, result)
results = merged
# Lists
elif type(results[0]) == list:
elif isinstance(results[0], list):
merged = []
for result in results:
merged += result
if result not in merged:
merged += result
results = merged
@@ -133,11 +127,11 @@ def fireEvent(name, *args, **kwargs):
log.debug('Return modified results for %s', name)
results = modified_results
if not is_after_event:
if not options['is_after_event']:
fireEvent('%s.after' % name, is_after_event = True)
if on_complete:
on_complete()
if options['on_complete']:
options['on_complete']()
return results
except KeyError, e:

View File

@@ -68,7 +68,7 @@ def tryUrlencode(s):
return new[1:]
else:
for letter in toUnicode(s):
for letter in ss(s):
try:
new += quote_plus(letter)
except:

View File

@@ -1,3 +1,4 @@
from couchpotato.core.helpers.encoding import simplifyString, toSafeString
from couchpotato.core.logger import CPLog
import hashlib
import os.path
@@ -9,15 +10,34 @@ import sys
log = CPLog(__name__)
def getUserDir():
try:
import pwd
os.environ['HOME'] = pwd.getpwuid(os.geteuid()).pw_dir
except:
pass
return os.path.expanduser('~')
def getDownloadDir():
user_dir = getUserDir()
# OSX
if 'darwin' in platform.platform().lower():
return os.path.join(user_dir, 'Downloads')
if os.name == 'nt':
return os.path.join(user_dir, 'Downloads')
return user_dir
def getDataDir():
# Windows
if os.name == 'nt':
return os.path.join(os.environ['APPDATA'], 'CouchPotato')
import pwd
os.environ['HOME'] = pwd.getpwuid(os.geteuid()).pw_dir
user_dir = os.path.expanduser('~')
user_dir = getUserDir()
# OSX
if 'darwin' in platform.platform().lower():
@@ -84,7 +104,7 @@ def cleanHost(host):
return host
def getImdb(txt, check_inside = True):
def getImdb(txt, check_inside = True, multiple = False):
if check_inside and os.path.isfile(txt):
output = open(txt, 'r')
@@ -92,8 +112,10 @@ def getImdb(txt, check_inside = True):
output.close()
try:
id = re.findall('(tt\d{7})', txt)[0]
return id
ids = re.findall('(tt\d{7})', txt)
if multiple:
return ids if len(ids) > 0 else []
return ids[0]
except IndexError:
pass
@@ -118,12 +140,32 @@ def getTitle(library_dict):
try:
return library_dict['titles'][0]['title']
except:
log.error('Could not get title for %s', library_dict['identifier'])
return None
try:
for title in library_dict.titles:
if title.default:
return title.title
except:
log.error('Could not get title for %s', library_dict.identifier)
return None
log.error('Could not get title for %s', library_dict['identifier'])
return None
except:
log.error('Could not get title for library item: %s', library_dict)
return None
def possibleTitles(raw_title):
titles = []
titles.append(toSafeString(raw_title).lower())
titles.append(raw_title.lower())
titles.append(simplifyString(raw_title))
return list(set(titles))
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)] 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

@@ -5,7 +5,7 @@ import traceback
class CPLog(object):
context = ''
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h']
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key']
def __init__(self, context = ''):
if context.endswith('.main'):
@@ -17,6 +17,9 @@ class CPLog(object):
def info(self, msg, replace_tuple = ()):
self.logger.info(self.addContext(msg, replace_tuple))
def info2(self, msg, replace_tuple = ()):
self.logger.log(19, self.addContext(msg, replace_tuple))
def debug(self, msg, replace_tuple = ()):
self.logger.debug(self.addContext(msg, replace_tuple))
@@ -53,7 +56,8 @@ class CPLog(object):
if not Env.get('dev'):
for replace in self.replace_private:
msg = re.sub('(%s=)[^\&]+' % replace, '%s=xxx' % replace, msg)
msg = re.sub('(\?%s=)[^\&]+' % replace, '?%s=xxx' % replace, msg)
msg = re.sub('(&%s=)[^\&]+' % replace, '&%s=xxx' % replace, msg)
# Replace api key
try:

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

@@ -14,7 +14,7 @@ class Notification(Plugin):
test_message = 'ZOMG Lazors Pewpewpew!'
listen_to = [
'movie.downloaded', 'movie.snatched',
'renamer.after', 'movie.snatched',
'updater.available', 'updater.updated',
]
dont_listen_to = []
@@ -30,10 +30,10 @@ class Notification(Plugin):
addEvent(listener, self.createNotifyHandler(listener))
def createNotifyHandler(self, listener):
def notify(message, data):
def notify(message = None, group = {}, data = None):
if not self.conf('on_snatch', default = True) and listener == 'movie.snatched':
return
return self.notify(message = message, data = data, listener = listener)
return self.notify(message = message, data = data if data else group, listener = listener)
return notify

View File

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

View File

@@ -3,7 +3,7 @@ from couchpotato.api import addApiView, addNonBlockApiView
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.helpers.variable import tryInt, splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from couchpotato.core.settings.model import Notification as Notif
@@ -22,7 +22,7 @@ class CoreNotifier(Notification):
listeners = []
listen_to = [
'movie.downloaded', 'movie.snatched',
'renamer.after', 'movie.snatched',
'updater.available', 'updater.updated',
]
@@ -67,7 +67,7 @@ class CoreNotifier(Notification):
ids = None
if getParam('ids'):
ids = [x.strip() for x in getParam('ids').split(',')]
ids = splitString(getParam('ids'))
db = get_session()
@@ -79,7 +79,6 @@ class CoreNotifier(Notification):
q.update({Notif.read: True})
db.commit()
#db.close()
return jsonified({
'success': True
@@ -93,7 +92,7 @@ class CoreNotifier(Notification):
q = db.query(Notif)
if limit_offset:
splt = [x.strip() for x in limit_offset.split(',')]
splt = splitString(limit_offset)
limit = splt[0]
offset = 0 if len(splt) is 1 else splt[1]
q = q.limit(limit).offset(offset)
@@ -107,7 +106,6 @@ class CoreNotifier(Notification):
ndict['type'] = 'notification'
notifications.append(ndict)
#db.close()
return jsonified({
'success': True,
'empty': len(notifications) == 0,
@@ -133,7 +131,6 @@ class CoreNotifier(Notification):
self.frontend(type = listener, data = data)
#db.close()
return True
def frontend(self, type = 'notification', data = {}, message = None):

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

@@ -69,7 +69,7 @@ class NMJ(Notification):
'mount': mount,
})
def addToLibrary(self, group = {}):
def addToLibrary(self, message = None, group = {}):
if self.isDisabled(): return
host = self.conf('host')
@@ -114,8 +114,8 @@ class NMJ(Notification):
def failed(self):
return jsonified({'success': False})
def test(self):
return jsonified({'success': self.addToLibrary()})

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

@@ -1,3 +1,4 @@
from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
import pynma
@@ -11,7 +12,7 @@ class NotifyMyAndroid(Notification):
if self.isDisabled(): return
nma = pynma.PyNMA()
keys = [x.strip() for x in self.conf('api_key').split(',')]
keys = splitString(self.conf('api_key'))
nma.addkey(keys)
nma.developerkey(self.conf('dev_key'))

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

@@ -1,3 +1,4 @@
from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from pynmwp import PyNMWP
@@ -10,7 +11,7 @@ class NotifyMyWP(Notification):
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
keys = [x.strip() for x in self.conf('api_key').split(',')]
keys = splitString(self.conf('api_key'))
p = PyNMWP(keys, self.conf('dev_key'))
response = p.push(application = self.default_title, event = message, description = message, priority = self.conf('priority'), batch_mode = len(keys) > 1)

View File

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

View File

@@ -17,7 +17,7 @@ class Plex(Notification):
super(Plex, self).__init__()
addEvent('renamer.after', self.addToLibrary)
def addToLibrary(self, group = {}):
def addToLibrary(self, message = None, group = {}):
if self.isDisabled(): return
log.info('Sending notification to Plex')

View File

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

View File

@@ -1,39 +1,35 @@
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from httplib import HTTPSConnection
import traceback
log = CPLog(__name__)
class Prowl(Notification):
urls = {
'api': 'https://api.prowlapp.com/publicapi/add'
}
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
http_handler = HTTPSConnection('api.prowlapp.com')
data = {
'apikey': self.conf('api_key'),
'application': self.default_title,
'description': toUnicode(message),
'priority': self.conf('priority'),
}
headers = {
'Content-type': 'application/x-www-form-urlencoded'
}
http_handler.request('POST',
'/publicapi/add',
headers = {'Content-type': 'application/x-www-form-urlencoded'},
body = tryUrlencode(data)
)
response = http_handler.getresponse()
request_status = response.status
if request_status == 200:
try:
self.urlopen(self.urls['api'], headers = headers, params = data, multipart = True, show_error = False)
log.info('Prowl notifications sent.')
return True
elif request_status == 401:
log.error('Prowl auth failed: %s', response.reason)
return False
else:
log.error('Prowl notification failed.')
return False
except:
log.error('Prowl failed: %s', traceback.format_exc())
return False

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

@@ -1,6 +1,8 @@
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
import os
import subprocess
log = CPLog(__name__)
@@ -8,14 +10,17 @@ log = CPLog(__name__)
class Synoindex(Notification):
index_path = '/usr/syno/bin/synoindex'
def __init__(self):
super(Synoindex, self).__init__()
addEvent('renamer.after', self.addToLibrary)
def addToLibrary(self, group = {}):
def addToLibrary(self, message = None, group = {}):
if self.isDisabled(): return
command = ['/usr/syno/bin/synoindex', '-A', group.get('destination_dir')]
log.info(u'Executing synoindex command: %s ', command)
command = [self.index_path, '-A', group.get('destination_dir')]
log.info('Executing synoindex command: %s ', command)
try:
p = subprocess.Popen(command, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
out = p.communicate()
@@ -26,3 +31,6 @@ class Synoindex(Notification):
return False
return True
def test(self):
return jsonified({'success': os.path.isfile(self.index_path)})

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

@@ -38,22 +38,33 @@ class Twitter(Notification):
direct_message = self.conf('direct_message')
direct_message_users = self.conf('screen_name')
mention = self.conf('mention')
mention_tag = None
if mention:
if direct_message:
direct_message_users = '%s %s' % (direct_message_users, mention)
direct_message_users = direct_message_users.replace('@',' ')
direct_message_users = direct_message_users.replace(',',' ')
direct_message_users = direct_message_users.replace('@', ' ')
direct_message_users = direct_message_users.replace(',', ' ')
else:
message = '%s @%s' % (message, mention.lstrip('@'))
mention_tag = '@%s' % mention.lstrip('@')
message = '%s %s' % (message, mention_tag)
try:
if direct_message:
for user in direct_message_users.split():
api.PostDirectMessage(user, '[%s] %s' % (self.default_title, message))
else:
api.PostUpdate('[%s] %s' % (self.default_title, message))
update_message = '[%s] %s' % (self.default_title, message)
if len(update_message) > 140:
if mention_tag:
api.PostUpdate(update_message[:135 - len(mention_tag)] + ('%s 1/2 ' % mention_tag))
api.PostUpdate(update_message[135 - len(mention_tag):] + ('%s 2/2 ' % mention_tag))
else:
api.PostUpdate(update_message[:135] + ' 1/2')
api.PostUpdate(update_message[135:] + ' 2/2')
else:
api.PostUpdate(update_message)
except Exception, e:
log.error('Error sending tweet: %s', e)
return False

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',

186
couchpotato/core/notifications/xbmc/main.py Normal file → Executable file
View File

@@ -1,43 +1,189 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from flask.helpers import json
import base64
import traceback
import urllib
log = CPLog(__name__)
class XBMC(Notification):
listen_to = ['movie.downloaded']
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 = [x.strip() for x in self.conf('host').split(",")]
hosts = splitString(self.conf('host'))
successful = 0
for host in hosts:
if self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host):
successful += 1
if self.send({'command': 'ExecBuiltIn', 'parameter': 'XBMC.updatelibrary(video)'}, host):
successful += 1
return successful == len(hosts)*2
if self.use_json_notifications.get(host) is None:
self.getXBMCJSONversion(host, message = message)
def send(self, command, host):
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', {})])
url = 'http://%s/xbmcCmds/xbmcHttp/?%s' % (host, tryUrlencode(command))
try:
for result in response:
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']))
headers = {}
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'):
headers = {
'Authorization': "Basic %s" % base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password')))[:-1]
}
base64string = base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password'))).replace('\n', '')
headers['Authorization'] = 'Basic %s' % base64string
try:
self.urlopen(url, headers = headers, show_error = False)
except:
log.error("Couldn't sent command to XBMC")
return False
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
data = []
for req in requests:
method, kwargs = req
data.append({
'method': method,
'params': kwargs,
'jsonrpc': '2.0',
'id': method,
})
data = json.dumps(data)
headers = {
'Content-Type': 'application/json',
}
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 request to %s: %s', (host, data))
rdata = self.urlopen(server, headers = headers, params = data, multipart = True)
response = json.loads(rdata)
log.debug('Returned from request %s: %s', (host, response))
return response
except:
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
return []
log.info('XBMC notification to %s successful.', host)
return True

View File

@@ -5,13 +5,12 @@ def start():
config = [{
'name': 'automation',
'order': 30,
'order': 101,
'groups': [
{
'tab': 'automation',
'name': 'automation',
'label': 'Automation',
'description': 'Minimal movie requirements',
'label': 'Minimal movie requirements',
'options': [
{
'name': 'year',

View File

@@ -18,9 +18,17 @@ class Automation(Plugin):
def addMovies(self):
movies = fireEvent('automation.get_movies', merge = True)
movie_ids = []
for imdb_id in movies:
prop_name = 'automation.added.%s' % imdb_id
added = Env.prop(prop_name, default = False)
if not added:
fireEvent('movie.add', params = {'identifier': imdb_id}, force_readd = False)
added_movie = fireEvent('movie.add', params = {'identifier': imdb_id}, force_readd = False, search_after = False, update_library = True, single = True)
if added_movie:
movie_ids.append(added_movie['id'])
Env.prop(prop_name, True)
for movie_id in movie_ids:
movie_dict = fireEvent('movie.get', movie_id, single = True)
fireEvent('searcher.single', movie_dict)

View File

@@ -1,8 +1,8 @@
from StringIO import StringIO
from couchpotato import addView
from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.encoding import tryUrlencode, simplifyString, ss
from couchpotato.core.helpers.variable import getExt
from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString
from couchpotato.core.helpers.variable import getExt, md5
from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
from flask.templating import render_template_string
@@ -34,7 +34,7 @@ class Plugin(object):
http_failed_disabled = {}
def registerPlugin(self):
addEvent('app.shutdown', self.doShutdown)
addEvent('app.do_shutdown', self.doShutdown)
addEvent('plugin.running', self.isRunning)
def conf(self, attr, value = None, default = None):
@@ -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 = {}
@@ -113,8 +114,11 @@ class Plugin(object):
# Don't try for failed requests
if self.http_failed_disabled.get(host, 0) > 0:
if self.http_failed_disabled[host] > (time.time() - 900):
log.info('Disabled calls to %s for 15 minutes because so many failed requests.', host)
raise Exception
log.info2('Disabled calls to %s for 15 minutes because so many failed requests.', host)
if not show_error:
raise
else:
return ''
else:
del self.http_failed_request[host]
del self.http_failed_disabled[host]
@@ -123,11 +127,14 @@ class Plugin(object):
try:
if multipart:
log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()]))
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:
@@ -218,7 +225,7 @@ class Plugin(object):
def getCache(self, cache_key, url = None, **kwargs):
cache_key = simplifyString(cache_key)
cache_key = md5(ss(cache_key))
cache = Env.get('cache').get(cache_key)
if cache:
if not Env.get('dev'): log.debug('Getting cache %s', cache_key)
@@ -233,18 +240,36 @@ class Plugin(object):
del kwargs['cache_timeout']
data = self.urlopen(url, **kwargs)
if data:
self.setCache(cache_key, data, timeout = cache_timeout)
return data
except:
pass
if not kwargs.get('show_error', True):
raise
return ''
def setCache(self, cache_key, value, timeout = 300):
log.debug('Setting cache %s', cache_key)
Env.get('cache').set(cache_key, value, timeout)
return value
def createNzbName(self, data, movie):
tag = self.cpTag(movie)
return '%s%s' % (toSafeString(data.get('name')[:127 - len(tag)]), tag)
def createFileName(self, data, filedata, movie):
name = os.path.join(self.createNzbName(data, movie))
if data.get('type') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata:
return '%s.%s' % (name, 'rar')
return '%s.%s' % (name, data.get('type'))
def cpTag(self, movie):
if Env.setting('enabled', 'renamer'):
return '.cp(' + movie['library'].get('identifier') + ')' if movie['library'].get('identifier') else ''
return ''
def isDisabled(self):
return not self.isEnabled()

View File

@@ -1,5 +1,6 @@
from couchpotato.api import addApiView
from couchpotato.core.helpers.request import getParam, jsonified
from couchpotato.core.helpers.variable import getUserDir
from couchpotato.core.plugins.base import Plugin
import ctypes
import os
@@ -14,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):
@@ -27,6 +28,8 @@ class FileBrowser(Plugin):
},
'return': {'type': 'object', 'example': """{
'is_root': bool, //is top most folder
'parent': string, //parent folder of requested path
'home': string, //user home folder
'empty': bool, //directory is empty
'dirs': array, //directory names
}"""}
@@ -63,15 +66,28 @@ class FileBrowser(Plugin):
def view(self):
path = getParam('path', '/')
home = getUserDir()
if not path:
path = home
try:
dirs = self.getDirectories(path = path, show_hidden = getParam('show_hidden', True))
except:
dirs = []
parent = os.path.dirname(path.rstrip(os.path.sep))
if parent == path.rstrip(os.path.sep):
parent = '/'
elif parent != '/' and parent[-2:] != ':\\':
parent += os.path.sep
return jsonified({
'is_root': path == '/' or not path,
'is_root': path == '/',
'empty': len(dirs) == 0,
'parent': parent,
'home': home + os.path.sep,
'platform': os.name,
'dirs': dirs,
})
@@ -82,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

@@ -2,12 +2,15 @@ from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.helpers.variable import md5, getExt
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.plugins.scanner.main import Scanner
from couchpotato.core.settings.model import FileType, File
from couchpotato.environment import Env
import os.path
import time
import traceback
log = CPLog(__name__)
@@ -28,6 +31,54 @@ class FileManager(Plugin):
'return': {'type': 'file'}
})
addApiView('file.types', self.getTypesView, docs = {
'desc': 'Return a list of all the file types and their ids.',
'return': {'type': 'object', 'example': """{
'types': [
{
"identifier": "poster_original",
"type": "image",
"id": 1,
"name": "Poster_original"
},
{
"identifier": "poster",
"type": "image",
"id": 2,
"name": "Poster"
},
etc
]
}"""}
})
addEvent('app.load', self.cleanup)
addEvent('app.load', self.init)
def init(self):
for type_tuple in Scanner.file_types.values():
self.getType(type_tuple)
def cleanup(self):
# Wait a bit after starting before cleanup
time.sleep(3)
log.debug('Cleaning up unused files')
python_cache = Env.get('cache')._path
try:
db = get_session()
for root, dirs, walk_files in os.walk(Env.get('cache_dir')):
for filename in walk_files:
if root == python_cache or 'minified' in filename: continue
file_path = os.path.join(root, filename)
f = db.query(File).filter(File.path == toUnicode(file_path)).first()
if not f:
os.remove(file_path)
except:
log.error('Failed removing unused file: %s', traceback.format_exc())
def showCacheFile(self, filename = ''):
cache_dir = Env.get('cache_dir')
@@ -89,7 +140,6 @@ class FileManager(Plugin):
db.commit()
type_dict = ft.to_dict()
#db.close()
return type_dict
def getTypes(self):
@@ -102,5 +152,10 @@ class FileManager(Plugin):
for type_object in results:
types.append(type_object.to_dict())
#db.close()
return types
def getTypesView(self):
return jsonified({
'types': self.getTypes()
})

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

@@ -1,6 +1,7 @@
from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library, LibraryTitle, File
@@ -37,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)
@@ -52,7 +53,6 @@ class LibraryPlugin(Plugin):
library_dict = l.to_dict(self.default_dict)
#db.close()
return library_dict
def update(self, identifier, default_title = '', force = False):
@@ -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,26 +104,29 @@ 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()
# Files
images = info.get('images', [])
for type in images:
for image in images[type]:
if not isinstance(image, str):
for image_type in ['poster']:
for image in images.get(image_type, []):
if not isinstance(image, (str, unicode)):
continue
file_path = fireEvent('file.download', url = image, single = True)
if file_path:
file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', type), single = True)
file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', image_type), single = True)
try:
file_obj = db.query(File).filter_by(id = file_obj.get('id')).one()
library.files.append(file_obj)
db.commit()
break
except:
log.debug('Failed to attach to library: %s', traceback.format_exc())
@@ -136,26 +140,23 @@ class LibraryPlugin(Plugin):
library = db.query(Library).filter_by(identifier = identifier).first()
if not library.info:
library_dict = self.update(identifier)
dates = library_dict.get('info', {}).get('release_dates')
library_dict = self.update(identifier, force = True)
dates = library_dict.get('info', {}).get('release_date')
else:
dates = library.info.get('release_date')
if dates and dates.get('expires', 0) < time.time():
if dates and dates.get('expires', 0) < time.time() or not dates:
dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
library.info['release_date'] = dates
library.info = library.info
library.info = mergeDicts(library.info, {'release_date': dates })
db.commit()
dates = library.info.get('release_date', {})
#db.close()
return dates
def simplifyTitle(self, title):
title = toUnicode(title)
nr_prefix = '' if title[0] in ascii_letters else '#'
title = simplifyString(title)

View File

@@ -2,22 +2,35 @@ 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 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
log = CPLog(__name__)
class Manage(Plugin):
in_progress = False
def __init__(self):
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 = {}):
return self.scanFilesToLibrary(folder = group['destination_dir'], files = group['renamed_files'])
addEvent('renamer.after', after_rename, priority = 110)
addApiView('manage.update', self.updateLibraryView, docs = {
'desc': 'Update the library by scanning for new movies',
'params': {
@@ -25,11 +38,23 @@ class Manage(Plugin):
}
})
addApiView('manage.progress', self.getProgress, docs = {
'desc': 'Get the progress of current manage update',
'return': {'type': 'object', 'example': """{
'progress': False || object, total & to_go,
}"""},
})
if not Env.get('dev'):
def updateLibrary():
self.updateLibrary(full = False)
addEvent('app.load', updateLibrary)
def getProgress(self):
return jsonified({
'progress': self.in_progress
})
def updateLibraryView(self):
full = getParam('full', default = 1)
@@ -43,49 +68,181 @@ class Manage(Plugin):
def updateLibrary(self, full = True):
last_update = float(Env.prop('manage.last_update', default = 0))
if self.isDisabled() or (last_update > time.time() - 20):
if self.in_progress:
log.info('Already updating library: %s', self.in_progress)
return
elif self.isDisabled() or (last_update > time.time() - 20):
return
directories = self.directories()
added_identifiers = []
self.in_progress = {}
fireEvent('notify.frontend', type = 'manage.updating', data = True)
for directory in directories:
try:
if not os.path.isdir(directory):
if len(directory) > 0:
log.error('Directory doesn\'t exist: %s', directory)
continue
directories = self.directories()
added_identifiers = []
log.info('Updating manage library: %s', directory)
identifiers = fireEvent('scanner.folder', folder = directory, newer_than = last_update if not full else 0, single = True)
if identifiers:
added_identifiers.extend(identifiers)
# Add some progress
self.in_progress = {}
for directory in directories:
self.in_progress[os.path.normpath(directory)] = {
'total': None,
'to_go': None,
}
# Break if CP wants to shut down
if self.shuttingDown():
for directory in directories:
folder = os.path.normpath(directory)
if not os.path.isdir(folder):
if len(directory) > 0:
log.error('Directory doesn\'t exist: %s', folder)
continue
log.info('Updating manage library: %s', folder)
fireEvent('notify.frontend', type = 'manage.update', data = True, message = 'Scanning for movies in "%s"' % folder)
onFound = self.createAddToLibrary(folder, added_identifiers)
fireEvent('scanner.scan', folder = folder, simple = True, newer_than = last_update if not full else 0, on_found = onFound, single = True)
# Break if CP wants to shut down
if self.shuttingDown():
break
# If cleanup option is enabled, remove offline files from database
if self.conf('cleanup') and full and not self.shuttingDown():
# Get movies with done status
total_movies, done_movies = fireEvent('movie.list', status = 'done', single = True)
for done_movie in done_movies:
if done_movie['library']['identifier'] not in added_identifiers:
fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all')
else:
for release in done_movie.get('releases', []):
if len(release.get('files', [])) == 0:
fireEvent('release.delete', release['id'])
else:
for release_file in release.get('files', []):
# Remove release not available anymore
if not os.path.isfile(ss(release_file['path'])):
fireEvent('release.clean', release['id'])
break
# Check if there are duplicate releases (different quality) use the last one, delete the rest
if len(done_movie.get('releases', [])) > 1:
used_files = {}
for release in done_movie.get('releases', []):
for release_file in release.get('files', []):
already_used = used_files.get(release_file['path'])
if already_used:
if already_used < release['id']:
fireEvent('release.delete', release['id'], single = True) # delete this one
else:
fireEvent('release.delete', already_used, single = True) # delete previous one
break
else:
used_files[release_file['path']] = release.get('id')
del used_files
Env.prop('manage.last_update', time.time())
except:
log.error('Failed updating library: %s', (traceback.format_exc()))
while True and not self.shuttingDown():
delete_me = {}
for folder in self.in_progress:
if self.in_progress[folder]['to_go'] <= 0:
delete_me[folder] = True
for delete in delete_me:
del self.in_progress[delete]
if len(self.in_progress) == 0:
break
# If cleanup option is enabled, remove offline files from database
if self.conf('cleanup') and full and not self.shuttingDown():
time.sleep(1)
# Get movies with done status
total_movies, done_movies = fireEvent('movie.list', status = 'done', single = True)
fireEvent('notify.frontend', type = 'manage.updating', data = False)
self.in_progress = False
for done_movie in done_movies:
if done_movie['library']['identifier'] not in added_identifiers:
fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all')
else:
for release in done_movie.get('releases', []):
for release_file in release.get('files', []):
# Remove release not available anymore
if not os.path.isfile(ss(release_file['path'])):
fireEvent('release.clean', release['id'])
break
def createAddToLibrary(self, folder, added_identifiers = []):
def addToLibrary(group, total_found, to_go):
if self.in_progress[folder]['total'] is None:
self.in_progress[folder] = {
'total': total_found,
'to_go': total_found,
}
Env.prop('manage.last_update', time.time())
if group['library'] and group['library'].get('identifier'):
identifier = group['library'].get('identifier')
added_identifiers.append(identifier)
# Add it to release and update the info
fireEvent('release.add', group = group)
fireEventAsync('library.update', identifier = identifier, on_complete = self.createAfterUpdate(folder, identifier))
return addToLibrary
def createAfterUpdate(self, folder, identifier):
# Notify frontend
def afterUpdate():
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 [x.strip() for x in self.conf('library', default = '').split('::')]
if self.conf('library', default = '').strip():
return splitString(self.conf('library', default = ''), '::')
except:
return []
pass
return []
def scanFilesToLibrary(self, folder = None, files = None):
folder = os.path.normpath(folder)
groups = fireEvent('scanner.scan', folder = folder, files = files, single = True)
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

@@ -3,14 +3,16 @@ from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.helpers.request import getParams, jsonified, getParam
from couchpotato.core.helpers.variable import getImdb
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')
@@ -107,29 +138,43 @@ class MoviePlugin(Plugin):
def get(self, movie_id):
db = get_session()
m = db.query(Movie).filter_by(id = movie_id).first()
imdb_id = getImdb(str(movie_id))
if(imdb_id):
m = db.query(Movie).filter(Movie.library.has(identifier = imdb_id)).first()
else:
m = db.query(Movie).filter_by(id = movie_id).first()
results = None
if m:
results = m.to_dict(self.default_dict)
#db.close()
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 = []
@@ -149,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)) \
@@ -161,7 +209,7 @@ class MoviePlugin(Plugin):
.options(joinedload_all('files'))
if limit_offset:
splt = [x.strip() for x in limit_offset.split(',')]
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)
@@ -180,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 = ''
@@ -189,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()
@@ -201,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,
@@ -226,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,
@@ -239,7 +306,7 @@ class MoviePlugin(Plugin):
db = get_session()
for id in getParam('id').split(','):
for id in splitString(getParam('id')):
movie = db.query(Movie).filter_by(id = id).first()
if movie:
@@ -278,7 +345,7 @@ class MoviePlugin(Plugin):
'movies': movies,
})
def add(self, params = {}, force_readd = True, search_after = True):
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.'
@@ -287,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)
@@ -298,11 +364,13 @@ class MoviePlugin(Plugin):
pass
library = fireEvent('library.add', single = True, attrs = params, update_after = False)
library = fireEvent('library.add', single = True, attrs = params, update_after = update_library)
# Status
status_active = fireEvent('status.add', 'active', single = True)
status_snatched = fireEvent('status.add', 'snatched', single = True)
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)
@@ -314,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()
@@ -326,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:
@@ -337,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()
@@ -381,7 +454,7 @@ class MoviePlugin(Plugin):
available_status = fireEvent('status.get', 'available', single = True)
ids = [x.strip() for x in params.get('id').split(',')]
ids = splitString(params.get('id'))
for movie_id in ids:
m = db.query(Movie).filter_by(id = movie_id).first()
@@ -417,7 +490,7 @@ class MoviePlugin(Plugin):
params = getParams()
ids = [x.strip() for x in params.get('id').split(',')]
ids = splitString(params.get('id'))
for movie_id in ids:
self.delete(movie_id, delete_from = params.get('delete_from', 'all'))
@@ -431,9 +504,11 @@ class MoviePlugin(Plugin):
movie = db.query(Movie).filter_by(id = movie_id).first()
if movie:
deleted = False
if delete_from == 'all':
db.delete(movie)
db.commit()
deleted = True
else:
done_status = fireEvent('status.get', 'done', single = True)
@@ -441,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
@@ -456,6 +531,7 @@ class MoviePlugin(Plugin):
if total_releases == total_deleted:
db.delete(movie)
db.commit()
deleted = True
elif new_movie_status:
new_status = fireEvent('status.get', new_movie_status, single = True)
movie.profile_id = None
@@ -464,6 +540,9 @@ class MoviePlugin(Plugin):
else:
fireEvent('movie.restatus', movie.id, single = True)
if deleted:
fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict())
#db.close()
return True

View File

@@ -5,6 +5,7 @@ var MovieList = new Class({
options: {
navigation: true,
limit: 50,
load_more: true,
menu: [],
add_new: false
},
@@ -12,37 +13,67 @@ 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();
if(options.add_new)
App.addEvent('movie.added', self.movieAdded.bind(self))
App.addEvent('movie.added', self.movieAdded.bind(self))
App.addEvent('movie.deleted', self.movieDeleted.bind(self))
},
movieDeleted: function(notification){
var self = this;
if(self.movies_added[notification.data.id]){
self.movies.each(function(movie){
if(movie.get('id') == notification.data.id){
movie.destroy();
delete self.movies_added[notification.data.id]
}
})
}
self.checkIfEmpty();
},
movieAdded: function(notification){
var self = this;
window.scroll(0,0);
if(!self.movies_added[notification.data.id])
if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){
window.scroll(0,0);
self.createMovie(notification.data, 'top');
self.checkIfEmpty();
}
},
create: function(){
@@ -52,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;
},
@@ -78,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();
}
@@ -86,34 +109,31 @@ var MovieList = new Class({
Object.each(movies, function(movie){
self.createMovie(movie);
});
self.total_movies = total;
self.setCounter(total);
},
setCounter: function(count){
var self = this;
if(!self.navigation_counter) return;
self.navigation_counter.set('text', (count || 0));
},
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)
@@ -124,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(
@@ -197,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': {
@@ -281,10 +301,11 @@ var MovieList = new Class({
},
deleteSelected: function(){
var self = this;
var ids = self.getSelectedMovies()
var self = this,
ids = self.getSelectedMovies(),
help_msg = self.identifier == 'wanted' ? 'If you do, you won\'t be able to watch them, as they won\'t get downloaded!' : 'Your files will be safe, this will only delete the reference from the CouchPotato manage list';
var qObj = new Question('Are you sure you want to delete '+ids.length+' movie'+ (ids.length != 1 ? 's' : '') +'?', 'If you do, you won\'t be able to watch them, as they won\'t get downloaded!', [{
var qObj = new Question('Are you sure you want to delete '+ids.length+' movie'+ (ids.length != 1 ? 's' : '') +'?', help_msg, [{
'text': 'Yes, delete '+(ids.length != 1 ? 'them' : 'it'),
'class': 'delete',
'events': {
@@ -309,6 +330,8 @@ var MovieList = new Class({
erase_movies.each(function(movie){
self.movies.erase(movie);
movie.destroy()
});
self.calculateSelected();
@@ -376,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){
@@ -396,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(){
@@ -446,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
@@ -456,8 +483,12 @@ 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()
}
});
},
@@ -475,6 +506,34 @@ var MovieList = new Class({
},
checkIfEmpty: function(){
var self = this;
var is_empty = self.movies.length == 0 && (self.total_movies == 0 || self.total_movies === undefined);
if(self.title)
self.title[is_empty ? 'hide' : 'show']()
if(self.description)
self.description[is_empty ? 'hide' : 'show']()
if(is_empty && self.options.on_empty_element){
self.el.grab(self.options.on_empty_element);
if(self.navigation)
self.navigation.hide();
self.empty_element = self.options.on_empty_element;
}
else if(self.empty_element){
self.empty_element.destroy();
if(self.navigation)
self.navigation.show();
}
},
toElement: function(){
return this.el;
}

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 {
.movies.list_list .movie:hover:not(.details_view),
.movies.mass_edit_list .movie {
background: rgba(255,255,255,0.03);
}
.movies .movie_container {
overflow: hidden;
}
.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;
@@ -313,7 +457,7 @@
padding-bottom: 4px;
height: auto;
}
.movies .movie .trailer_container {
width: 100%;
background: #000;
@@ -324,7 +468,7 @@
.movies .movie .trailer_container.hide {
height: 0 !important;
}
.movies .movie .hide_trailer {
position: absolute;
top: 0;
@@ -335,17 +479,17 @@
padding: 3px 10px;
background: #4e5969;
border-radius: 0 0 2px 2px;
transition: all .6s cubic-bezier(0.9,0,0.1,1) .2s;
transition: all .2s cubic-bezier(0.9,0,0.1,1) .2s;
}
.movies .movie .hide_trailer.hide {
top: -30px;
}
.movies .movie .hide_trailer.hide {
top: -30px;
}
.movies .movie .try_container {
padding: 5px 10px;
text-align: center;
}
.movies .movie .try_container a {
margin: 0 5px;
padding: 2px 5px;
@@ -354,15 +498,15 @@
.movies .movie .releases .next_release {
border-left: 6px solid #2aa300;
}
.movies .movie .releases .next_release > :first-child {
margin-left: -6px;
}
.movies .movie .releases .last_release {
border-left: 6px solid #ffa200;
}
.movies .movie .releases .last_release > :first-child {
margin-left: -6px;
}
@@ -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;
@@ -394,8 +538,8 @@
border-radius: 0;
}
.movies .alph_nav ul.numbers,
.movies .alph_nav .counter,
.movies .alph_nav ul.numbers,
.movies .alph_nav .counter,
.movies .alph_nav ul.actions {
list-style: none;
padding: 0 0 1px;
@@ -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;
}
@@ -518,7 +663,7 @@
padding: 3px 7px;
}
.movies .alph_nav .mass_edit_form .refresh,
.movies .alph_nav .mass_edit_form .refresh,
.movies .alph_nav .mass_edit_form .delete {
float: left;
padding: 8px 0 0 8px;
@@ -534,5 +679,67 @@
}
.movies .alph_nav .more_menu > a {
background-position: center -157px;
background-position: center -158px;
}
.movies .empty_wanted {
background-image: url('../images/emptylist.png');
height: 750px;
width: 800px;
padding-top: 260px;
margin-top: -50px;
}
.movies .empty_manage {
text-align: center;
font-size: 25px;
line-height: 150%;
}
.movies .empty_manage .after_manage {
margin-top: 30px;
font-size: 16px;
}
.movies .progress {
border-radius: 2px;
padding: 10px;
margin: 5px 0;
text-align: left;
}
.movies .progress > div {
padding: 5px 10px;
font-size: 12px;
line-height: 12px;
text-align: left;
display: inline-block;
width: 49%;
background: rgba(255, 255, 255, 0.05);
margin: 2px 0.5%;
border-radius: 3px;
}
.movies .progress > div .folder {
display: inline-block;
padding: 5px 20px 5px 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 85%;
direction: rtl;
vertical-align: middle;
}
.movies .progress > div .percentage {
font-weight: bold;
display: inline-block;
text-transform: uppercase;
text-shadow: none;
font-weight: normal;
font-size: 20px;
border-left: 1px solid rgba(255, 255, 255, .2);
width: 15%;
text-align: right;
vertical-align: middle;
}

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');
@@ -16,14 +16,41 @@ var Movie = new Class({
self.profile = Quality.getProfile(data.profile_id) || {};
self.parent(self, options);
App.addEvent('movie.update.'+data.id, self.update.bind(self));
self.addEvents();
},
addEvents: function(){
var self = this;
App.addEvent('movie.update.'+self.data.id, self.update.bind(self));
['movie.busy', 'searcher.started'].each(function(listener){
App.addEvent(listener+'.'+data.id, function(notification){
App.addEvent(listener+'.'+self.data.id, function(notification){
if(notification.data)
self.busy(true)
});
})
App.addEvent('searcher.ended.'+self.data.id, function(notification){
if(notification.data)
self.busy(false)
});
},
destroy: function(){
var self = this;
self.el.destroy();
delete self.list.movies_added[self.get('id')];
self.list.movies.erase(self)
self.list.checkIfEmpty();
// Remove events
App.removeEvents('movie.update.'+self.data.id);
['movie.busy', 'searcher.started'].each(function(listener){
App.removeEvents(listener+'.'+self.data.id);
})
},
busy: function(set_busy){
@@ -45,7 +72,6 @@ var Movie = new Class({
else if(!self.spinner) {
self.createMask();
self.spinner = createSpinner(self.mask);
self.positionMask();
self.mask.fade('in');
}
},
@@ -54,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(){
@@ -76,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();
@@ -87,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
@@ -147,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),
@@ -214,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');
}
},
@@ -244,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]
},
@@ -261,377 +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((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;
if( status.identifier == 'ignored' || status.identifier == 'failed'){
self.last_release = release;
}
else if(!self.next_release && status.identifier == 'available'){
self.next_release = release;
}
// Create release
new Element('div', {
'class': 'item '+status.identifier +
(self.next_release && self.next_release.id == release.id ? ' next_release' : '') +
(self.last_release && self.last_release.id == release.id ? ' last_release' : ''),
'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)
});
self.trynext_container.adopt(
new Element('span.or', {
'text': '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': 'or'
}) : 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

@@ -91,10 +91,15 @@
.movie_result {
overflow: hidden;
height: 140px;
position: relative;
}
.movie_result .options {
height: 140px;
position: absolute;
height: 100%;
width: 100%;
top: 0;
left: 0;
border: 1px solid transparent;
border-width: 1px 0;
border-radius: 0;
@@ -133,17 +138,21 @@
.movie_result .data {
padding: 0 15px;
width: 470px;
position: relative;
height: 140px;
position: absolute;
height: 100%;
width: 100%;
top: 0;
margin: -140px 0 0 0;
left: 0;
background: #5c697b;
cursor: pointer;
border-bottom: 1px solid #333;
border-top: 1px solid rgba(255,255,255, 0.15);
transition: all .6s cubic-bezier(0.9,0,0.1,1);
}
.movie_result .data.open {
left: 100%;
}
.movie_result:last-child .data { border-bottom: 0; }
@@ -182,6 +191,8 @@
.movie_result .info h2 {
margin: 0;
font-size: 17px;
line-height: 20px;
}
.movie_result .info h2 span {
@@ -191,6 +202,12 @@
.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%;
width: 100%;
left: 0;
top: 0;
}

View File

@@ -19,7 +19,9 @@ Block.Search = new Class({
self.hideResults(false)
},
'blur': function(){
self.el.removeClass('focused')
(function(){
self.el.removeClass('focused')
}).delay(2000);
}
}
}),
@@ -117,7 +119,7 @@ Block.Search = new Class({
self.hideResults(false);
if(!cache){
self.positionMask().fade('in');
self.mask.fade('in');
if(!self.spinner)
self.spinner = createSpinner(self.mask);
@@ -139,7 +141,6 @@ Block.Search = new Class({
fill: function(q, json){
var self = this;
self.positionMask()
self.cache[q] = json
self.movies = {}
@@ -168,19 +169,6 @@ Block.Search = new Class({
},
positionMask: function(){
var self = this;
var s = self.result_container.getSize()
return self.mask.setStyles({
'width': s.x,
'height': s.y
}).position({
'relativeTo': self.result_container
})
},
loading: function(bool){
this.el[bool ? 'addClass' : 'removeClass']('loading')
},
@@ -193,7 +181,7 @@ Block.Search = new Class({
Block.Search.Item = new Class({
initialize: function(info){
initialize: function(info, options){
var self = this;
self.info = info;
@@ -203,14 +191,13 @@ Block.Search.Item = new Class({
},
create: function(){
var self = this;
var info = self.info;
var self = this,
info = self.info;
self.el = new Element('div.movie_result', {
'id': info.imdb
}).adopt(
self.options = new Element('div.options.inlay'),
self.options_el = new Element('div.options.inlay'),
self.data_container = new Element('div.data', {
'tween': {
duration: 400,
@@ -273,11 +260,7 @@ Block.Search.Item = new Class({
self.createOptions();
if(!self.width)
self.width = self.data_container.getCoordinates().width
self.data_container.tween('left', 0, self.width);
self.data_container.addClass('open');
self.el.addEvent('outerClick', self.closeOptions.bind(self))
},
@@ -285,7 +268,7 @@ Block.Search.Item = new Class({
closeOptions: function(){
var self = this;
self.data_container.tween('left', self.width, 0);
self.data_container.removeClass('open');
self.el.removeEvents('outerClick')
},
@@ -302,29 +285,32 @@ Block.Search.Item = new Class({
'profile_id': self.profile_select.get('value')
},
'onComplete': function(json){
self.options.empty();
self.options.adopt(
self.options_el.empty();
self.options_el.adopt(
new Element('div.message', {
'text': json.added ? 'Movie succesfully added.' : 'Movie didn\'t add properly. Check logs'
'text': json.added ? 'Movie successfully added.' : 'Movie didn\'t add properly. Check logs'
})
);
self.mask.fade('out');
},
'onFailure': function(){
self.options.empty();
self.options.adopt(
self.options_el.empty();
self.options_el.adopt(
new Element('div.message', {
'text': 'Something went wrong, check the logs for more info.'
})
);
self.mask.fade('out');
}
});
},
createOptions: function(){
var self = this;
var self = this,
info = self.info;
if(!self.options_el.hasClass('set')){
if(!self.options.hasClass('set')){
if(self.info.in_library){
var in_library = [];
self.info.in_library.releases.each(function(release){
@@ -332,14 +318,14 @@ Block.Search.Item = new Class({
});
}
self.options.adopt(
self.options_el.grab(
new Element('div').adopt(
self.option_thumbnail = self.info.images && self.info.images.poster.length > 0 ? new Element('img.thumbnail', {
'src': self.info.images.poster[0],
self.thumbnail = (info.images && info.images.poster.length > 0) ? new Element('img.thumbnail', {
'src': info.images.poster[0],
'height': null,
'width': null
}) : null,
self.info.in_wanted ? new Element('span.in_wanted', {
self.info.in_wanted && self.info.in_wanted.profile ? new Element('span.in_wanted', {
'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label
}) : (in_library ? new Element('span.in_library', {
'text': 'Already in library: ' + in_library.join(', ')
@@ -372,7 +358,7 @@ Block.Search.Item = new Class({
}).inject(self.profile_select)
});
self.options.addClass('set');
self.options_el.addClass('set');
}
},
@@ -380,17 +366,7 @@ Block.Search.Item = new Class({
loadingMask: function(){
var self = this;
var s = self.options.getSize();
self.mask = new Element('span.mask', {
'styles': {
'position': 'relative',
'width': s.x,
'height': s.y,
'top': -s.y,
'display': 'block'
}
}).inject(self.options).fade('hide')
self.mask = new Element('div.mask').inject(self.el).fade('hide')
createSpinner(self.mask)
self.mask.fade('in')

View File

@@ -47,7 +47,6 @@ class ProfilePlugin(Plugin):
for profile in profiles:
temp.append(profile.to_dict(self.to_dict))
#db.close()
return temp
def save(self):
@@ -84,7 +83,6 @@ class ProfilePlugin(Plugin):
profile_dict = p.to_dict(self.to_dict)
#db.close()
return jsonified({
'success': True,
'profile': profile_dict
@@ -95,7 +93,6 @@ class ProfilePlugin(Plugin):
db = get_session()
default = db.query(Profile).first()
default_dict = default.to_dict(self.to_dict)
#db.close()
return default_dict
@@ -113,7 +110,6 @@ class ProfilePlugin(Plugin):
order += 1
db.commit()
#db.close()
return jsonified({
'success': True
@@ -137,8 +133,6 @@ class ProfilePlugin(Plugin):
except Exception, e:
message = log.error('Failed deleting Profile: %s', e)
#db.close()
return jsonified({
'success': success,
'message': message
@@ -181,10 +175,10 @@ class ProfilePlugin(Plugin):
)
p.types.append(profile_type)
db.commit()
quality_order += 1
order += 1
#db.close()
db.commit()
return True

View File

@@ -86,7 +86,10 @@ var Profile = new Class({
},
'onComplete': function(json){
if(json.success){
self.data = json.profile
self.data = json.profile;
self.type_container.getElement('li:first-child input[type=checkbox]')
.set('checked', true)
.getParent().addClass('checked');
}
}
});
@@ -239,9 +242,17 @@ Profile.Type = new Class({
),
new Element('span.finish').adopt(
self.finish = new Element('input.inlay.finish[type=checkbox]', {
'checked': data.finish,
'checked': data.finish !== undefined ? data.finish : 1,
'events': {
'change': self.fireEvent.bind(self, 'change')
'change': function(e){
if(self.el == self.el.getParent().getElement(':first-child')){
self.finish_class.check();
alert('Top quality always finishes the search')
return;
}
self.fireEvent('change');
}
}
})
),
@@ -255,7 +266,7 @@ Profile.Type = new Class({
self.el[self.data.quality_id > 0 ? 'removeClass' : 'addClass']('is_empty');
new Form.Check(self.finish);
self.finish_class = new Form.Check(self.finish);
},

View File

@@ -7,8 +7,10 @@ from couchpotato.core.helpers.variable import mergeDicts, md5, getExt
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Quality, Profile, ProfileType
from sqlalchemy.sql.expression import or_
import os.path
import re
import time
log = CPLog(__name__)
@@ -17,15 +19,15 @@ class QualityPlugin(Plugin):
qualities = [
{'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]},
{'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
{'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['avi']},
{'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
{'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi']},
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']},
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}
]
pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr']
@@ -68,7 +70,6 @@ class QualityPlugin(Plugin):
q = mergeDicts(self.getQuality(quality.identifier), quality.to_dict())
temp.append(q)
#db.close()
return temp
def single(self, identifier = ''):
@@ -76,11 +77,10 @@ class QualityPlugin(Plugin):
db = get_session()
quality_dict = {}
quality = db.query(Quality).filter_by(identifier = identifier).first()
quality = db.query(Quality).filter(or_(Quality.identifier == identifier, Quality.id == identifier)).first()
if quality:
quality_dict = dict(self.getQuality(quality.identifier), **quality.to_dict())
#db.close()
return quality_dict
def getQuality(self, identifier):
@@ -100,7 +100,6 @@ class QualityPlugin(Plugin):
setattr(quality, params.get('value_type'), params.get('value'))
db.commit()
#db.close()
return jsonified({
'success': True
})
@@ -113,46 +112,48 @@ class QualityPlugin(Plugin):
for q in self.qualities:
# Create quality
quality = db.query(Quality).filter_by(identifier = q.get('identifier')).first()
qual = db.query(Quality).filter_by(identifier = q.get('identifier')).first()
if not quality:
if not qual:
log.info('Creating quality: %s', q.get('label'))
quality = Quality()
db.add(quality)
qual = Quality()
qual.order = order
qual.identifier = q.get('identifier')
qual.label = toUnicode(q.get('label'))
qual.size_min, qual.size_max = q.get('size')
quality.order = order
quality.identifier = q.get('identifier')
quality.label = toUnicode(q.get('label'))
quality.size_min, quality.size_max = q.get('size')
db.add(qual)
# Create single quality profile
profile = db.query(Profile).filter(
prof = db.query(Profile).filter(
Profile.core == True
).filter(
Profile.types.any(quality = quality)
Profile.types.any(quality = qual)
).all()
if not profile:
if not prof:
log.info('Creating profile: %s', q.get('label'))
profile = Profile(
prof = Profile(
core = True,
label = toUnicode(quality.label),
label = toUnicode(qual.label),
order = order
)
db.add(profile)
db.add(prof)
profile_type = ProfileType(
quality = quality,
profile = profile,
quality = qual,
profile = prof,
finish = True,
order = 0
)
profile.types.append(profile_type)
prof.types.append(profile_type)
order += 1
db.commit()
#db.close()
db.commit()
time.sleep(0.3) # Wait a moment
return True
def guess(self, files, extra = {}):
@@ -198,9 +199,14 @@ class QualityPlugin(Plugin):
for quality in self.all():
# Last check on resolution only
if quality.get('width', 480) == extra.get('resolution_width', 0):
log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width', 480), extra.get('resolution_width', 0)))
# Check width resolution, range 20
if (quality.get('width', 720) - 20) <= extra.get('resolution_width', 0) <= (quality.get('width', 720) + 20):
log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width', 720), extra.get('resolution_width', 0)))
return self.setCache(hash, quality)
# Check height resolution, range 20
if (quality.get('height', 480) - 20) <= extra.get('resolution_height', 0) <= (quality.get('height', 480) + 20):
log.debug('Found %s via resolution_height: %s == %s', (quality['identifier'], quality.get('height', 480), extra.get('resolution_height', 0)))
return self.setCache(hash, quality)
if 480 <= extra.get('resolution_width', 0) <= 720:

View File

@@ -88,8 +88,6 @@ class Release(Plugin):
fireEvent('movie.restatus', movie.id)
#db.close()
return True
@@ -108,7 +106,6 @@ class Release(Plugin):
release_id = getParam('id')
#db.close()
return jsonified({
'success': self.delete(release_id)
})
@@ -136,6 +133,9 @@ class Release(Plugin):
db.delete(release_file)
db.commit()
if len(rel.files) == 0:
self.delete(id)
return True
return False
@@ -152,7 +152,6 @@ class Release(Plugin):
rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id')
db.commit()
#db.close()
return jsonified({
'success': True
})
@@ -161,6 +160,7 @@ class Release(Plugin):
db = get_session()
id = getParam('id')
status_snatched = fireEvent('status.add', 'snatched', single = True)
rel = db.query(Relea).filter_by(id = id).first()
if rel:
@@ -181,14 +181,16 @@ class Release(Plugin):
'files': {}
}), manual = True, single = True)
#db.close()
if success:
rel.status_id = status_snatched.get('id')
db.commit()
return jsonified({
'success': success
})
else:
log.error('Couldn\'t find release with id: %s', id)
#db.close()
return jsonified({
'success': False
})

View File

@@ -1,4 +1,5 @@
from couchpotato.core.plugins.renamer.main import Renamer
import os
def start():
return Renamer()
@@ -82,6 +83,15 @@ config = [{
'unit': 'min(s)',
'description': 'Detect movie status every X minutes. Will start the renamer if movie is <strong>completed</strong> or handle <strong>failed</strong> download if these options are enabled',
},
{
'advanced': True,
'name': 'force_every',
'label': 'Force every',
'default': 2,
'type': 'int',
'unit': 'hour(s)',
'description': 'Forces the renamer to scan every X hours',
},
{
'advanced': True,
'name': 'next_on_failed',
@@ -102,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',
@@ -124,13 +143,6 @@ config = [{
'type': 'choice',
'options': rename_options
},
{
'name': 'trailer_name',
'label': 'Trailer naming',
'default': '<filename>-trailer.<ext>',
'type': 'choice',
'options': rename_options
},
],
},
],

View File

@@ -3,7 +3,8 @@ from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import toUnicode, ss
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
getImdb
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library, File, Profile, Release
@@ -12,6 +13,7 @@ import errno
import os
import re
import shutil
import time
import traceback
log = CPLog(__name__)
@@ -20,6 +22,7 @@ log = CPLog(__name__)
class Renamer(Plugin):
renaming_started = False
checking_snatched = False
def __init__(self):
@@ -31,8 +34,13 @@ class Renamer(Plugin):
addEvent('renamer.check_snatched', self.checkSnatched)
addEvent('app.load', self.scan)
addEvent('app.load', self.checkSnatched)
fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'))
if self.conf('run_every') > 0:
fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'))
if self.conf('force_every') > 0:
fireEvent('schedule.interval', 'renamer.check_snatched_forced', self.scan, hours = self.conf('force_every'))
def scanView(self):
@@ -48,7 +56,7 @@ class Renamer(Plugin):
return
if self.renaming_started is True:
log.info('Renamer is disabled to avoid infinite looping of the same error.')
log.info('Renamer is already running, if you see this often, check the logs above for errors.')
return
# Check to see if the "to" folder is inside the "from" folder.
@@ -127,6 +135,8 @@ class Renamer(Plugin):
'resolution_width': group['meta_data'].get('resolution_width'),
'resolution_height': group['meta_data'].get('resolution_height'),
'imdb_id': library['identifier'],
'cd': '',
'cd_nr': '',
}
for file_type in group['files']:
@@ -144,7 +154,7 @@ class Renamer(Plugin):
continue
# Move other files
multiple = len(group['files']['movie']) > 1 and not group['is_dvd']
multiple = len(group['files'][file_type]) > 1 and not group['is_dvd']
cd = 1 if multiple else 0
for current_file in sorted(list(group['files'][file_type])):
@@ -157,23 +167,19 @@ class Renamer(Plugin):
replacements['ext'] = getExt(current_file)
# cd #
replacements['cd'] = ' cd%d' % cd if cd else ''
replacements['cd_nr'] = cd
replacements['cd'] = ' cd%d' % cd if multiple else ''
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)]
# Group filename without cd extension
replacements['cd'] = ''
replacements['cd_nr'] = ''
# Meta naming
if file_type is 'trailer':
final_file_name = self.doReplace(trailer_name, replacements)
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)
final_file_name = self.doReplace(nfo_name, replacements, remove_multiple = True).lstrip('. ')
# Seperator replace
if separator:
@@ -204,10 +210,16 @@ class Renamer(Plugin):
# Check for extra subtitle files
if file_type is 'subtitle':
# rename subtitles with or without language
rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
remove_multiple = False
if len(group['files']['movie']) == 1:
remove_multiple = True
sub_langs = group['subtitle_language'].get(current_file, [])
# rename subtitles with or without language
sub_name = self.doReplace(file_name, replacements, remove_multiple = remove_multiple)
rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name)
rename_extras = self.getRenameExtras(
extra_type = 'subtitle_extra',
replacements = replacements,
@@ -215,20 +227,19 @@ class Renamer(Plugin):
file_name = file_name,
destination = destination,
group = group,
current_file = current_file
current_file = current_file,
remove_multiple = remove_multiple,
)
# Don't add language if multiple languages in 1 file
if len(sub_langs) > 1:
rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
elif len(sub_langs) == 1:
# Don't add language if multiple languages in 1 subtitle file
if len(sub_langs) == 1:
sub_name = final_file_name.replace(replacements['ext'], '%s.%s' % (sub_langs[0], replacements['ext']))
rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name)
rename_files = mergeDicts(rename_files, rename_extras)
# Filename without cd etc
if file_type is 'movie':
elif file_type is 'movie':
rename_extras = self.getRenameExtras(
extra_type = 'movie_extra',
replacements = replacements,
@@ -240,7 +251,7 @@ class Renamer(Plugin):
)
rename_files = mergeDicts(rename_files, rename_extras)
group['filename'] = self.doReplace(file_name, replacements)[:-(len(getExt(final_file_name)) + 1)]
group['filename'] = self.doReplace(file_name, replacements, remove_multiple = True)[:-(len(getExt(final_file_name)) + 1)]
group['destination_dir'] = os.path.join(destination, final_folder_name)
if multiple:
@@ -265,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()))
@@ -304,7 +316,12 @@ class Renamer(Plugin):
elif release.status_id is snatched_status.get('id'):
if release.quality.id is group['meta_data']['quality']['id']:
log.debug('Marking release as downloaded')
release.status_id = downloaded_status.get('id')
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
@@ -328,6 +345,7 @@ class Renamer(Plugin):
log.info('Removing "%s"', src)
try:
src = ss(src)
if os.path.isfile(src):
os.remove(src)
@@ -341,7 +359,10 @@ class Renamer(Plugin):
# Delete leftover folder from older releases
for delete_folder in delete_folders:
self.deleteEmptyFolder(delete_folder, show_error = False)
try:
self.deleteEmptyFolder(delete_folder, show_error = False)
except Exception, e:
log.error('Failed to delete folder: %s %s', (e, traceback.format_exc()))
# Rename all files marked
group['renamed_files'] = []
@@ -375,22 +396,22 @@ class Renamer(Plugin):
except:
log.error('Failed removing %s: %s', (group['parentdir'], traceback.format_exc()))
# Search for trailers etc
fireEventAsync('renamer.after', group)
# Notify on download
# Notify on download, search for trailers etc
download_message = 'Downloaded %s (%s)' % (movie_title, replacements['quality'])
fireEventAsync('movie.downloaded', message = download_message, data = group)
try:
fireEvent('renamer.after', message = download_message, group = group, in_order = True)
except:
log.error('Failed firing (some) of the renamer.after events: %s', traceback.format_exc())
# Break if CP wants to shut down
if self.shuttingDown():
break
#db.close()
self.renaming_started = False
def getRenameExtras(self, extra_type = '', replacements = {}, folder_name = '', file_name = '', destination = '', group = {}, current_file = ''):
def getRenameExtras(self, extra_type = '', replacements = {}, folder_name = '', file_name = '', destination = '', group = {}, current_file = '', remove_multiple = False):
replacements = replacements.copy()
rename_files = {}
def test(s):
@@ -399,8 +420,8 @@ class Renamer(Plugin):
for extra in set(filter(test, group['files'][extra_type])):
replacements['ext'] = getExt(extra)
final_folder_name = self.doReplace(folder_name, replacements)
final_file_name = self.doReplace(file_name, replacements)
final_folder_name = self.doReplace(folder_name, replacements, remove_multiple = remove_multiple)
final_file_name = self.doReplace(file_name, replacements, remove_multiple = remove_multiple)
rename_files[extra] = os.path.join(destination, final_folder_name, final_file_name)
return rename_files
@@ -438,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)))
@@ -451,15 +474,20 @@ class Renamer(Plugin):
except:
log.error('Couldn\'t move file "%s" to "%s": %s', (old, dest, traceback.format_exc()))
raise Exception
raise
return True
def doReplace(self, string, replacements):
def doReplace(self, string, replacements, remove_multiple = False):
'''
replace confignames with the real thing
'''
replacements = replacements.copy()
if remove_multiple:
replacements['cd'] = ''
replacements['cd_nr'] = ''
replaced = toUnicode(string)
for x, r in replacements.iteritems():
if r is not None:
@@ -477,6 +505,7 @@ class Renamer(Plugin):
return string.replace(' ', ' ').replace(' .', '.')
def deleteEmptyFolder(self, folder, show_error = True):
folder = ss(folder)
loge = log.error if show_error else log.debug
for root, dirs, files in os.walk(folder):
@@ -495,6 +524,11 @@ class Renamer(Plugin):
loge('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc()))
def checkSnatched(self):
if self.checking_snatched:
log.debug('Already checking snatched')
self.checking_snatched = True
snatched_status = fireEvent('status.get', 'snatched', single = True)
ignored_status = fireEvent('status.get', 'ignored', single = True)
failed_status = fireEvent('status.get', 'failed', single = True)
@@ -504,57 +538,71 @@ class Renamer(Plugin):
db = get_session()
rels = db.query(Release).filter_by(status_id = snatched_status.get('id')).all()
if rels:
log.debug('Checking status snatched releases...')
scan_required = False
for rel in rels:
if rels:
self.checking_snatched = True
log.debug('Checking status snatched releases...')
# Get current selected title
default_title = ''
for title in rel.movie.library.titles:
if title.default: default_title = title.title
# Check if movie has already completed and is manage tab (legacy db correction)
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')
db.commit()
continue
item = {}
for info in rel.info:
item[info.identifier] = info.value
movie_dict = fireEvent('movie.get', rel.movie_id, single = True)
# check status
downloadstatus = fireEvent('download.status', data = item, movie = movie_dict, single = True)
if not downloadstatus:
statuses = fireEvent('download.status', merge = True)
if not statuses:
log.debug('Download status functionality is not implemented for active downloaders.')
scan_required = True
else:
log.debug('Download status: %s' , downloadstatus)
try:
for rel in rels:
rel_dict = rel.to_dict({'info': {}})
if downloadstatus == 'failed':
if self.conf('next_on_failed'):
fireEvent('searcher.try_next_release', movie_id = rel.movie_id)
else:
rel.status_id = failed_status.get('id')
db.commit()
# Get current selected title
default_title = getTitle(rel.movie.library)
log.info('Download of %s failed.', item['name'])
# Check if movie has already completed and is manage tab (legacy db correction)
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
elif downloadstatus == 'completed':
log.info('Download of %s completed!', item['name'])
scan_required = True
movie_dict = fireEvent('movie.get', rel.movie_id, single = True)
elif downloadstatus == 'not_found':
log.info('%s not found in downloaders', item['name'])
rel.status_id = ignored_status.get('id')
db.commit()
# check status
nzbname = self.createNzbName(rel_dict['info'], movie_dict)
found = False
for item in statuses:
if item['name'] == nzbname or rel_dict['info']['name'] in item['name'] or getImdb(item['name']) == movie_dict['library']['identifier']:
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))
if item['status'] == 'busy':
pass
elif item['status'] == 'failed':
fireEvent('download.remove_failed', item, single = True)
if self.conf('next_on_failed'):
fireEvent('searcher.try_next_release', movie_id = rel.movie_id)
else:
rel.status_id = failed_status.get('id')
rel.last_edit = int(time.time())
db.commit()
elif item['status'] == 'completed':
log.info('Download of %s completed!', item['name'])
scan_required = True
found = True
break
if not found:
log.info('%s not found in downloaders', nzbname)
except:
log.error('Failed checking for release in downloader: %s', traceback.format_exc())
# Note that Queued, Downloading, Paused, Repair and Unpackimg are also available as status for SabNZBd
if scan_required:
fireEvent('renamer.scan')
self.checking_snatched = False
return True

View File

@@ -23,7 +23,7 @@ class Scanner(Plugin):
'media': 314572800, # 300MB
'trailer': 1048576, # 1MB
}
ignored_in_path = ['_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'],
@@ -34,6 +34,7 @@ class Scanner(Plugin):
'subtitle_extra': ['idx'],
'trailer': ['mov', 'mp4', 'flv']
}
file_types = {
'subtitle': ('subtitle', 'subtitle'),
'subtitle_extra': ('subtitle', 'subtitle_extra'),
@@ -42,6 +43,8 @@ class Scanner(Plugin):
'movie': ('video', 'movie'),
'movie_extra': ('movie', 'movie_extra'),
'backdrop': ('image', 'backdrop'),
'poster': ('image', 'poster'),
'thumbnail': ('image', 'thumbnail'),
'leftover': ('leftover', 'leftover'),
}
@@ -50,6 +53,20 @@ class Scanner(Plugin):
'video': ['x264', 'h264', 'divx', 'xvid']
}
audio_codec_map = {
0x2000: 'ac3',
0x2001: 'dts',
0x0055: 'mp3',
0x0050: 'mp2',
0x0001: 'pcm',
0x003: 'pcm',
0x77a1: 'tta1',
0x5756: 'wav',
0x6750: 'vorbis',
0xF1AC: 'flac',
0x00ff: 'aac',
}
source_media = {
'bluray': ['bluray', 'blu-ray', 'brrip', 'br-rip'],
'hddvd': ['hddvd', 'hd-dvd'],
@@ -72,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):
@@ -80,55 +97,10 @@ class Scanner(Plugin):
addEvent('scanner.remove_cptag', self.removeCPTag)
addEvent('scanner.scan', self.scan)
addEvent('scanner.files', self.scanFilesToLibrary)
addEvent('scanner.folder', self.scanFolderToLibrary)
addEvent('scanner.name_year', self.getReleaseNameYear)
addEvent('scanner.partnumber', self.getPartNumber)
def after_rename(group):
return self.scanFilesToLibrary(folder = group['destination_dir'], files = group['renamed_files'])
addEvent('renamer.after', after_rename)
def scanFilesToLibrary(self, folder = None, files = None):
folder = os.path.normpath(folder)
groups = self.scan(folder = folder, files = files)
for group in groups.itervalues():
if group['library']:
fireEvent('release.add', group = group)
def scanFolderToLibrary(self, folder = None, newer_than = 0, simple = True):
folder = os.path.normpath(folder)
if not os.path.isdir(folder):
return
groups = self.scan(folder = folder, simple = simple, newer_than = newer_than)
added_identifier = []
while True and not self.shuttingDown():
try:
identifier, group = groups.popitem()
except:
break
# Save to DB
if group['library']:
# Add release
fireEvent('release.add', group = group)
library_item = fireEvent('library.update', identifier = group['library'].get('identifier'), single = True)
if library_item:
added_identifier.append(library_item['identifier'])
return added_identifier
def scan(self, folder = None, files = [], simple = False, newer_than = 0):
def scan(self, folder = None, files = None, simple = False, newer_than = 0, on_found = None):
folder = ss(os.path.normpath(folder))
@@ -141,7 +113,8 @@ class Scanner(Plugin):
leftovers = []
# Scan all files of the folder if no files are set
if len(files) == 0:
if not files:
check_file_date = True
try:
files = []
for root, dirs, walk_files in os.walk(folder):
@@ -150,6 +123,7 @@ class Scanner(Plugin):
except:
log.error('Failed getting files from %s: %s', (folder, traceback.format_exc()))
else:
check_file_date = False
files = [ss(x) for x in files]
db = get_session()
@@ -279,8 +253,8 @@ class Scanner(Plugin):
del path_identifiers[identifier]
del delete_identifiers
# Determine file types
processed_movies = {}
# Make sure we remove older / still extracting files
valid_files = {}
while True and not self.shuttingDown():
try:
identifier, group = movie_files.popitem()
@@ -302,7 +276,7 @@ class Scanner(Plugin):
if file_too_new:
break
if file_too_new:
if check_file_date and file_too_new:
try:
time_string = time.ctime(file_time[0])
except:
@@ -320,17 +294,33 @@ class Scanner(Plugin):
# Only process movies newer than x
if newer_than and newer_than > 0:
has_new_files = False
for cur_file in group['unsorted_files']:
file_time = [os.path.getmtime(cur_file), os.path.getctime(cur_file)]
if file_time[0] > time.time() or file_time[1] > time.time():
if file_time[0] > newer_than or file_time[1] > newer_than:
has_new_files = True
break
log.debug('None of the files have changed since %s for %s, skipping.', (time.ctime(newer_than), identifier))
if not has_new_files:
log.debug('None of the files have changed since %s for %s, skipping.', (time.ctime(newer_than), identifier))
# Delete the unsorted list
del group['unsorted_files']
# Delete the unsorted list
del group['unsorted_files']
continue
continue
valid_files[identifier] = group
del movie_files
# Determine file types
processed_movies = {}
total_found = len(valid_files)
while True and not self.shuttingDown():
try:
identifier, group = valid_files.popitem()
except:
break
# Group extra (and easy) files first
# images = self.getImages(group['unsorted_files'])
@@ -351,11 +341,11 @@ 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)
group['meta_data'] = self.getMetaData(group)
group['meta_data'] = self.getMetaData(group, folder = folder)
# Subtitle meta
group['subtitle_language'] = self.getSubtitleLanguage(group) if not simple else {}
@@ -392,9 +382,12 @@ class Scanner(Plugin):
movie = db.query(Movie).filter_by(library_id = group['library']['id']).first()
group['movie_id'] = None if not movie else movie.id
processed_movies[identifier] = group
# Notify parent & progress on something found
if on_found:
on_found(group, total_found, total_found - len(processed_movies))
if len(processed_movies) > 0:
log.info('Found %s movies in the folder %s', (len(processed_movies), folder))
else:
@@ -402,7 +395,7 @@ class Scanner(Plugin):
return processed_movies
def getMetaData(self, group):
def getMetaData(self, group, folder = ''):
data = {}
files = list(group['files']['movie'])
@@ -428,10 +421,10 @@ class Scanner(Plugin):
if not data['quality']:
data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True)
data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 else 'SD'
data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 or data['quality'].get('hd') else 'SD'
filename = re.sub('(.cp\(tt[0-9{7}]+\))', '', files[0])
data['group'] = self.getGroup(filename)
data['group'] = self.getGroup(filename[len(folder):])
data['source'] = self.getSourceMedia(filename)
return data
@@ -440,9 +433,18 @@ class Scanner(Plugin):
try:
p = enzyme.parse(filename)
# Video codec
vc = ('h264' if p.video[0].codec == 'AVC1' else p.video[0].codec).lower()
# Audio codec
ac = p.audio[0].codec
try: ac = self.audio_codec_map.get(p.audio[0].codec)
except: pass
return {
'video': p.video[0].codec,
'audio': p.audio[0].codec,
'video': vc,
'audio': ac,
'resolution_width': tryInt(p.video[0].width),
'resolution_height': tryInt(p.video[0].height),
}
@@ -539,7 +541,6 @@ class Scanner(Plugin):
break
except:
pass
#db.close()
# Search based on OpenSubtitleHash
if not imdb_id and not group['is_dvd']:
@@ -760,8 +761,8 @@ class Scanner(Plugin):
def getGroup(self, file):
try:
match = re.search('-(?P<group>[A-Z0-9]+).', file, re.I)
return match.group('group') or ''
match = re.findall('\-([A-Z0-9]+)[\.\/]', file, re.I)
return match[-1] or ''
except:
return ''
@@ -774,7 +775,7 @@ class Scanner(Plugin):
return None
def findYear(self, text):
matches = re.search('(?P<year>[12]{1}[0-9]{3})', text)
matches = re.search('(?P<year>19[0-9]{2}|20[0-9]{2})', text)
if matches:
return matches.group('year')

View File

@@ -27,9 +27,9 @@ class Score(Plugin):
score += sizeScore(nzb['size'])
# Torrents only
if nzb.get('seeds'):
if nzb.get('seeders'):
try:
score += nzb.get('seeds') / 5
score += nzb.get('seeders') / 5
score += nzb.get('leechers') / 10
except:
pass

View File

@@ -10,7 +10,7 @@ name_scores = [
# Video
'x264:1', 'h264:1',
# Audio
'DTS:4', 'AC3:2',
'dts:4', 'ac3:2',
# Quality
'720p:10', '1080p:10', 'bluray:10', 'dvd:1', 'dvdrip:1', 'brrip:1', 'bdrip:1', 'bd50:1', 'bd25:1',
# Language / Subs
@@ -116,7 +116,7 @@ def sizeScore(size):
def providerScore(provider):
if provider in ['NZBMatrix', 'Nzbs', 'Newzbin']:
if provider in ['OMGWTFNZBs', 'PassThePopcorn', 'SceneAccess', 'TorrentLeech']:
return 20
if provider in ['Newznab']:

View File

@@ -24,18 +24,19 @@ config = [{
'name': 'required_words',
'label': 'Required words',
'default': '',
'description': 'Ignore releases that don\'t contain at least one of these words.'
'placeholder': 'Example: DTS, AC3 & English',
'description': 'Ignore releases that don\'t contain at least one set of words. Sets are separated by "," and each word within a set must be separated with "&"'
},
{
'name': 'ignored_words',
'label': 'Ignored words',
'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub',
'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs',
},
{
'name': 'preferred_method',
'label': 'First search',
'description': 'Which of the methods do you prefer',
'default': 'nzb',
'default': 'both',
'type': 'dropdown',
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrents', 'torrent')],
},

View File

@@ -1,9 +1,10 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import md5, getTitle
from couchpotato.core.helpers.variable import md5, getTitle, splitString, \
possibleTitles
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
@@ -11,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
@@ -28,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',
@@ -36,9 +39,38 @@ class Searcher(Plugin):
},
})
addApiView('searcher.full_search', self.allMoviesView, docs = {
'desc': 'Starts a full search for all wanted movies',
})
addApiView('searcher.progress', self.getProgress, docs = {
'desc': 'Get the progress of current full search',
'return': {'type': 'object', 'example': """{
'progress': False || object, total & to_go,
}"""},
})
# Schedule cronjob
fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
def allMoviesView(self):
in_progress = self.in_progress
if not in_progress:
fireEventAsync('searcher.all')
fireEvent('notify.frontend', type = 'searcher.started', data = True, message = 'Full search started')
else:
fireEvent('notify.frontend', type = 'searcher.already_started', data = True, message = 'Full search already in progress')
return jsonified({
'success': not in_progress
})
def getProgress(self):
return jsonified({
'progress': self.in_progress
})
def allMovies(self):
@@ -53,30 +85,51 @@ class Searcher(Plugin):
movies = db.query(Movie).filter(
Movie.status.has(identifier = 'active')
).all()
random.shuffle(movies)
for movie in movies:
movie_dict = movie.to_dict({
'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files':{}},
'files': {}
})
self.in_progress = {
'total': len(movies),
'to_go': len(movies),
}
try:
self.single(movie_dict)
except IndexError:
fireEvent('library.update', movie_dict['library']['identifier'], force = True)
except:
log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
try:
search_types = self.getSearchTypes()
# Break if CP wants to shut down
if self.shuttingDown():
break
for movie in movies:
movie_dict = movie.to_dict({
'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files':{}},
'files': {}
})
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()))
self.in_progress['to_go'] -= 1
# Break if CP wants to shut down
if self.shuttingDown():
break
except SearchSetupError:
pass
#db.close()
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)
@@ -91,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.')
@@ -99,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
@@ -118,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:
@@ -135,19 +195,23 @@ 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')
)
db.add(rls)
db.commit()
else:
[db.delete(info) for info in rls.info]
db.commit()
[db.delete(old_info) for old_info in rls.info]
rls.last_edit = int(time.time())
db.commit()
for info in nzb:
try:
@@ -159,14 +223,19 @@ class Searcher(Plugin):
value = toUnicode(nzb[info])
)
rls.info.append(rls_info)
db.commit()
except InterfaceError:
log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc()))
db.commit()
nzb['status_id'] = rls.status_id
for nzb in sorted_results:
if not quality_type.get('finish', False) and quality_type.get('wait_for', 0) > 0 and nzb.get('age') <= quality_type.get('wait_for', 0):
log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name']))
continue
if nzb['status_id'] == ignored_status.get('id'):
log.info('Ignored: %s', nzb['name'])
continue
@@ -181,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'])
@@ -192,111 +267,149 @@ class Searcher(Plugin):
fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True)
#db.close()
return ret
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 is '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
# Mark release as snatched
db = get_session()
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
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 profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
log.info('Renamer disabled, marking movie as finished: %s', log_movie)
# 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 Exception, e:
log.error('Failed marking movie finished: %s %s', (e, 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())
#db.close()
return True
# If renamer isn't used, mark movie done
if not renamer_enabled:
active_status = fireEvent('status.get', 'active', single = True)
done_status = fireEvent('status.get', 'done', single = True)
try:
if movie['status_id'] == active_status.get('id'):
for profile_type in movie['profile']['types']:
if profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
log.info('Renamer disabled, marking movie as finished: %s', log_movie)
# Mark release done
rls.status_id = done_status.get('id')
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)
retention = Env.setting('retention', section = 'nzb')
if nzb.get('seeds') is None and retention < nzb.get('age', 0):
log.info('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name']))
if nzb.get('seeders') is None and 0 < retention < nzb.get('age', 0):
log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name']))
return False
movie_name = getTitle(movie['library'])
movie_words = re.split('\W+', simplifyString(movie_name))
nzb_name = simplifyString(nzb['name'])
nzb_words = re.split('\W+', nzb_name)
required_words = [x.strip().lower() for x in self.conf('required_words').lower().split(',')]
required_words = splitString(self.conf('required_words').lower())
if self.conf('required_words') and not list(set(nzb_words) & set(required_words)):
log.info("Wrong: Required word missing: %s" % nzb['name'])
req_match = 0
for req_set in required_words:
req = splitString(req_set, '&')
req_match += len(list(set(nzb_words) & set(req))) == len(req)
if self.conf('required_words') and req_match == 0:
log.info2("Wrong: Required word missing: %s" % nzb['name'])
return False
ignored_words = [x.strip().lower() for x in self.conf('ignored_words').split(',')]
blacklisted = list(set(nzb_words) & set(ignored_words))
ignored_words = splitString(self.conf('ignored_words').lower())
blacklisted = list(set(nzb_words) & set(ignored_words) - set(movie_words))
if self.conf('ignored_words') and blacklisted:
log.info("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted)))
log.info2("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted)))
return False
pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic']
for p_tag in pron_tags:
if p_tag in nzb_words and p_tag not in movie_words:
log.info('Wrong: %s, probably pr0n', (nzb['name']))
return False
pron_words = list(set(nzb_words) & set(pron_tags) - set(movie_words))
if pron_words:
log.info('Wrong: %s, probably pr0n', (nzb['name']))
return False
#qualities = fireEvent('quality.all', single = True)
preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True)
# Contains lower quality string
if self.containsOtherQuality(nzb, movie_year = movie['library']['year'], preferred_quality = preferred_quality):
log.info('Wrong: %s, looking for %s', (nzb['name'], quality['label']))
log.info2('Wrong: %s, looking for %s', (nzb['name'], quality['label']))
return False
# File to small
if nzb['size'] and preferred_quality['size_min'] > nzb['size']:
log.info('"%s" is too small to be %s. %sMB instead of the minimal of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_min']))
log.info2('Wrong: "%s" is too small to be %s. %sMB instead of the minimal of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_min']))
return False
# File to large
if nzb['size'] and preferred_quality.get('size_max') < nzb['size']:
log.info('"%s" is too large to be %s. %sMB instead of the maximum of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_max']))
log.info2('Wrong: "%s" is too large to be %s. %sMB instead of the maximum of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_max']))
return False
@@ -314,20 +427,21 @@ class Searcher(Plugin):
return True
# Check if nzb contains imdb link
if self.checkIMDB([nzb['description']], movie['library']['identifier']):
if self.checkIMDB([nzb.get('description', '')], movie['library']['identifier']):
return True
for movie_title in movie['library']['titles']:
movie_words = re.split('\W+', simplifyString(movie_title['title']))
for raw_title in movie['library']['titles']:
for movie_title in possibleTitles(raw_title['title']):
movie_words = re.split('\W+', simplifyString(movie_title))
if self.correctName(nzb['name'], movie_title['title']):
# if no IMDB link, at least check year range 1
if len(movie_words) > 2 and self.correctYear([nzb['name']], movie['library']['year'], 1):
return True
if self.correctName(nzb['name'], movie_title):
# if no IMDB link, at least check year range 1
if len(movie_words) > 2 and self.correctYear([nzb['name']], movie['library']['year'], 1):
return True
# if no IMDB link, at least check year
if len(movie_words) <= 2 and self.correctYear([nzb['name']], movie['library']['year'], 0):
return True
# if no IMDB link, at least check year
if len(movie_words) <= 2 and self.correctYear([nzb['name']], movie['library']['year'], 0):
return True
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie_name, movie['library']['year']))
return False
@@ -350,13 +464,20 @@ class Searcher(Plugin):
if list(set(nzb_words) & set(quality['alternative'])):
found[quality['identifier']] = True
# Try guessing via quality tags
guess = fireEvent('quality.guess', [nzb.get('name')], single = True)
if guess:
found[guess['identifier']] = True
# Hack for older movies that don't contain quality tag
year_name = fireEvent('scanner.name_year', name, single = True)
if movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None):
if len(found) == 0 and movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None):
if size > 3000: # Assume dvdr
return 'dvdr' == preferred_quality['identifier']
log.info('Quality was missing in name, assuming it\'s a DVD-R based on the size: %s', (size))
found['dvdr'] = True
else: # Assume dvdrip
return 'dvdrip' == preferred_quality['identifier']
log.info('Quality was missing in name, assuming it\'s a DVD-Rip based on the size: %s', (size))
found['dvdrip'] = True
# Allow other qualities
for allowed in preferred_quality.get('allow'):
@@ -373,23 +494,32 @@ class Searcher(Plugin):
return False
def correctYear(self, haystack, year, range):
def correctYear(self, haystack, year, year_range):
for string in haystack:
if str(year) in string or str(int(year) + range) in string or str(int(year) - range) in string: # 1 year of is fine too
year_name = fireEvent('scanner.name_year', string, single = True)
if year_name and ((year - year_range) <= year_name.get('year') <= (year + year_range)):
log.debug('Movie year matches range: %s looking for %s', (year_name.get('year'), year))
return True
log.debug('Movie year doesn\'t matche range: %s looking for %s', (year_name.get('year'), year))
return False
def correctName(self, check_name, movie_name):
check_names = [check_name]
try:
check_names.append(re.search(r'([\'"])[^\1]*\1', check_name).group(0))
except:
pass
for check_name in check_names:
# Match names between "
try: check_names.append(re.search(r'([\'"])[^\1]*\1', check_name).group(0))
except: pass
# Match longest name between []
try: check_names.append(max(check_name.split('['), key = len))
except: pass
for check_name in list(set(check_names)):
check_movie = fireEvent('scanner.name_year', check_name, single = True)
try:
@@ -403,14 +533,19 @@ class Searcher(Plugin):
return False
def couldBeReleased(self, wanted_quality, dates, pre_releases):
def couldBeReleased(self, is_pre_release, dates):
now = int(time.time())
if not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0):
return True
else:
if wanted_quality in pre_releases:
# For movies before 1972
if dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0:
return True
if is_pre_release:
# Prerelease 1 week before theaters
if dates.get('theater') - 604800 < now:
return True
@@ -465,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

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

View File

@@ -8,9 +8,9 @@ config = [{
'groups': [
{
'tab': 'renamer',
'subtab': 'subtitles',
'name': 'subtitle',
'label': 'Download subtitles after rename',
'label': 'Download subtitles',
'description': 'after rename',
'options': [
{
'name': 'enabled',

View File

@@ -1,6 +1,7 @@
from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library, FileType
@@ -13,7 +14,7 @@ log = CPLog(__name__)
class Subtitle(Plugin):
services = ['opensubtitles', 'thesubdb', 'subswiki']
services = ['opensubtitles', 'thesubdb', 'subswiki', 'podnapisi']
def __init__(self):
addEvent('renamer.before', self.searchSingle)
@@ -40,8 +41,6 @@ class Subtitle(Plugin):
# get subtitles for those files
subliminal.list_subtitles(files, cache_dir = Env.get('cache_dir'), multi = True, languages = self.getLanguages(), services = self.services)
#db.close()
def searchSingle(self, group):
if self.isDisabled(): return
@@ -50,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:
@@ -58,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]
@@ -69,4 +70,4 @@ class Subtitle(Plugin):
return False
def getLanguages(self):
return [x.strip() for x in self.conf('languages').split(',')]
return splitString(self.conf('languages'))

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,
})

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