Compare commits

...

1069 Commits

Author SHA1 Message Date
Ruud
6772b9d965 Don't migrate when db is closed 2014-07-17 23:09:26 +02:00
Ruud
5df14d67e1 One up 2014-07-17 22:28:32 +02:00
Ruud
73abd1f022 Merge branch 'refs/heads/master' into desktop 2014-07-17 22:27:23 +02:00
Ruud
e75a8529c9 Try fix migration failure from 2.5.1 2014-07-17 22:26:23 +02:00
Ruud
07a7f8cbcf Change fanart api url 2014-07-16 10:32:02 +02:00
Ruud
9b35a0fb20 Only trigger onClose when it's set 2014-07-08 21:21:22 +02:00
Ruud
0622e6e5ab One up 2014-06-29 23:16:09 +02:00
Ruud
f16931906f Don't remove pyc files when using desktop updater 2014-06-29 23:15:36 +02:00
Ruud
68dcba8853 One up 2/2 2014-06-29 21:56:51 +02:00
Ruud
ae8f66df1a Exit main loop on crash 2014-06-29 21:56:39 +02:00
Ruud
5237ead5cb Merge branch 'refs/heads/develop' into desktop 2014-06-29 17:01:47 +02:00
Ruud
45b2dff6d2 Merge branch 'refs/heads/develop' 2014-06-29 11:01:09 +02:00
Ruud
4cbc089de2 Log subfolder errors in renamer 2014-06-29 10:51:33 +02:00
Ruud
c45c04659f Use html parser for hdtrailers 2014-06-29 10:24:19 +02:00
Ruud
61a9037835 Don't error out if XBMC is turned off. fix #3515 2014-06-29 09:49:48 +02:00
Ruud
30d56b5d2c Merge branch 'refs/heads/develop' 2014-06-29 00:02:55 +02:00
Ruud
ad33c0bcca Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-06-28 23:58:21 +02:00
Ruud
7afc524a9f Freespace Windows not working. fix #3535 2014-06-28 23:58:02 +02:00
Ruud Burger
c5a4bc9a1b Merge pull request #3534 from mano3m/develop_fix_torrentshack
Fix torrentshack
2014-06-28 23:55:47 +02:00
Ruud
1c0178dbaf Fix 'ignore' mis-tagging 2014-06-28 23:54:52 +02:00
Ruud
dbf7feca3e Properly delete from manage 2014-06-28 23:46:28 +02:00
mano3m
d92de8ec4e Fix torrentshack 2014-06-28 23:43:16 +02:00
Ruud Burger
8347da5a58 Merge pull request #3529 from genial123/api-fix
Finish non-existent API calls instead of timing out
2014-06-28 23:17:37 +02:00
Ruud
59e248d7de Wrong argument split. fix #3518 2014-06-28 22:57:42 +02:00
genial123
12e556e1d1 Finish non-existent API calls instead of timing out 2014-06-28 08:19:05 +02:00
Ruud
14d3ab93da Add mp4 quality brrip 2014-06-25 23:22:35 +02:00
Ruud
e27ece512f Use release quality, not identifier to match downloaded release 2014-06-25 23:08:37 +02:00
Ruud
b88d8efc8d Allow 720p in cam quality. fix #3512 2014-06-25 22:56:40 +02:00
Ruud
5ff6824ae9 Merge branch 'refs/heads/develop' 2014-06-25 18:26:10 +02:00
Ruud
9ec4c2837e Don't put original title first 2014-06-25 18:20:42 +02:00
Ruud
ffc3fc9ec4 Check for broken indexes and reindex if needed 2014-06-25 18:16:45 +02:00
Ruud
a566b4f428 Setup property index with database module 2014-06-25 16:13:35 +02:00
Ruud
69819460f3 Add zoink.it for torrent caching 2014-06-25 09:27:23 +02:00
Ruud
0210859155 Merge branch 'refs/heads/develop' 2014-06-25 09:17:12 +02:00
Ruud
24a8cb41fe Keep previous status if restatus check fails 2014-06-24 22:01:27 +02:00
Ruud
1de0443492 Get default "stop after" if it isn't set yet. fix #3499 2014-06-24 21:56:46 +02:00
Ruud
bb19b380b4 Don't start CP when less then 100MB is available. fix #3502 2014-06-24 21:20:13 +02:00
Ruud
b6b936ddf3 Use other name guess. fix #3501 2014-06-24 20:50:25 +02:00
Ruud
b00b6acba8 Profile don't save. fix #3437 2014-06-24 20:18:51 +02:00
Ruud
3941076c06 Forgot to add the separator to test 2014-06-24 10:09:34 +02:00
Ruud
7401201af2 Add subfolder path test 2014-06-24 10:02:43 +02:00
Ruud
5c586fbf30 Update isSubFolder test 2014-06-24 10:02:14 +02:00
Ruud
5c891b7e8e Try next on failed trailer download 2014-06-23 23:47:30 +02:00
Ruud
665478db13 Merge branch 'refs/heads/develop' 2014-06-23 23:45:03 +02:00
Ruud
5425fcae9e Manually get with_status releases 2014-06-23 23:40:36 +02:00
Ruud
4008cce12f Manually get media with status 2014-06-23 23:37:17 +02:00
Ruud
d227105527 Make keep search advanced 2014-06-23 22:04:42 +02:00
Ruud
508649e6b6 Optimize import 2014-06-23 21:51:14 +02:00
Ruud
b4e25d4345 Indent fixes 2014-06-23 21:50:23 +02:00
Ruud
733f925c75 Merge branch 'refs/heads/mano3m-develop_wait_for_better' into develop 2014-06-23 21:49:21 +02:00
mano3m
40e910192e Fix tagging 2014-06-23 21:14:00 +02:00
mano3m
424a3cd892 Clean-up 2014-06-23 21:13:59 +02:00
mano3m
9f6036c8d6 Redo status update for media 2014-06-23 21:13:58 +02:00
mano3m
5af5749d4a Catch missing deleted profile error
@RuudBurger should we reset the profile of the media to default or None
in case this happens or leave it the way it is?
2014-06-23 21:10:11 +02:00
mano3m
f01449f14c Rename scanned files for done media properly 2014-06-23 21:10:10 +02:00
mano3m
03dff14ee9 Massive bug fix 2014-06-23 21:10:09 +02:00
mano3m
e55302592a Improve description 2014-06-23 21:10:09 +02:00
mano3m
dbeaab052d Wait before marking media as done 2014-06-23 21:10:08 +02:00
Ruud
84c366ab54 Merge branch 'master' of github.com:RuudBurger/CouchPotatoServer 2014-06-23 20:47:30 +02:00
Ruud
908e5eae77 Merge branch 'refs/heads/develop' 2014-06-23 20:47:06 +02:00
Ruud
9f07dd5a21 Reindex after full scan. fix #3492 2014-06-23 20:46:26 +02:00
Ruud
b933cd8718 Delete when total releases was 0 2014-06-23 20:40:04 +02:00
Ruud
c4aaa10308 One up 2014-06-23 20:00:06 +02:00
Ruud
d10536a829 Remove path from getOptions 2014-06-23 20:00:00 +02:00
Ruud
1e7fa82e11 Merge branch 'refs/heads/develop' into desktop 2014-06-23 19:01:58 +02:00
Ruud
8d85dde2c6 Don't use empty name_year return for moviemeter. fix #3493 2014-06-23 16:19:40 +02:00
Ruud
1d448f3d9c Merge branch 'refs/heads/develop' 2014-06-23 14:29:20 +02:00
Ruud
eaaa8dc834 Only try other if it's different 2014-06-23 14:15:00 +02:00
Ruud
5350dbf0ce Filter out extended and try other result on determine media. fix #3489 2014-06-23 14:13:32 +02:00
Ruud
28ffad10ab Standardize path for list directory api call. #3487 2014-06-23 13:43:33 +02:00
Ruud
338b5f427a Merge branch 'refs/heads/develop' 2014-06-23 13:37:50 +02:00
Ruud
a37517bf6a Use ssl startup options. fix #3490
Thanks @sjmcinness
2014-06-23 13:37:13 +02:00
Ruud
fab9b96c8e Keep done releases when removing from wanted/dashboard. fix #3488 2014-06-23 13:16:05 +02:00
Ruud
59e3e73c4c Merge branch 'refs/heads/develop' 2014-06-23 01:19:05 +02:00
Ruud
50d6882a98 Close all attached after start 2014-06-23 01:17:06 +02:00
Ruud
94064ac7da Rework restart methods 2014-06-23 01:09:32 +02:00
Ruud
1c5f19a68a Better reload hook name 2014-06-22 23:41:31 +02:00
Ruud
a26abd0dbb Don't use nonblock requests results if empty 2014-06-22 23:39:43 +02:00
Ruud
fb9080c18a Except value error 2014-06-22 23:38:51 +02:00
Ruud
15980471b0 Create api lock on the fly 2014-06-22 22:41:56 +02:00
Ruud
b11bb9cdac Catch missing profile in restatus 2014-06-22 21:32:52 +02:00
Ruud
cb2614127c Merge branch 'refs/heads/develop' 2014-06-22 21:14:44 +02:00
Ruud
474cd45fc5 Reset profile
to default when old one is empty or doesn't exist anymore
2014-06-22 21:14:31 +02:00
Ruud
0b6843a1b9 Force readd not adding with proper profile 2014-06-22 20:58:56 +02:00
Ruud
fdbd826917 Merge branch 'refs/heads/develop' 2014-06-22 20:35:30 +02:00
Ruud
fdcdf07fa6 Untag on delete from dashboard 2014-06-22 20:35:19 +02:00
Ruud
5617953d39 Mark as done missing. #3472 2014-06-22 20:23:47 +02:00
Ruud
964144996f Advanced not hidden. 2014-06-22 16:17:11 +02:00
Ruud
37214dd413 Put Pushover in config. close #3480 2014-06-22 16:15:36 +02:00
Ruud
5a08fed0b6 Manage release_id not assigned. fix #3479 2014-06-22 15:53:43 +02:00
Ruud
443866ef04 Use default title for search query. fix #3477 2014-06-21 18:50:36 +02:00
Ruud
96275adaff Use always search and ignore ETA. fix #3475 2014-06-21 18:44:09 +02:00
Ruud
31daf4915e Merge branch 'refs/heads/develop' 2014-06-20 21:31:48 +02:00
Ruud
33884deb6c Send single Pushbullet when no device is selected. fix #3471 2014-06-20 21:29:43 +02:00
Ruud
4ca7691afd Merge branch 'refs/heads/develop' 2014-06-20 21:08:33 +02:00
Ruud
7db291fc93 Show all in wizard 2014-06-20 21:07:17 +02:00
Ruud
9df14bd55a Cleanup provider lists 2014-06-20 21:04:24 +02:00
Ruud
1e183625c9 Description update 2014-06-20 20:45:26 +02:00
Ruud
643be19711 Update descriptions 2014-06-20 20:45:17 +02:00
Ruud
21a1770f3f Nzb icons 2014-06-20 18:14:08 +02:00
Ruud
07063d855a Add icons to torrent providers 2014-06-20 17:35:15 +02:00
Ruud
cf95e417f1 Delete publichd 2014-06-20 17:11:11 +02:00
Ruud
64d3ecd9b8 Merge branch 'refs/heads/develop' 2014-06-20 14:52:15 +02:00
Ruud
3f92ed0ea0 Don't autodownload releases with no file size. fix #3467 2014-06-20 14:33:20 +02:00
Ruud
d55df3240f Merge branch 'refs/heads/develop' 2014-06-20 14:14:26 +02:00
Ruud
578b74f2c0 Fix PushBullet url. fix #3470 2014-06-20 14:14:10 +02:00
Ruud
8e17b9aea5 Remove BoxCar 2014-06-20 14:12:43 +02:00
Ruud
52214e4938 Merge branch 'refs/heads/develop' 2014-06-20 12:22:13 +02:00
Ruud
6f766aae8c Tag and untag dashboard media 2014-06-20 12:13:54 +02:00
Ruud
5797348bb3 Update tag index 2014-06-20 12:10:40 +02:00
Ruud
57ca5067ff Insert themoviedb original_title in by default. 2014-06-19 16:48:30 +02:00
Ruud
e8ff8a41de Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-06-19 14:15:53 +02:00
Ruud
0b5dfe826a Fix BitSoup parsing. close #3465 2014-06-19 14:15:42 +02:00
Ruud
67fbcc8238 Tag filter index 2014-06-17 23:32:27 +02:00
Ruud
dd61c7dc21 Compact DB every 7 days if needed 2014-06-17 16:27:22 +02:00
Ruud
3786b5435f Only remove movie title from 3d words check 2014-06-17 15:36:18 +02:00
Ruud
1857e047b0 Remove moviename words when scanning for 3d tags. fix #3395 2014-06-17 15:22:20 +02:00
Ruud
648ac7793f Add multiple 3d tags to clean regex 2014-06-17 15:21:30 +02:00
Ruud
664ce6421f Try only parse filename for release name 2014-06-17 15:21:08 +02:00
Ruud
cfb77a1076 Don't use extension to test for quality tags. fix #3457 2014-06-17 14:22:42 +02:00
Ruud
f65ddbbb9e Encode environments args in html 2014-06-16 22:39:02 +02:00
Ruud
76126271fc Don't add default profile if status is done 2014-06-16 22:06:39 +02:00
Ruud
3faece0b4c Don't log already deleted releases. 2014-06-16 21:21:55 +02:00
Ruud
530d3cd91e Update rentals URL 2014-06-15 22:37:18 +02:00
Ruud
e659aba176 Clean .pyc files before starting 2014-06-15 22:22:34 +02:00
Ruud
a196a499ae Only cache qualities if list length is correct 2014-06-15 22:13:01 +02:00
Ruud
58bd9cd7a1 Unable to hide & reorder profiles. fix #3437 2014-06-15 14:59:02 +02:00
Ruud
9dd9f850c6 Treat seeding as "done" 2014-06-14 18:59:52 +02:00
Ruud
cbecb74307 Show ETA on soon list. fix #2702 2014-06-14 18:57:27 +02:00
Ruud
b45307e493 Merge branch 'refs/heads/develop' 2014-06-11 23:51:05 +02:00
Ruud
8ae1e58614 Don't call parent init for synoindex 2014-06-11 23:34:17 +02:00
Ruud
83e8ae392d Don't create a new "done" release on rename. fix #3250 2014-06-11 23:17:59 +02:00
Ruud
c0297f10cb Force download on "best release" selection 2014-06-11 22:24:11 +02:00
Ruud
41052ae508 Use same before ETA message 2014-06-11 22:14:13 +02:00
Ruud
2d243d51e4 Ignore ETA on manual refresh 2014-06-11 22:03:38 +02:00
Ruud
fdec80f676 Set last_force_eta time 2014-06-11 21:21:32 +02:00
Ruud
5d3b0deb4d Simpler progress update 2014-06-11 21:10:20 +02:00
Ruud
f68c356944 Update title index 2014-06-11 21:07:02 +02:00
Ruud
553f8d6ccd Ignore ETA every 7 days on search 2014-06-11 17:05:08 +02:00
Ruud
60fb3e33ae Sony PS3 metadata 2014-06-11 15:22:07 +02:00
Ruud
9b7c1db509 Sony PS3 metadata 2014-06-11 15:20:21 +02:00
Ruud
963ce356fb MediaBrowser metadata 2014-06-11 15:19:25 +02:00
Ruud
dcd0364ecc Re-use tiny scroller for webkit 2014-06-11 14:59:06 +02:00
Ruud
a2da428777 Chart css cleanup
Tiny webkit scroll
2014-06-11 14:40:22 +02:00
Ruud
876c602710 Code cleanup 2014-06-11 12:29:31 +02:00
Ruud
79cb716ced Update Mootools 2014-06-11 10:34:52 +02:00
Ruud
4320369448 Merge branch 'refs/heads/develop' 2014-06-11 10:15:31 +02:00
Ruud
ba9c975335 Allow empty quality 2014-06-11 10:11:36 +02:00
Ruud
ef407bcb3c Don't clear pyc when develop 2014-06-11 09:53:52 +02:00
Ruud
2898a066fe Prevent threading from GC before proper close. fix #3420 2014-06-11 09:49:30 +02:00
Ruud
7950c4bdb4 Update fedora service init 2014-06-11 09:34:06 +02:00
Ruud
2499012d88 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-06-11 09:31:43 +02:00
Ruud
7788669de1 Fill in profiles & qualities when they are empty. fix #3396 2014-06-11 09:31:29 +02:00
Ruud
f560dc093c Merge branch 'refs/heads/develop' 2014-06-10 22:54:14 +02:00
Ruud
d7f6fad3dd Unicode filenames before saving release. fix #3383 2014-06-10 22:51:15 +02:00
Ruud
699c562d34 Return default resolution if nothing found 2014-06-10 22:47:23 +02:00
Ruud
36d8225389 Trigger search change on paste. fix #3416 2014-06-10 21:02:48 +02:00
Ruud
17ba9ee96b Allow full library refresh interval. fix #2807 2014-06-10 13:54:31 +02:00
Ruud
2769fc28d3 Catch RecordNotFound error. fix #3373 2014-06-10 13:40:51 +02:00
Ruud
f5f3cfba50 More general logs 2014-06-10 11:14:19 +02:00
Ruud
1b1c77d225 Use magnetprovider for yify #3406 2014-06-10 11:13:18 +02:00
Ruud
cfc49e286b Allowed datadir giving false positive. fix #3399 2014-06-08 11:57:53 +02:00
Ruud
d26a2b1480 Merge branch 'refs/heads/develop' 2014-06-07 20:44:49 +02:00
Ruud
a2b3677c59 Settings.save doc update. closes #3391 2014-06-07 08:48:11 +02:00
Ruud
e5cfafdb00 Update Tornado 3.2.2 2014-06-06 22:25:16 +02:00
Ruud
bff05925e8 Only allow 3d tag as single word, not partial. fix #3368 2014-06-06 21:37:16 +02:00
Ruud
05f4b2b8ce Allow full scan and quick scan separately 2014-06-06 21:36:35 +02:00
Ruud
2eac294643 Allow already deleted releases 2014-06-06 20:42:01 +02:00
Ruud
f6789f79ea Import cleanup 2014-06-06 20:14:57 +02:00
Ruud
0b5976bdb1 Catch HTTPError properly in trailer search. fix #3388 2014-06-06 18:51:51 +02:00
Ruud
7d2b2b9809 Metadata fixes 2014-06-06 18:09:17 +02:00
Ruud
cce92dc1f8 Don't test for redirect. fix #3381 2014-06-06 18:04:13 +02:00
Ruud
fa7e59e842 Don't save profile order twice 2014-06-06 17:26:54 +02:00
Ruud
e11b07b559 Don't save profile order twice 2014-06-06 17:26:45 +02:00
Ruud
b6ee8ef4d4 Merge branch 'refs/heads/develop' 2014-06-06 11:24:24 +02:00
Ruud
8635f0ddb2 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-06-06 11:23:46 +02:00
Ruud
c90a423012 Disable SSL verification 2014-06-06 11:23:21 +02:00
Ruud
f0daee669b Add opener to env 2014-06-06 10:53:08 +02:00
Ruud Burger
d252b660f5 Merge pull request #3362 from clinton-hall/patch-1
Fix instructions for Ubuntu
2014-06-04 08:18:41 +02:00
Clinton Hall
e717a49c0c Fix instructions for Ubuntu 2014-06-04 13:29:20 +09:30
Ruud
426155e65c Add extra score if size is unique. fix #3344 2014-06-03 23:34:32 +02:00
Ruud
6b9b446e3d Quality guess keyerror. fix #3347 2014-06-03 22:53:30 +02:00
Ruud
ab2b2cfe6e Cleaner empty dir cleanup 2014-06-03 22:49:12 +02:00
Ruud
f80559d380 Merge branch 'refs/heads/develop' 2014-06-03 22:31:20 +02:00
Ruud
4b236c6ed6 Only cleanup source folders 2014-06-03 22:25:17 +02:00
Ruud
2396fadf04 Remove debug print 2014-06-03 20:25:57 +02:00
Ruud
a3bffb5867 Use searchOnTitle for TorrentLeech. fix #3351 2014-06-03 17:32:53 +02:00
Ruud
1b44fc40af Properly delete from late-list. fix #3350 2014-06-03 17:26:38 +02:00
Ruud
8530b00e7b Merge branch 'refs/heads/develop' 2014-06-03 17:18:11 +02:00
Ruud
b894139ca1 Make full path for logs 2014-06-03 16:54:21 +02:00
Ruud
daa0662869 XMPP was importing itself 2014-06-03 16:54:08 +02:00
Ruud
81de9529c3 Force folder creation on startup 2014-06-03 16:52:44 +02:00
Ruud
5851e1e69f Merge branch 'refs/heads/develop' 2014-06-02 23:51:01 +02:00
Ruud
6b06caf00d Api call release lock never triggered 2014-06-02 22:57:45 +02:00
Ruud
9370366112 Don't limit fanart calls 2014-06-02 22:57:27 +02:00
Ruud
32bcf6e615 Requests 2.3.1 2014-06-02 22:36:09 +02:00
Ruud
aa804471a7 Prioritize image info 2014-06-02 22:27:56 +02:00
Ruud
681d8b1ddc Simplify fanart provider 2014-06-02 22:23:26 +02:00
Ruud
686bfd62eb Merge branch 'refs/heads/develop' 2014-06-02 15:10:29 +02:00
Ruud
c82b1f51e3 Get messages from last 7 days, not just unread. fix #3331 2014-06-02 14:44:09 +02:00
Ruud
6d048e0003 Don't try to parse faulty IMDB page 2014-06-02 14:35:06 +02:00
Ruud
9b82603c26 Merge branch 'refs/heads/develop' 2014-06-02 14:20:50 +02:00
Ruud
0314910bbe Don't migrate empty library items 2014-06-02 14:11:26 +02:00
Ruud
3bd831782c Release lock inside thread 2014-06-02 14:02:55 +02:00
Ruud
40f01dca6f Use async request for all api calls 2014-06-02 13:31:18 +02:00
Ruud
f41792915f Merge branch 'refs/heads/develop' 2014-06-02 12:59:47 +02:00
Ruud
8dead66b58 Migration fixes 2014-06-02 12:59:21 +02:00
Ruud
2fa77fb610 Merge branch 'refs/heads/develop' 2014-06-02 10:40:07 +02:00
Ruud
18807191c0 Don't reindex on startup 2014-06-01 17:36:34 +02:00
Ruud
9d9630a27a Sorted backup files 2014-06-01 16:46:21 +02:00
Ruud
8ac851555d Can't trigger same api call
Thread never closes
2014-06-01 16:14:53 +02:00
Ruud
e64d0e33fc Merge branch 'refs/heads/develop' 2014-06-01 14:31:39 +02:00
Ruud
27f331a1fc Don't verify ssl for downloaders 2014-06-01 14:30:45 +02:00
Ruud
e6b4d32506 IMDB Watchlist count was off 2014-06-01 11:37:57 +02:00
Ruud
a28ee58a1f Remove digestauth header 2014-06-01 00:26:37 +02:00
Ruud
47749c2d73 Transmission login failed. #1110 2014-06-01 00:23:46 +02:00
Ruud
d6d0ff724a Change label 2014-06-01 00:11:09 +02:00
Ruud
ba65700aad Use textarea value for log posting 2014-05-31 23:46:34 +02:00
Ruud
b168643600 Merge branch 'refs/heads/develop'
Conflicts:
	couchpotato/core/helpers/variable.py
2014-05-31 22:50:02 +02:00
Ruud
84a7cfe07d Add CP version by default in logs 2014-05-31 22:09:18 +02:00
Ruud
9ccd4a5e84 Shutdown logging 2014-05-31 21:56:37 +02:00
Ruud
616434a00f Delay release cleanup 2014-05-31 21:31:06 +02:00
Ruud
4cf62f73da Use proper conf variable 2014-05-31 19:51:58 +02:00
Ruud
0145aecab4 data['size'] sometimes doesn't exist 2014-05-31 13:53:45 +02:00
Ruud
6c4184d1f5 Use minimal requirements for popular movie automation 2014-05-31 13:37:30 +02:00
Ruud
9d011b42a9 Moved PopularMovies automation to single file 2014-05-31 13:30:40 +02:00
Ruud
bf81b5cacc Move automation provider 2014-05-31 13:24:23 +02:00
Ruud
8d2b6e4097 Merge branch 'refs/heads/sjlu-develop' into develop 2014-05-31 13:22:18 +02:00
Ruud
50d8399f09 Merge branch 'develop' of git://github.com/sjlu/CouchPotatoServer into sjlu-develop 2014-05-31 13:21:58 +02:00
Ruud
bc99b77dbe Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-05-31 13:18:48 +02:00
Ruud
1c7edc9487 TPB and Kickass proxy update 2014-05-31 13:18:11 +02:00
Ruud Burger
90c06fb3c9 Merge pull request #3290 from kackar/patch-4
Update contributing.md
2014-05-31 13:14:17 +02:00
Ruud Burger
10a04c16ba Merge pull request #3289 from kackar/patch-3
Update README.md
2014-05-31 13:13:29 +02:00
Ruud
90a618bd7e Allow search on imdb urls 2014-05-31 12:33:35 +02:00
Ruud
b630b84ab0 Get proper poster image from tmdb 2014-05-31 12:09:55 +02:00
Ruud
a5ee362fc0 Remove scandir lib, use os.walk 2014-05-31 11:53:24 +02:00
Ruud
7c0870b6b8 Capitalize 2014-05-29 20:44:15 +02:00
Ruud
a42264b280 Tornado 3.2.1 2014-05-29 20:09:16 +02:00
Ruud
e714604ec0 Requests 2.3.0 2014-05-29 20:01:22 +02:00
Ruud
c094120f04 Do verify requests 2014-05-29 19:50:36 +02:00
Ruud
6691c8ddd7 Convert underscore method to proper camelcase 2014-05-29 19:43:33 +02:00
kackar
013705c318 Update contributing.md 2014-05-25 21:40:49 +02:00
kackar
bda6f92a4d Update README.md 2014-05-25 21:34:59 +02:00
Ruud
7ceb8dc79c ILoveTorrents search fix 2014-05-19 21:49:08 +02:00
Ruud
7f48210c97 Use libs as import 2014-05-19 21:16:58 +02:00
Ruud
23c440cd58 Merge branch 'refs/heads/Boehemyth-develop' into develop 2014-05-19 21:07:56 +02:00
Ruud
0097167dec Fanart PR cleanup 2014-05-19 21:07:51 +02:00
Ruud
21e5f156bb Merge branch 'develop' of git://github.com/Boehemyth/CouchPotatoServer into Boehemyth-develop 2014-05-19 19:41:31 +02:00
Ruud
08f55314d5 Re-use imdb page parser 2014-05-19 19:25:16 +02:00
Ruud
577bf09859 Add fallback imdb filter.
BeautifulSoup fails to load elements based on ID for the html imdb returns...
Hacky way of filtering out the correct elements.
2014-05-19 13:35:44 +02:00
Ruud Burger
c446cd2fb0 Merge pull request #3252 from mano3m/develop_notif
Add 3D to download notification. fixes #3242
2014-05-19 10:28:43 +02:00
mano3m
06a8414f12 Add 3D to download notification. fixes #3242 2014-05-13 20:23:43 +02:00
Ruud
1ac01456a9 Don't show snatched movies in late section. fix #3013 2014-05-11 20:18:50 +02:00
Ruud
b86853f06f More path encoding 2014-05-11 19:36:22 +02:00
Ruud
311a2798dd Revert "Encode before path join"
This reverts commit b87c00c041.
2014-05-11 18:58:14 +02:00
Ruud
fe9998fb9d Revert "Don't re-encode by filesystem encoding"
This reverts commit d5e19db5e6.
2014-05-11 18:53:13 +02:00
Ruud
ce648c5d35 Post mass edit commands 2014-05-11 18:35:38 +02:00
Ruud
5a2a9bbf9a Loop through release files properly 2014-05-11 17:40:55 +02:00
Ruud
0f8ab05fd4 Clean up managed movies 2014-05-11 17:40:43 +02:00
Ruud
b87c00c041 Encode before path join 2014-05-11 17:18:27 +02:00
Ruud
8999f51dc9 Make quality guess debug message 2014-05-11 16:07:23 +02:00
Ruud
d5e19db5e6 Don't re-encode by filesystem encoding 2014-05-11 16:03:27 +02:00
Ruud
675bee83ca Path encode 2014-05-11 16:02:49 +02:00
Ruud
33e5dd1fdb Speed up log highlight
Allow reverse selection
2014-05-11 00:52:55 +02:00
Ruud Burger
4ff2794c83 Merge pull request #3234 from mikke89/fix-providers
Fix searches on torrentday and sceneaccess
2014-05-11 00:30:13 +02:00
Ruud
81f9302da1 Use super threaded db connection 2014-05-11 00:19:05 +02:00
Ruud
93f4b8b537 Don't log non existing properties 2014-05-11 00:14:56 +02:00
mikke89
0587d2f8db Fix searches on torrentday and sceneaccess 2014-05-10 22:26:41 +02:00
Ruud
6ba25b5468 Better highlight 2014-05-10 15:58:29 +02:00
Ruud
cc10969506 Keep log filter
Pre-fill in issue when possible
2014-05-10 15:53:27 +02:00
Ruud
c2eb50a7ee Log reporting 2014-05-10 13:23:13 +02:00
Dan Boehm
33d24068fd Merge remote-tracking branch 'upstream/develop' into develop 2014-05-09 13:04:36 -05:00
Ruud
3a4c191b11 Make logs filterable 2014-05-09 16:53:25 +02:00
Ruud
e06b4ccb3f Ignore "wait for" for all if 1 is old enough 2014-05-09 15:53:37 +02:00
Ruud
3c6b86ea28 Delay first search 2014-05-09 15:53:07 +02:00
Ruud
c4a9a13d6c Don't continue searching lower qualities of correct one is found 2014-05-09 14:30:06 +02:00
Ruud
c0f1a3c603 Show chart scrollbar only on hover 2014-05-09 12:14:29 +02:00
Ruud
9d3425061a Resize thumbnail-less soon movies 2014-05-09 12:04:07 +02:00
Ruud
c2dcd2f67d Use the "wait for" option properly. fix #3224 2014-05-09 11:46:32 +02:00
Ruud
24b822aecd Info2 log 2014-05-09 11:44:58 +02:00
Ruud
a7d3de766f Don't migrate release if quality doesn't exist 2014-05-09 00:58:52 +02:00
Ruud
b56c897e4b Don't give negative score for non matching size 2014-05-08 23:45:35 +02:00
Ruud
df14032107 Add offset for log partial 2014-05-08 16:59:41 +02:00
Ruud
66b4821f7f Profile references before assigned. fix #3220 2014-05-08 16:40:27 +02:00
Ruud
d301cde266 Newznab custom tag wasn't used. fix #3219 2014-05-08 16:37:36 +02:00
Ruud
0590a0d722 Update log api 2014-05-08 16:30:58 +02:00
Ruud
fc71a03a12 Just loop over log array 2014-05-08 16:27:13 +02:00
Ruud
923c794e39 Logs return list 2014-05-08 16:16:42 +02:00
Ruud
e7fbff5b3f Only remove non-existing releases only once 2014-05-08 15:40:24 +02:00
Ruud
1bd556fbb3 Close DB on shutdown 2014-05-08 15:39:40 +02:00
Ruud
18a870f8c3 Log if no quality is found 2014-05-08 14:57:26 +02:00
Ruud
3e2a2c3bee Remove unused variable 2014-05-08 14:52:03 +02:00
Ruud
73e74881a6 Always return handler 2014-05-08 14:51:53 +02:00
Ruud
b37112600e Only cache ignored proxies for 1 day 2014-05-08 14:51:42 +02:00
Ruud
6172ce4960 Use contains other quality in log 2014-05-08 14:51:26 +02:00
Ruud
3d277e1c01 Quality scoring and tests 2014-05-08 14:51:12 +02:00
Ruud
b3b13899f1 Return found qualities 2014-05-08 14:50:59 +02:00
Ruud
7c4a59539a Return brrip in Yify provider 2014-05-08 14:50:04 +02:00
Ruud
240283405e variable 'year' referenced before assignment 2014-05-07 11:50:36 +02:00
Ruud
e6dfb3da16 Allow last year films to be search after April 2014-05-07 00:47:50 +02:00
Ruud
8e220ededa Bitsoup: Allow param in search url 2014-05-07 00:25:18 +02:00
Ruud
11126f8083 forceDefaults priority 2014-05-07 00:23:08 +02:00
Ruud
ac8a13db22 Remove orphaned releases 2014-05-06 23:49:34 +02:00
Ruud
5ab10ff97a Change dognzb default url 2014-05-06 22:24:58 +02:00
Ruud
f3b0346ba2 Use encoding as backup 2014-05-06 21:38:08 +02:00
Ruud
96c94f97f4 Filter out tvshows in charts 2014-05-06 20:35:00 +02:00
Ruud
192c0200e5 Disable top 250 chart 2014-05-06 20:06:06 +02:00
Ruud
03ae8f459c Merge branch 'refs/heads/mano3m-develop_higherq' into develop 2014-05-06 16:08:47 +02:00
Ruud
377fdd9e5e Use correct event 2014-05-06 16:08:36 +02:00
Ruud
daec7d20fe Merge branch 'develop_higherq' of git://github.com/mano3m/CouchPotatoServer into mano3m-develop_higherq 2014-05-06 16:00:16 +02:00
Ruud
66a149590b Chart fixes 2014-05-06 15:51:09 +02:00
Ruud
1b6f010df2 Merge branch 'refs/heads/mano3m-develop_rentals' into develop 2014-05-06 15:39:44 +02:00
Ruud
7e4bc29b59 Chart cleanup 2014-05-06 15:39:41 +02:00
Ruud
0284fa9b0a Load correct beautifulsoup module 2014-05-06 14:31:24 +02:00
Ruud
e5bcea59b5 Merge branch 'develop_rentals' of git://github.com/mano3m/CouchPotatoServer into mano3m-develop_rentals 2014-05-06 14:14:41 +02:00
Ruud
16f603ced2 Cast float 2014-05-05 22:42:14 +02:00
Ruud
bdcb3b7e33 Merge branch 'refs/heads/mano3m-develop_3D_stuff' into develop 2014-05-05 22:36:45 +02:00
Ruud
0def6fcfe3 Cleanup PR 2014-05-05 22:36:41 +02:00
Ruud
75a352fef3 Merge branch 'develop_3D_stuff' of git://github.com/mano3m/CouchPotatoServer into mano3m-develop_3D_stuff 2014-05-05 22:13:36 +02:00
Ruud
07eb1f7f4c Allow page ordering 2014-05-05 22:01:11 +02:00
Ruud
8e35c02763 Cleanup searchOnTitle queries 2014-05-05 21:18:30 +02:00
Ruud
c1f6d9a858 Merge branch 'refs/heads/mikke89-dev-torrentsearch' into develop 2014-05-05 20:45:04 +02:00
Ruud
3e20a3bac7 Merge branch 'dev-torrentsearch' of git://github.com/mikke89/CouchPotatoServer into mikke89-dev-torrentsearch 2014-05-05 20:44:34 +02:00
Ruud
818570fd2d Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-05-05 20:42:22 +02:00
Ruud
bcd2d22fbf Update bitsoup searchOnTitle 2014-05-05 20:38:47 +02:00
mikke89
ffc99cd4f4 Fix false positives from Sceneaccess and Torrentday 2014-05-04 23:23:45 +02:00
Ruud Burger
bb56750c1a Merge pull request #3143 from mano3m/develop_charts
Improve charts
2014-05-04 14:50:15 +02:00
Ruud Burger
b08d587a22 Merge pull request #3166 from harrv/develop
Added an mpaa_only rename replacement token
2014-05-04 13:58:37 +02:00
Ruud Burger
47f4132b39 Merge pull request #3188 from rfgamaral/duplicate_subtitle_identifier
Avoid duplicate subtitle language identifier
2014-05-04 13:48:06 +02:00
Ruud Burger
faefab5554 Merge pull request #3189 from rfgamaral/force_download_subtitles
Force download all subtitle languages
2014-05-04 13:44:39 +02:00
Ruud Burger
243a033055 Merge pull request #3192 from mano3m/develop_yifi
Fix Yifi
2014-05-04 13:42:52 +02:00
Ruud Burger
db1eeaae38 Merge pull request #3193 from mano3m/develop_api
Add Blu-ray to Bit HDTV provider
2014-05-04 13:41:17 +02:00
mano3m
8c2960e891 Add Blu-ray to Bit HDTV provider
Fixes #3171
2014-05-04 11:14:54 +02:00
mano3m
d6a86e8616 correct caps 2014-05-04 10:11:25 +02:00
mano3m
5260f42378 Fix Yifi
this should fix #3114
2014-05-04 10:03:30 +02:00
Ricardo Amaral
84f28f3c54 Add advanced option to force download all languages 2014-05-03 17:50:12 +01:00
Ricardo Amaral
860b6793fb Prevent duplicate subtitle language identifier 2014-05-03 17:44:03 +01:00
harrv
df03409d7a Added an mpaa_only rename replacement token
The mpaa replacement token includes certifications from around the world. If the user wishes to limit the values to one of 'G', 'PG', 'PG-13', 'R', 'NC-17' or 'Not Rated' they can use the added mpaa_only replacement token. The original mpaa replacement token remains unchanged.
2014-04-27 00:47:38 -06:00
Dan Boehm
6a81f2241d Added option to run the Artwork Downloader addon during XBMC notify.
This option will only work in XBMCv12 (Frodo) or later.  It also requires the Artwork Downloader
Addon.

Since XBMC's API doesn't support notifications over HTML, there is no way for couchpotato to know
when the Library Scan is complete.  Since running the Artwork Downloader before the movie has
been scanned won't solve anything, a delay timer can be adjusted to suit the user's needs.

Squashed commit of the following:

commit bd60ed585f77cc40c31fd67d4ae732e0845d31ab
Merge: fcb092e b113a4d
Author: Dan Boehm <dboehm.dev@gmail.com>
Date:   Thu Apr 24 14:26:24 2014 -0500

    Merge branch 'fanarttv' into artdlnotify

commit b113a4def197a9ca8545bde9f5081c0591b93b36
Author: Dan Boehm <dboehm.dev@gmail.com>
Date:   Thu Apr 24 14:24:12 2014 -0500

    Bug-fix and code cleanup.

    Fixed a bug where the movie.info event would crash if there aren't any pictures to scrape in
    fanart.tv.

commit fcb092e776e00ceabea016b3c26d9394e32d72b0
Author: Dan Boehm <dboehm.dev@gmail.com>
Date:   Thu Apr 24 14:21:27 2014 -0500

    Option to run the artwork downloader addon during XBMC notify.

commit adf7a4675d472e9e95a316c6cccc681a52804f13
Author: Dan Boehm <dboehm.dev@gmail.com>
Date:   Wed Apr 23 16:15:03 2014 -0500

    Added support for extrafanart.
    Also, the main fanart will be taken from fanart.tv unless one
    does not exist.

commit 1791e46c8602f40bb56fe0cf7ecb0607f35b4b12
Author: Dan Boehm <dboehm.dev@gmail.com>
Date:   Wed Apr 23 15:13:14 2014 -0500

    Couchpotato now downloads extrathumbs from the extra tmdb backdrops if they exist.

    This commit made some major changes to the core image creation functionality that
    makes writing multiple images to folders possible.

commit c0858807873749dbc928c0260037138f51f894ca
Author: Dan Boehm <dboehm.dev@gmail.com>
Date:   Wed Apr 23 12:18:53 2014 -0500

    Bug Fix & Implemented functionality to select bluray or dvd disc images.

    Currently, only blurays will be selected, unless there are no blurays.
    However, if a mechanism for determining the quality of the release is
    implemented, it would be simple to make this selection based on the
    quality.

commit 786751371d243f53d0f5c6f2c38d92288d8608ba
Author: Dan Boehm <dboehm.dev@gmail.com>
Date:   Wed Apr 23 10:59:25 2014 -0500

    Fixed a bug where non-HD clearart and logos couldn't be downloaded.

commit feda8df483d13b5a5df3a869f25de8f2c7e6ffe3
Author: Dan Boehm <dboehm.dev@gmail.com>
Date:   Wed Apr 23 10:12:31 2014 -0500

    Fixed some problems that were missed with the previous merge.

commit 5ddab6c40e69a5accc6c0336cd7485920ff82d8f
Merge: 7273abf ff46aa0
Author: Dan Boehm <dboehm.dev@gmail.com>
Date:   Wed Apr 23 10:02:11 2014 -0500

    Merge branch 'develop' into fanarttv

    Conflicts:
    	couchpotato/core/media/movie/providers/info/themoviedb.py
    	couchpotato/core/providers/metadata/xbmc/__init__.py

commit 7273abf827735cf245711c3d3199a6a173a964aa
Author: dan <dan@DBoehm-Arch.danboehm>
Date:   Thu Feb 27 13:29:57 2014 -0600

    Downloads extra artwork from fanart.tv

    Downloads occur with correct filenaming when XBMC metadata is generated,
    but the image URLs are selected when the movie.info event is called.

commit 9080d9d749c7e1ddbdc78f7b37a3c5f83195d580
Author: dan <dan@DBoehm-Arch.danboehm>
Date:   Wed Feb 26 16:31:37 2014 -0600

    Added basic functionality for fanarttv provider.

    This should be mostly done and is based on the tvdb provider.

commit 1b39b246c2a9d65f9ef86c4e150a12d893e362c0
Author: dan <dan@DBoehm-Arch.danboehm>
Date:   Wed Feb 26 14:50:17 2014 -0600

    Updated fanarttv library with the correct package hierarchy
    (libs.fanarttv).

commit 8abb7c8f8ad3347900debb9f6a6d5a7acb7df396
Author: dan <dan@DBoehm-Arch.danboehm>
Date:   Wed Feb 26 13:12:48 2014 -0600

    Added fanart.tv API python library (lib.fanarttv).

    The upstream for this library is at
    https://github.com/z4r/python-fanart.
2014-04-24 15:02:29 -05:00
Dan Boehm
5ce817cee6 Support for downloading extra artwork from Fanart.tv (resolves #1023).
New image types include:
* clearart
* discart
* extrathumbs
* extrafanart
* logo
* banner
* landscape (16:9 Thumb)

There are a couple things that should be noted:
1. Only English images will be downloaded.
2. The fanart image is now downloaded from Fanart.tv if it can find one, otherwise it uses TMDB
like it used to.  This is because the images on Fanart.tv tend to be higher resolutions &
quality.
3. Since multiple extrathumbs and extrafanarts are downloaded into a subdirectory, subdirectories
are now supported for metadata file names.  The subdirectories will be automatically created if
they don't exist.
4. Bluray discart will always be preferred over DVD.  Ideally, it would prefer DVD versions for
SD quality movies, but I couldn't find an easy way to determine the quality from within the
plugin.  I suspect major changes would be needed to the plugin system in general in order to get
this to work.  If a user cares about the distinction, the best work-around is to not download
these in Couchpotato and run the Artwork Downloader addon from within XBMC.
5. A maximum of 4 extrathumbs and 20 extrafanarts will be downloaded.

Squashed commit of the following:

commit b113a4def197a9ca8545bde9f5081c0591b93b36
Author: Dan Boehm <dboehm.dev@gmail.com>
Date:   Thu Apr 24 14:24:12 2014 -0500

    Bug-fix and code cleanup.

    Fixed a bug where the movie.info event would crash if there aren't any pictures to scrape in
    fanart.tv.

commit adf7a4675d472e9e95a316c6cccc681a52804f13
Author: Dan Boehm <dboehm.dev@gmail.com>
Date:   Wed Apr 23 16:15:03 2014 -0500

    Added support for extrafanart.
    Also, the main fanart will be taken from fanart.tv unless one
    does not exist.

commit 1791e46c8602f40bb56fe0cf7ecb0607f35b4b12
Author: Dan Boehm <dboehm.dev@gmail.com>
Date:   Wed Apr 23 15:13:14 2014 -0500

    Couchpotato now downloads extrathumbs from the extra tmdb backdrops if they exist.

    This commit made some major changes to the core image creation functionality that
    makes writing multiple images to folders possible.

commit c0858807873749dbc928c0260037138f51f894ca
Author: Dan Boehm <dboehm.dev@gmail.com>
Date:   Wed Apr 23 12:18:53 2014 -0500

    Bug Fix & Implemented functionality to select bluray or dvd disc images.

    Currently, only blurays will be selected, unless there are no blurays.
    However, if a mechanism for determining the quality of the release is
    implemented, it would be simple to make this selection based on the
    quality.

commit 786751371d243f53d0f5c6f2c38d92288d8608ba
Author: Dan Boehm <dboehm.dev@gmail.com>
Date:   Wed Apr 23 10:59:25 2014 -0500

    Fixed a bug where non-HD clearart and logos couldn't be downloaded.

commit feda8df483d13b5a5df3a869f25de8f2c7e6ffe3
Author: Dan Boehm <dboehm.dev@gmail.com>
Date:   Wed Apr 23 10:12:31 2014 -0500

    Fixed some problems that were missed with the previous merge.

commit 5ddab6c40e69a5accc6c0336cd7485920ff82d8f
Merge: 7273abf ff46aa0
Author: Dan Boehm <dboehm.dev@gmail.com>
Date:   Wed Apr 23 10:02:11 2014 -0500

    Merge branch 'develop' into fanarttv

    Conflicts:
    	couchpotato/core/media/movie/providers/info/themoviedb.py
    	couchpotato/core/providers/metadata/xbmc/__init__.py

commit 7273abf827735cf245711c3d3199a6a173a964aa
Author: dan <dan@DBoehm-Arch.danboehm>
Date:   Thu Feb 27 13:29:57 2014 -0600

    Downloads extra artwork from fanart.tv

    Downloads occur with correct filenaming when XBMC metadata is generated,
    but the image URLs are selected when the movie.info event is called.

commit 9080d9d749c7e1ddbdc78f7b37a3c5f83195d580
Author: dan <dan@DBoehm-Arch.danboehm>
Date:   Wed Feb 26 16:31:37 2014 -0600

    Added basic functionality for fanarttv provider.

    This should be mostly done and is based on the tvdb provider.

commit 1b39b246c2a9d65f9ef86c4e150a12d893e362c0
Author: dan <dan@DBoehm-Arch.danboehm>
Date:   Wed Feb 26 14:50:17 2014 -0600

    Updated fanarttv library with the correct package hierarchy
    (libs.fanarttv).

commit 8abb7c8f8ad3347900debb9f6a6d5a7acb7df396
Author: dan <dan@DBoehm-Arch.danboehm>
Date:   Wed Feb 26 13:12:48 2014 -0600

    Added fanart.tv API python library (lib.fanarttv).

    The upstream for this library is at
    https://github.com/z4r/python-fanart.
2014-04-24 15:00:04 -05:00
mano3m
7cdf124f9d Improve charts
- Add a max height to each chart with a scrollbar
- Add advanced options to hide chart items already in wanted or library
(note that this can be done more efficiently...)
2014-04-21 16:18:51 +02:00
Ruud Burger
ff46aa0226 Merge pull request #3138 from mano3m/develop_kat
Add verified only option for kat
2014-04-21 15:21:37 +02:00
mano3m
669e331f6c Ruud's comments 2014-04-21 12:15:05 +02:00
mano3m
4179ba642b Various Fixes 2014-04-21 00:26:23 +02:00
mano3m
00954d98f7 Improve scanner
- Fix the disc and tag removal: they received the filename with capse
- add default metadata when resolution is known
2014-04-21 00:26:21 +02:00
mano3m
037e77860b Add 3D type to renamer (e.g. SBS, Half OU, etc) 2014-04-21 00:26:20 +02:00
mano3m
47e187449d Add use of size to scanner
And check if snatched quality is the same as what we detected
2014-04-21 00:26:19 +02:00
mano3m
06e9afbe69 Improve quality self test 2014-04-21 00:26:18 +02:00
mano3m
bfe8aa5f5f Add size to quality guessing
And cleanup searcher
2014-04-21 00:26:17 +02:00
mano3m
e51ddd7a50 BR-Disk detection fixes 2014-04-21 00:26:16 +02:00
mano3m
442552c024 fix debug msg 2014-04-21 00:26:16 +02:00
mano3m
ce4806df64 Add 3D renamer option 2014-04-21 00:26:15 +02:00
mano3m
0c2e65c92b Check for better quality
Actually check the quality profile order and determine:
- if the searcher needs to search for a certain quality
- if the renamer needs to rename a certain qualoty release

Fixes #3122
2014-04-21 00:25:27 +02:00
mano3m
b01aa2b385 Add verified only option for kat
Fixes ##3137
2014-04-20 22:54:43 +02:00
Ruud Burger
2e04890756 Merge pull request #3087 from softcat/develop
Added filmstarts.de userscript
2014-04-20 10:20:59 +02:00
Ruud Burger
1657857b4a Merge pull request #3131 from mano3m/develop_prefix
Add 'A' and 'An' to 'The' prefix
2014-04-20 10:20:25 +02:00
mano3m
72383592ba Clean-up 2014-04-20 10:18:44 +02:00
Ruud Burger
d093f935f9 Merge pull request #3130 from mano3m/develop_binsearch
Simplify binsearch result string
2014-04-20 10:09:02 +02:00
Ruud Burger
8cc7d101aa Merge pull request #3119 from mano3m/develop_tagging
Only tag existing files
2014-04-20 10:07:43 +02:00
Ruud Burger
f39eebbd22 Merge pull request #3118 from mano3m/develop_spotweb
Add password searching in spots from spotweb
2014-04-20 10:07:16 +02:00
Ruud Burger
3ac8bc738a Merge pull request #3105 from mano3m/develop_standardize_renamer
Use more standardized codec/source names
2014-04-20 10:06:46 +02:00
mano3m
0eac041a26 Add 'A' and 'An' to 'The' prefix
This was bothering me for a long time now ;) We do put The at the end
but not A nor An. Fixed now :)
2014-04-19 22:20:17 +02:00
mano3m
ab0f5daaf3 Simplify binsearch result string
fixes #3099
2014-04-19 21:17:44 +02:00
mano3m
b59a0f82ab Add IMDB rentals list to charts
This should add the IMDB rentals list to the charts and imdb automation.
This is actually a nice list as you can download the movies right away
instead of waiting until they release like with the rest of the imdb
charts.

The problem is that this does not work. And frankly I gave up. When I
type this in my python command window it works:

'''
from bs4 import BeautifulSoup
import urllib2

data = urllib2.urlopen('http://www.imdb.com/boxoffice/rentals')
html = BeautifulSoup(data)
result_div = html.find('div', attrs = {'id': 'main'})
'''

Then result_div contains the list of movies. In the code from this PR
result_div becomes None....?!?!?! @Ruudburger please help before I jump
off my building ;)
2014-04-19 21:13:32 +02:00
mano3m
9b75e6af5c Only tag existing files
Fixes #3088
2014-04-15 21:35:16 +02:00
mano3m
aa37f2b0ef Add password searching in spots from spotweb 2014-04-15 19:23:07 +02:00
Steven Lu
d22237a5cc Adding in a new source for automation. 2014-04-14 23:50:10 -04:00
Ruud Burger
26f5e8aa4b Merge pull request #3109 from mano3m/develop_provider
Provider fixes
2014-04-14 09:06:07 +02:00
Ruud Burger
9072c6cae0 Merge pull request #3112 from jonnsl/bluray_chart
Don't show duplicated results in the blu-ray releases chart.
2014-04-14 09:01:22 +02:00
Jonnathan
8739c1197f Don't show duplicated results in the blu-ray releases chart. 2014-04-14 03:56:59 -03:00
mano3m
a477973862 Provider fixes
Fixes #3097 #3086  #3106
2014-04-13 15:48:25 +02:00
mano3m
95ce26d261 Use more standardized codec/source names
Fixes #999
2014-04-12 18:27:59 +02:00
Joel Kåberg
8c934c1ca8 Merge pull request #3080 from fuzeman/feature/dev_rtorrent
[rtorrent] fixed how torrent status is determined
2014-04-09 14:48:18 +02:00
softcat
349d7d4866 Added filmstarts.de userscript 2014-04-08 13:44:03 +02:00
Dean Gardiner
f1ea8fa693 [rtorrent] fixed how torrent status is determined 2014-04-06 22:24:27 +12:00
Ruud
685210aee3 Nested media index 2014-04-05 21:18:09 +02:00
Ruud
ae42b62b3c Remove downloaders.js from clientscript 2014-04-05 16:39:31 +02:00
Ruud
7faa7c3dba Use correct super class 2014-04-05 12:48:36 +02:00
Ruud
eba36b6d57 Allow type option in listing 2014-04-05 11:52:10 +02:00
Ruud
84a2afe08f Refactor downloaders and pages 2014-04-05 11:30:23 +02:00
Ruud
98a85f6950 Charts cleanup 2014-04-05 09:54:24 +02:00
Ruud
c89c99b272 Don't refresh charts at startup 2014-04-04 19:06:20 +02:00
Ruud
3f16dbd09c Sort releases based on preferred method in api return 2014-04-04 17:52:33 +02:00
Ruud
e547851905 Failed deleting from wanted 2014-04-04 17:17:26 +02:00
Ruud
cbb0462948 Only list inactive downloadstatus support once 2014-04-04 17:10:57 +02:00
Ruud Burger
a185292578 Merge pull request #3052 from jeremiahelroy/develop
making the scanner follow symlinks
2014-04-04 15:58:14 +02:00
Ruud Burger
cec1f54cdd Merge pull request #3042 from mano3m/develop_update_unrar
Update unrar2 lib to 0.99.3
2014-04-04 15:56:34 +02:00
Ruud
0112a3141b Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-04-04 15:50:41 +02:00
Ruud
5f93b08c23 Merge branch 'refs/heads/mikke89-charts-v2' into develop 2014-04-04 15:50:32 +02:00
Ruud
ff0de896c4 Cleanup and default charts 2014-04-04 15:50:20 +02:00
Ruud Burger
6d98f67668 Merge pull request #3070 from mano3m/develop_fix_ignore
Check if folder exists in tagging
2014-04-04 14:59:33 +02:00
mikke89
5d5cf5cf29 Display charts (such as from imdb, blu-ray.com) on home page. 2014-04-04 06:42:50 +02:00
mano3m
610edea20e Re-add path 2014-04-03 22:46:22 +02:00
mano3m
8f4219a93c Check if folder exists in tagging
Fixes #3069
2014-04-03 22:41:38 +02:00
Ruud
9540ae5a19 Hotlink userscript gif 2014-04-01 20:48:58 +02:00
Ruud
0f7c3f5d0f Use correct id returned from automation add. fix #3050 2014-04-01 20:38:27 +02:00
Ruud
39fb3a1107 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-04-01 20:34:04 +02:00
Ruud
e609931d2c Show off browser extension 2014-04-01 20:33:47 +02:00
Ruud Burger
70d94cda8c Merge pull request #3051 from bazbjzy/develop
Added custom sounds ability to Pushover Advanced Settings
2014-04-01 08:07:22 +02:00
jeremiahelroy
5c89a52f23 making the scanner follow symlinks 2014-04-01 00:48:08 -04:00
bazbjzy
686e0a9441 Removed empty line 2014-03-31 18:04:02 -07:00
bazbjzy
e8dcf5ee02 Added ability to configure Pushover custom sounds. 2014-03-31 17:59:14 -07:00
Ruud
95369e79a5 Add profile to returned in_wanted values 2014-03-31 00:31:19 +02:00
Ruud
eb0a8454bc Update description text 2014-03-30 23:35:18 +02:00
Ruud
4f059c2549 Point to browser extension 2014-03-30 23:32:56 +02:00
Ruud
fb7dbd5716 Update Userscript 2014-03-30 23:26:14 +02:00
Ruud Burger
07fc4b3728 Merge pull request #3043 from fuzeman/develop_renamer
Fixed release files bug in renamer
2014-03-30 17:52:53 +02:00
Ruud Burger
2d5b02baf9 Merge pull request #3040 from fuzeman/develop_iptorrents
Fixed searching bug in IPT provider
2014-03-30 17:50:41 +02:00
Ruud
f8b2547a45 Movie add faulty parameter. fix #3020 2014-03-30 17:33:44 +02:00
Dean Gardiner
f8cc8acfec Fixed release files bug in renamer 2014-03-30 21:54:44 +13:00
Dean Gardiner
17787c5a4f Fixed searching bug in IPT provider 2014-03-30 20:58:10 +13:00
Ruud
304de5adb6 Load correct html files 2014-03-29 23:47:35 +01:00
Ruud
46db38c5bf Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-03-29 23:44:41 +01:00
Ruud
99e77e409a Spring cleanup 2014-03-29 23:44:14 +01:00
mano3m
6152ddbd5f Unrar cleanup 2014-03-29 21:39:38 +01:00
mano3m
f99a94d685 Update unrar2 lib to 0.99.3
Fixes #2930
2014-03-29 21:25:26 +01:00
Ruud Burger
47f58ff45f Merge pull request #3039 from fuzeman/feature/dev_rtorrent
[rtorrent] Removed broken ratio group usage
2014-03-29 08:25:36 +01:00
Dean Gardiner
f225066130 [rtorrent] Removed broken ratio group usage 2014-03-29 01:21:05 +13:00
Ruud
83c5d701b3 Use release as dict not class 2014-03-24 22:14:47 +01:00
Ruud
ffb3359e66 Compact and reindex database api calls 2014-03-24 21:41:48 +01:00
Ruud
0861b21532 Make sure title is valid when adding it to index 2014-03-24 21:40:05 +01:00
Ruud
e7420367f1 Add Torrentz provider 2014-03-23 21:58:08 +01:00
Ruud
1998c779c7 Add more trackers 2014-03-23 21:50:37 +01:00
Ruud
93eb33811a Use correct default type 2014-03-23 21:50:24 +01:00
Ruud
d7bf9dba01 Skip leftover release info 2014-03-23 20:14:10 +01:00
Ruud
d5c6942266 Use media_id to get movie in renamer 2014-03-23 17:55:33 +01:00
Ruud
e870fab277 Scanner didn't use correct get key to determine movie 2014-03-23 17:55:12 +01:00
Ruud
f3ae63c7a9 Don't extend release_download when none is set 2014-03-23 17:50:49 +01:00
Ruud Burger
3df1f1b153 Merge pull request #3009 from wouter0100/patch-1
Fixed first item in quality group
2014-03-23 15:53:11 +01:00
Wouter van Os
74fd7c684e Fixed first item in quality group
First item within a quality group had always 3d on "true".
2014-03-23 15:18:18 +01:00
Ruud
745b262800 Don't check 3d checkbox on add 2014-03-22 23:10:31 +01:00
Ruud
72f6516a1c Fix handle image url 2014-03-22 23:01:03 +01:00
Ruud
7bb723d6b3 Key errors 2014-03-22 22:39:26 +01:00
Ruud
2ccdc8ffdc Allow 3d categories 2014-03-22 22:00:26 +01:00
Ruud
1cabf64993 Add 3d categories 2014-03-22 21:32:27 +01:00
Ruud
c55bd5a35d Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-03-22 21:06:59 +01:00
Ruud
77a3552797 Add 3d support for searching 2014-03-22 21:03:19 +01:00
jkaberg
81efd4bce7 [qbittorrent] minor cleanup 2014-03-22 18:35:11 +01:00
Ruud
98183ccc1e Sorted releases 2014-03-22 14:38:58 +01:00
Ruud Burger
09df863b6c Merge pull request #3004 from ramon86/develop
Added FilmCentrum.nl as a provider for the Chrome extension
2014-03-22 14:33:46 +01:00
Ramon van Dam
4e70c1882b Added FilmCentrum.nl as a provider for the Chrome extension 2014-03-22 14:31:29 +01:00
Ruud
d38d581d1d Add 3d to quality tags on movie item 2014-03-22 12:36:11 +01:00
Ruud
61c95240c2 Failed editing movie 2014-03-22 12:35:56 +01:00
Ruud
59347400c3 Add 3D tags 2014-03-22 12:20:16 +01:00
Ruud
f976e04597 Save 3d in quality profile 2014-03-22 12:19:42 +01:00
Ruud
1602fe88e6 Close connection not cursor 2014-03-22 09:59:22 +01:00
Ruud
d4eca60b1d Identifiers fix 2014-03-22 09:52:21 +01:00
Ruud
5a4467adb9 Only return movies from omdb 2014-03-21 21:40:38 +01:00
Ruud
f50852fee0 Get proper iTunes namespace. fix #2978 2014-03-21 18:52:11 +01:00
Ruud
1f647b3cc7 Autoload missing for notifications. fix #2980 2014-03-21 18:46:01 +01:00
Ruud
caf4eab104 Get correct size from HDBits api. fix #2997 2014-03-21 18:34:58 +01:00
Ruud
334078fc34 Allow password tag in returned release dict 2014-03-21 18:25:36 +01:00
Ruud
25b1d86c50 get identifier in awesomehd 2014-03-21 18:10:32 +01:00
Ruud
78e2ff4870 Cleanup 2014-03-21 18:01:52 +01:00
Ruud
ad94cce283 Make release files normal list 2014-03-21 17:35:26 +01:00
Ruud
b4610e5c23 Make sure to make release download id lowercase 2014-03-21 17:34:24 +01:00
Ruud
e12dcc2fb8 Also return releases on notify frontend 2014-03-21 16:37:35 +01:00
Ruud
a818276b6d Move multi-identifier search out of index 2014-03-21 16:32:31 +01:00
Ruud
269d779df7 Move for_media out of release index 2014-03-21 16:28:40 +01:00
Ruud
b63f7b7e5d Move with_status out of releases 2014-03-21 16:21:49 +01:00
Ruud
b4a3ac8081 Remove get_session 2014-03-21 16:20:25 +01:00
Ruud Burger
bbaaaa72fb Merge pull request #3000 from fuzeman/feature/dev_rtorrent
[rtorrent] Fixed connection bug when using SSL + Basic Auth
2014-03-21 14:50:50 +01:00
Ruud
89c83001ca Move status_get outside index 2014-03-21 14:49:44 +01:00
Dean Gardiner
61f1fdabd1 Removed print statement from rtorrent downloader 2014-03-22 02:46:05 +13:00
Dean Gardiner
28062eacb6 [rtorrent] cleaned up connection, '+https' is now added to 'httprpc' protocol if SSL option is enabled 2014-03-22 02:37:58 +13:00
Dean Gardiner
8bdbf8df2e Updated rtorrent-python library
- Fixed bug with basic auth on secure connections
 - Added 'test_connection' method to RTorrent class
 - Minor adjustment to authorization encoding
2014-03-22 02:37:57 +13:00
Ruud
27e4800ed2 try next release, use media_id 2014-03-21 14:31:49 +01:00
Ruud
37bc54e01e Add title to boxcar2 message
closes #2977
2014-03-21 13:32:36 +01:00
Ruud
6115f83a09 Update TPB proxies 2014-03-21 13:29:34 +01:00
Ruud
a8159c9e55 Make sure quality sizes are int on migrate 2014-03-21 13:23:52 +01:00
Ruud
f734e27d23 Make sure to safe size as int 2014-03-21 13:22:16 +01:00
Ruud
8a118df636 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-03-21 13:17:30 +01:00
Ruud Burger
a19b75760f Merge pull request #2987 from clinton-hall/dev-filesize
only compare filesize as int.
2014-03-21 13:17:20 +01:00
Ruud
1224b98745 Add releases to media.get.
re-use where possible
2014-03-21 13:15:35 +01:00
Ruud
6243ed3bd5 Add destination support to Synology downloader 2014-03-21 12:30:44 +01:00
Ruud
41e94e1e22 Make sure media dict has category key 2014-03-21 12:15:46 +01:00
Ruud
8c6940c351 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-03-21 11:06:41 +01:00
Ruud
384a2e0e15 Suggestions not showing 2014-03-21 11:06:25 +01:00
Joel Kåberg
a691841756 Merge pull request #2993 from fabcouwer/contribution_guide
Rewrite contributing.md
2014-03-20 22:55:49 +01:00
Friso Abcouwer
ff94bd6a90 Rewrite contributing.md 2014-03-20 20:17:13 +01:00
Ruud
00419910b4 Use getIdentifier in suggestions 2014-03-20 16:54:54 +01:00
Ruud
21c9d7fcc3 Use identifier helper 2014-03-20 16:53:27 +01:00
Ruud
e314c605f1 Missing identifier key 2014-03-19 23:28:12 +01:00
Ruud
8316b5cb29 Key identifiers missing 2014-03-19 23:24:16 +01:00
Ruud
be46ed12ac get identifier helper 2014-03-19 23:06:27 +01:00
Ruud
a2d22b6feb Set cleanup interval 2014-03-19 22:46:14 +01:00
Ruud
f4e373447e File not properly send to Sabnzbd 2014-03-19 22:37:44 +01:00
Ruud
b69f8b7ed5 Files not properly send to sabnzbd 2014-03-19 22:33:14 +01:00
Ruud
5b2dfffe0f Use correct year key 2014-03-19 22:09:27 +01:00
Ruud
b347f761a7 Ignore faulty category tables
They don't have any categories anyway, so might aswell ignore the error.
2014-03-19 18:13:55 +01:00
Ruud
445724573d Make sure movie is added with multi identifier 2014-03-19 18:02:45 +01:00
Ruud
8c5e0cf0a7 Make media index multi identifier based 2014-03-19 17:55:21 +01:00
Ruud
c6016a25df Destroy index and re-add on updated version 2014-03-19 09:22:51 +01:00
clinton-hall
5a0a5ad83b only compare filesize as int. 2014-03-19 15:45:20 +10:30
Ruud
3f0a0f552b Keep dict keys and only make array if all are ints in request params 2014-03-18 22:59:39 +01:00
Ruud
63fd35a95c Media helpers 2014-03-18 22:59:01 +01:00
jkaberg
db163e7bd1 remove debug stuff 2014-03-16 22:16:42 +01:00
jkaberg
f3cd569e77 fixed release_downloads, now properly returning data 2014-03-16 22:15:37 +01:00
jkaberg
a95671491d added missing vars to Torrent and File class 2014-03-16 22:13:59 +01:00
jkaberg
95295e47ab catch vars so we dont spam log 2014-03-16 21:50:11 +01:00
Ruud
a54e9ddd9c Merge branch 'refs/heads/nosql' into develop 2014-03-16 21:35:28 +01:00
Ruud
0e5f89d7d6 Use identifier to log already snatch quality 2014-03-16 21:33:49 +01:00
jkaberg
742d5cbfb3 qbittorrent downloader working 2014-03-16 21:33:45 +01:00
Ruud
e3fa695ad4 ThePirateBay don't overwrite search_url 2014-03-16 21:31:42 +01:00
Ruud
d6675f3311 Use correct super class 2014-03-16 21:18:31 +01:00
Ruud
64850a45da Merge branch 'refs/heads/nosql' into develop 2014-03-16 21:12:46 +01:00
Ruud
6aa0b7c748 Missing autoload 2014-03-16 21:04:23 +01:00
Ruud
fd80728857 Merge branch 'refs/heads/nosql' into develop
Conflicts:
	couchpotato/core/providers/torrent/yify/main.py
2014-03-16 20:59:31 +01:00
Ruud
9618a8d543 Position yeah in thumblist 2014-03-16 20:55:13 +01:00
Ruud
ec3a6e65ae Safer way to get reddit url 2014-03-16 18:27:58 +01:00
Ruud
d74578ec66 FreeBSD guide update 2014-03-16 17:54:57 +01:00
Ruud Burger
865dd24901 Merge pull request #2976 from MLWALK3R/develop
Proxy; Fixed a link with SSL on Yify
2014-03-16 16:54:35 +01:00
Ruud
e063028c7d Remove Yify ssl url 2014-03-16 16:54:21 +01:00
Ruud
db11c1b7a8 BinSearch: only check if there aren't enough parts 2014-03-16 16:53:15 +01:00
Michael
89d7a924fb Proxy; Fixed a link with SSL 2014-03-16 15:49:16 +00:00
jkaberg
fe16115b20 add qbittorrent client 2014-03-16 14:00:01 +01:00
Ruud
fbccba77a7 64Bit installer setup 2014-03-16 13:00:09 +01:00
jkaberg
07db34beb9 Merge remote-tracking branch 'origin/nosql' into nosql-qbitorrent 2014-03-16 12:38:12 +01:00
jkaberg
f42fb2fdd2 updated qbittorrent-python 2014-03-16 11:35:28 +01:00
Ruud
d3efda74b2 One up 2014-03-16 09:44:44 +01:00
Ruud
66b849cb29 Merge branch 'refs/heads/master' into desktop
Conflicts:
	version.py
2014-03-16 09:43:32 +01:00
Ruud
5853a373f3 Add library query 2014-03-16 09:20:53 +01:00
Ruud
951fbdccbd Don't load MovieBase twice 2014-03-16 00:46:56 +01:00
Ruud
c6d20eb91f Code cleanup 2014-03-16 00:06:39 +01:00
Ruud
b68cea3921 Getting release download didn't use correct key 2014-03-15 23:44:07 +01:00
Ruud
36125f1067 Make sure to use proper category id 2014-03-15 23:43:46 +01:00
Ruud
eee16c7a3d Newznab failed when doing manual download 2014-03-15 23:43:22 +01:00
Ruud
bf1d93f256 Test download connection failed 2014-03-15 23:42:58 +01:00
jkaberg
42e3c95f87 added qbittorrent-python 2014-03-15 17:05:26 +01:00
Ruud
ee702d92e6 Delete empty folders and leftover .pyc files on restart 2014-03-15 15:34:43 +01:00
Ruud
f5aae23111 Make sure log navigation doesn't overlay mask 2014-03-15 12:49:50 +01:00
Ruud
b19f98ef5b Merge branch 'refs/heads/develop' 2014-03-15 12:35:28 +01:00
Ruud
178f770b16 Use key to get release last_edit 2014-03-15 12:33:01 +01:00
Ruud
1b2d72531f Newznab url creation failed 2014-03-15 12:31:55 +01:00
Ruud
7e08454edd Import issues 2014-03-15 12:23:11 +01:00
Ruud
72d318323e Release ignore didn't get correct parameter 2014-03-15 12:17:53 +01:00
Ruud
b611a98bae Missing fireEvent import in torrentleech 2014-03-15 11:55:05 +01:00
Ruud
8b2eb50f29 Base classes for matcher and library 2014-03-15 11:51:45 +01:00
Ruud
988d0d6e35 Merge branch 'refs/heads/nosql2' into nosql 2014-03-15 10:17:45 +01:00
Ruud
48ec6fc757 Missing autoloads 2014-03-12 23:55:56 +01:00
Ruud
0921c5e160 Autoload suggestions 2014-03-12 23:44:24 +01:00
Ruud
83843ae210 Make sure in_library only contains relevant releases 2014-03-12 23:15:14 +01:00
Ruud
e5e768c56f Migrate hide profile 2014-03-12 22:46:12 +01:00
Ruud
f775c9da0b Only show log after successful load 2014-03-12 22:34:32 +01:00
Ruud
6a9c3dac77 Rename so it doesn't try to import itself 2014-03-12 22:30:10 +01:00
Ruud
11aaaecb7b Get colors back, remove logged string 2014-03-12 21:47:07 +01:00
Ruud
12c08154c5 Optimize imports 2014-03-12 21:41:29 +01:00
Ruud
79d6a6f85f Import fixes 2014-03-12 09:40:20 +01:00
Ruud
4513f03e8f Move movie to single file 2014-03-11 23:31:37 +01:00
Ruud
f3adfca9c5 Move media to single file 2014-03-11 23:01:42 +01:00
Ruud
0b61ec1e13 Move plugins to single file 2014-03-11 22:47:42 +01:00
Ruud
8492c9b214 Move notifications to single file 2014-03-11 22:31:47 +01:00
Ruud
2a60c52483 Move downloaders to single file 2014-03-11 22:28:56 +01:00
Ruud
917e813607 Move _base to single file 2014-03-11 22:15:58 +01:00
Ruud
c20f64685f Autoload from single file 2014-03-11 22:15:27 +01:00
Ruud
471229216a Provider restructure 2014-03-11 18:45:50 +01:00
Ruud
28661ab11a Move providers under media 2014-03-10 20:24:04 +01:00
Ruud
11c348a3d7 Merge branch 'refs/heads/develop' into nosql 2014-03-10 16:04:44 +01:00
Ruud
ffe6b7dd70 Add boxcar 2 support. closes #2886 2014-03-10 15:42:40 +01:00
Ruud
720af9085a Get new info when titles are missing 2014-03-10 14:30:16 +01:00
Ruud
8916ea5299 Don't unicode ints and floats 2014-03-10 14:12:19 +01:00
Ruud
bf9a43b3d1 Make sure name is string type, not bs4 class 2014-03-10 14:10:29 +01:00
Ruud
bca597c4e2 Speed up log view in Chrome 2014-03-10 00:23:41 +01:00
Ruud
33e2f63ed5 Don't try to use release info if it doesn't exist 2014-03-10 00:00:33 +01:00
Ruud
e3f6df7120 Convert sp to unicode 2014-03-09 23:47:33 +01:00
Ruud
58f198ddad Zero fill identifier when adding movie 2014-03-09 23:39:46 +01:00
Ruud
61edcfe4f3 Remove debug comment 2014-03-09 15:30:21 +01:00
Ruud
85bc3ddde6 Don't try to reuse generator 2014-03-09 15:29:53 +01:00
Ruud
488b631c38 Migrate release files 2014-03-09 14:32:41 +01:00
Ruud
da1b430200 Only update media when needed 2014-03-09 14:30:43 +01:00
Ruud
bef76f0118 Add download info to release when available 2014-03-09 14:30:23 +01:00
Ruud
dd6baa72fa Faster search title index 2014-03-09 14:29:54 +01:00
Ruud
519b832d8c Time migration 2014-03-09 11:52:15 +01:00
Ruud
131326675e Update release on rename
Prevent RevConflict
2014-03-09 00:24:54 +01:00
Ruud
73dfa232f9 Allow "or" for media and release status in movie.list 2014-03-08 23:14:16 +01:00
Ruud
3f64173905 Don't trigger toasts on init 2014-03-08 22:44:28 +01:00
Ruud
5cc4260d8e Remove unused method 2014-03-08 22:30:17 +01:00
Ruud
7f33a3847c Try existing images first 2014-03-08 22:16:19 +01:00
Ruud
c9be74ce80 Use correct keys when renaming media 2014-03-08 22:15:07 +01:00
Ruud
f5d29eafe0 Extend title helper 2014-03-08 22:14:54 +01:00
Ruud
274e2c1cc2 Use proper download_info dict 2014-03-08 20:22:35 +01:00
Ruud
c2233f7474 Merge branch 'refs/heads/develop' into nosql 2014-03-08 18:55:55 +01:00
Ruud
75f22f44a1 Reference before assigned 2014-03-08 18:55:38 +01:00
Ruud
d60a8a71b7 Check if file has moved, ignore copystat errors. close #2936 2014-03-08 18:18:06 +01:00
Ruud
f8bfd6fd3f Coming soon thumbnail height fix 2014-03-08 16:50:53 +01:00
Ruud
f2bc735bc0 Merge branch 'refs/heads/develop' into nosql
Conflicts:
	couchpotato/core/plugins/dashboard/main.py
2014-03-08 14:07:34 +01:00
Ruud
9e471ac389 Add "I Just Watched" Reddit to userscripts. fix #2621 2014-03-08 14:05:49 +01:00
Ruud
ca34cbd180 Check for year in coming soon 2014-03-08 12:34:42 +01:00
Ruud
9f4ea662da Use natural sorting
Conflicts:
	couchpotato/core/helpers/request.py
2014-03-08 12:30:47 +01:00
Ruud
3172a4d030 Check for year in coming soon 2014-03-08 12:27:06 +01:00
Ruud
c58315e2ee Use natural sorting 2014-03-08 11:59:12 +01:00
Ruud
dc0ea5b3f6 Use proper sorting 2014-03-08 11:52:59 +01:00
Ruud
b50cf1cf4c Only allow next year for couldbereleased check 2014-03-08 10:50:11 +01:00
Ruud
a7a4499dd4 Thumbnail check 2014-03-08 10:31:59 +01:00
Ruud
f78261ee32 Merge branch 'refs/heads/develop' into nosql 2014-03-08 10:02:42 +01:00
Ruud
b69898d624 Remove double self in filetime check. fixes #2952 2014-03-08 09:37:39 +01:00
Ruud
cfa8702654 Close sqlite connection 2014-03-07 20:42:08 +01:00
Ruud
dd9c65db4c Migrate properties 2014-03-07 20:19:52 +01:00
Ruud
9e7e29f03f Merge branch 'refs/heads/develop' into nosql 2014-03-07 19:14:02 +01:00
Ruud
2066625bf0 Don't use ctime on unix system. Cleanup check a bit. close #2904 2014-03-07 18:58:27 +01:00
Ruud
7af1d00ea2 Allow passwords inside nzb name 2014-03-07 18:10:17 +01:00
Ruud
5b279a48cb Make sure q is first for nzbclub 2014-03-07 17:38:40 +01:00
Ruud
3d6e84e11c Migrate logging 2014-03-07 16:05:52 +01:00
Ruud
527d6ab7ff Properly offset media listing 2014-03-07 15:44:09 +01:00
Ruud
8a295c72ba Only add single image file 2014-03-07 15:43:05 +01:00
Ruud
d2b82a37b2 Only migrate file if it is found 2014-03-07 15:42:06 +01:00
Ruud
ec4e680d62 Always check and remove files on movie info 2014-03-07 15:41:08 +01:00
Ruud
aa80ed3d4b Don't log big object 2014-03-07 15:33:12 +01:00
Ruud
1d4a7894e8 Fix broken files on refresh 2014-03-07 12:39:58 +01:00
Ruud
2d28cb6897 Backup databases 2014-03-07 11:18:14 +01:00
Ruud
89e561c991 Use time to index notifications 2014-03-06 22:39:11 +01:00
Ruud
d16dd7c75d Use proper sorting 2014-03-06 21:30:59 +01:00
Ruud
e51e6b2171 Use correct release status
Remove continue from category migrate
2014-03-05 16:43:35 +01:00
Ruud
3eaf5c9bc0 Use status from release when available 2014-03-05 16:27:55 +01:00
Ruud
4625c920c3 Stop notify frontend triggering for media add 2014-03-05 16:27:27 +01:00
Ruud
2ce0f7beb4 Migrate update 2014-03-05 16:27:08 +01:00
Ruud
81ba95f540 Migrate library file info 2014-03-05 15:41:16 +01:00
Ruud
b25e0ea393 Remove file_type references 2014-03-05 15:24:33 +01:00
Ruud
2632b34438 Use proper default id 2014-03-05 15:24:19 +01:00
Ruud
ff60013335 Only use filename for cache api call 2014-03-05 15:24:08 +01:00
Ruud
a0bcb03dde Index fixes 2014-03-05 15:12:00 +01:00
Ruud
f0044a9342 Use correct index for movie_type 2014-03-05 11:43:50 +01:00
Ruud
04fb81e071 More import 2014-03-04 22:42:03 +01:00
Ruud
642b665418 Merge branch 'refs/heads/develop' into nosql 2014-03-04 20:34:32 +01:00
Ruud
a5fa0681ed Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-03-03 23:35:06 +01:00
Ruud
22e922e860 Split socket error to nr 2014-03-03 23:34:49 +01:00
Ruud Burger
0126f5ae84 Merge pull request #2921 from MLWALK3R/patch-1
replaced a duplicate URL on TPB
2014-03-03 23:24:45 +01:00
Ruud
c389790cf2 Merge branch 'refs/heads/develop' 2014-03-03 22:19:29 +01:00
Ruud
cfb246fa84 Make sure imdb rating exists before using it 2014-03-03 22:11:59 +01:00
Michael Walker
651119b7dd replaced a duplicate URL
replaced a duplicate URL.
2014-02-28 17:35:08 +00:00
Ruud
d7445dfa80 Merge branch 'refs/heads/develop' 2014-02-26 14:00:56 +01:00
Ruud Burger
f944a70a9c Merge pull request #2911 from fuzeman/feature/dev_rtorrent
[rtorrent] Fixed naming issue
2014-02-26 13:46:52 +01:00
Dean Gardiner
9056f5ae59 Fixed naming issue in rtorrent downloader 2014-02-26 14:52:10 +13:00
Ruud
ed62c981cc Add quality tests 2014-02-25 22:04:43 +01:00
Ruud
36782768a4 Merge branch 'refs/heads/develop' 2014-02-25 21:37:29 +01:00
Ruud Burger
2a7ba28903 Merge pull request #2902 from MLWALK3R/develop
SSL'd and Updated
2014-02-25 21:35:45 +01:00
Ruud Burger
e8ec2ef8d1 Merge pull request #2906 from tehspede/develop
Search url has method defined twice (2 and 3) we only want 3.
2014-02-25 21:31:29 +01:00
Ruud
2c9d487614 Update build url 2014-02-25 21:20:59 +01:00
tehspede
864e8654c3 Search url has method defined twice (2 and 3) we only want 3. 2014-02-25 18:29:51 +02:00
Michael
e11453aafb SSL'd and Updated
Add SSL to some URL's and update the Apple RSS link.
2014-02-24 21:59:45 +00:00
Ruud
c1596f098c Merge branch 'refs/heads/develop' into nosql 2014-02-24 22:54:04 +01:00
Ruud
e57620f67c Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-02-24 22:49:09 +01:00
Ruud
e481763967 Merge branch 'refs/heads/mikke89-downloaders_test' into develop 2014-02-24 22:48:19 +01:00
Ruud
2b3d755c64 Cleanup downloader testbuttons PR 2014-02-24 22:48:12 +01:00
Ruud
fc2db36820 Merge branch 'downloaders_test' of git://github.com/mikke89/CouchPotatoServer into mikke89-downloaders_test
Conflicts:
	couchpotato/core/downloaders/rtorrent/main.py
2014-02-24 22:07:49 +01:00
Ruud Burger
4fdea782f3 Merge pull request #2901 from MLWALK3R/patch-5
Torrentshack description https
2014-02-24 21:57:03 +01:00
Michael Walker
188a1a3b03 HTTP to HTTPS
Updated URL to SSL, better account security.
2014-02-24 20:56:01 +00:00
Ruud Burger
8e2014f2d4 Merge pull request #2899 from MLWALK3R/patch-4
ILoveTorrents use SSL
2014-02-24 21:48:36 +01:00
Michael Walker
fb95d7923f HTTP to HTTPS
Updated URL's to SSL, better account security.
2014-02-24 20:46:47 +00:00
Ruud Burger
fe5ca69f36 Merge pull request #2897 from MLWALK3R/patch-2
Replaced proxies for TPB
2014-02-24 21:41:31 +01:00
Ruud Burger
1086b808dc Merge pull request #2898 from MLWALK3R/patch-3
Changed http to https
2014-02-24 21:32:32 +01:00
Michael Walker
0050e5cdfc Changed http to https
adjusted http to SSL, better security when dealing with logins.
2014-02-24 18:23:18 +00:00
Michael Walker
6b357674d0 Replaced proxies
Remove dead/blocked proxies, Added in new unblocked/working links
2014-02-24 18:18:39 +00:00
Ruud
8f44dfcde5 Merge branch 'refs/heads/develop' into nosql
Conflicts:
	couchpotato/core/providers/torrent/sceneaccess/main.py
2014-02-24 18:49:07 +01:00
Ruud Burger
82c0592e49 Merge pull request #2875 from fuzeman/feature/dev_rtorrent
[rtorrent] Fixed how torrent status is determined
2014-02-24 18:48:01 +01:00
Ruud
28ab4576d5 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-02-24 18:47:08 +01:00
Ruud Burger
2debd5598f Merge pull request #2888 from xombiemp/ptp-golden
PTP Golden release fix
2014-02-24 18:46:53 +01:00
Ruud
d86d44e2d4 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-02-24 18:46:24 +01:00
Ruud Burger
3d85460dc8 Merge pull request #2887 from koppelbakje/develop
[SceneAccess] Change search method to 3 (description)
2014-02-24 18:46:15 +01:00
Ruud
52ce85fbf2 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-02-24 18:45:27 +01:00
Ruud
6d70533e0b Yifi proxy changes 2014-02-24 18:45:19 +01:00
Ruud Burger
4d8338e829 Merge pull request #2878 from fuzeman/feature/rtorrent/httprpc
[rtorrent] HTTP-RPC support
2014-02-24 18:44:12 +01:00
Andrew Parker
a4e48e1f6b I've found that the score applied for Golden torrents is not enough to snatch them reliably. When I set the Prefer Golden setting, I expect it to always choose the Golden release over a Scene release. Here's an excerpt from my log that illustrates this setting failing to grab the Golden release over a Scene release:
02-22 13:56:17 INFO [core.media.movie.searcher] Search for Thor: The Dark World in 720P
02-22 13:56:21 INFO [otato.core.providers.base] Found correct release with weight 1.00, old_score(4581) now scaled to score(4581)
02-22 13:56:21 INFO [otato.core.providers.base] Found: score(4581) on PassThePopcorn: Thor The Dark World (2013) - 720p Blu-ray x264 Scene (720p)
02-22 13:56:21 INFO [otato.core.providers.base] Found correct release with weight 1.00, old_score(1257) now scaled to score(1257)
02-22 13:56:21 INFO [otato.core.providers.base] Found: score(1257) on PassThePopcorn: Thor The Dark World (2013) - 720p Blu-ray x264 HQ With Commentary (720p)
02-22 13:56:21 INFO [core.media._base.searcher] Wrong: Required word missing: thor the dark world 2013 720p web h 264 extras 720p
02-22 13:56:24 INFO [tato.core.plugins.release] Snatched "Thor The Dark World (2013) - 720p Blu-ray x264 Scene (720p)": Thor: The Dark World (2013) in 720P

With this modification it will fix this specific example and hopefully all others.
2014-02-22 15:35:08 -07:00
Leon Koppel
790a74f9e4 Change search method to 3 (description) 2014-02-22 18:43:19 +01:00
mikke89
893dde9958 rTorrent connection test: Error message on version check fail 2014-02-21 20:28:49 +01:00
Dean Gardiner
d448b8cd99 Adjusted rtorrent connect method to work with httprpc URIs, adjusted option descriptions 2014-02-21 15:48:17 +13:00
Dean Gardiner
ca2c4a0b3e Updated rtorrent-python library (HTTP-RPC support)
- Added URI transforming to cleanly support HTTP-RPC
2014-02-21 15:27:35 +13:00
mikke89
499b8193ab Added return message text to frontend 2014-02-21 02:26:04 +01:00
mikke89
1f18d2b09c Test downloader connection: Check version of uTorrent and Sabnzbd 2014-02-21 02:09:16 +01:00
Dean Gardiner
a92d6fd35c Fixed how the status is determined in the rtorrent downloader 2014-02-21 01:13:12 +13:00
Ruud
12adde8f80 Use new id for pushbullet. fix #2864 2014-02-17 20:41:03 +01:00
Ruud
6437079be3 Merge branch 'refs/heads/develop' into nosql
Conflicts:
	couchpotato/core/media/__init__.py
	couchpotato/core/notifications/trakt/main.py
2014-02-16 15:56:18 +01:00
Ruud
8b747dff9b Use correct var name in nzbvortex 2014-02-16 15:48:55 +01:00
Ruud
027ff43dfd Path encode files in rename. fix #2846 2014-02-16 14:55:35 +01:00
Ruud
f50c8504cf Encode before copy metadata. fix #2832 2014-02-16 14:19:15 +01:00
Ruud
30f5a3944c Use test url for trakt notification test. fix #2798 2014-02-16 14:11:21 +01:00
Ruud
a1c0b000a4 Update TMDB api 2014-02-16 10:48:48 +01:00
Ruud
f22778aacb Use proper check 2014-02-16 10:40:54 +01:00
Ruud
888ee07f65 Check responsecodes 2014-02-16 10:27:31 +01:00
Ruud Burger
aa5937c278 Merge pull request #2824 from fuzeman/feature/dev_rtorrent
[rtorrent] Fixed bug where setting changes would not take effect
2014-02-16 10:13:02 +01:00
Ruud
4831c80598 Update nzbclub url 2014-02-16 09:59:37 +01:00
Ruud
b9a724c8bb Merge branch 'refs/heads/develop' 2014-02-16 09:43:03 +01:00
Ruud
886a271d19 Use correct ordering for request arrays. fix #2810 2014-02-16 09:42:47 +01:00
Ruud
68d826ca1c Merge branch 'refs/heads/develop' 2014-02-15 19:48:07 +01:00
Ruud
8dfb0d1d5c Fire events after tab add 2014-02-15 19:47:55 +01:00
Ruud
d6921882e1 Merge branch 'refs/heads/develop' 2014-02-14 19:39:47 +01:00
Ruud
1f982b7999 Migration start 2014-02-14 19:36:23 +01:00
Ruud
eb1556f3e8 Add filter on database manage page 2014-02-12 21:14:06 +01:00
Ruud
061b79eac0 Title 2014-02-12 08:25:17 +01:00
Ruud
3bbeec513a Events 2014-02-12 08:24:15 +01:00
Ruud
a6be59bbea Delete documents 2014-02-12 08:16:10 +01:00
Ruud
96e8a909d8 database manage init 2014-02-11 22:19:55 +01:00
Ruud
8724076601 Remove sqlalchemy and elixir 2014-02-10 23:41:54 +01:00
Ruud
4b356aba3e Nosql 2014-02-10 23:22:11 +01:00
Ruud
a13e0a75e8 Remove test 2014-02-10 23:08:14 +01:00
Ruud
ecf91d616b nosql 2014-02-10 23:07:42 +01:00
Ruud
0d4d0f3126 scandir 2014-02-09 18:29:17 +01:00
Ruud
b9a8ca14c3 nosql 2014-02-09 18:21:47 +01:00
Ruud
f7e1a2a5eb nosql 2014-02-09 15:57:08 +01:00
Ruud
c3bc9c8591 Nosql 2014-02-08 17:58:45 +01:00
Dean Gardiner
3380e20e3a Cleaned up naming of functions in rtorrent downloader 2014-02-08 03:25:11 +13:00
Dean Gardiner
a2c87e1b7d Fixed bug where changes to rtorrent settings wouldn't take effect until a restart 2014-02-08 03:22:59 +13:00
Ruud
a609b401c4 nosql 2014-02-07 15:11:06 +01:00
Ruud Burger
9098e44513 Merge pull request #2823 from ramon86/develop
Category changes for Torrent provider TorrentBytes
2014-02-07 12:26:26 +01:00
Ramon van Dam
62524e01e1 * Added category 'bd50' (BR-Disk) to Torrent provider TorrentBytes
* Changed category identifier for category 'brrip' for Torrent provider TorrentBytes (see issue #2795)
2014-02-07 12:08:17 +01:00
Ruud Burger
78bf1d274e Merge pull request #2817 from fuzeman/feature/dev_rtorrent
[rtorrent] Fixed bug which caused large torrents to fail
2014-02-06 14:04:46 +01:00
Dean Gardiner
461e469f28 Updated rtorrent-python library
- Fixed bencode encoding bug with long types
2014-02-07 01:40:11 +13:00
Ruud
99252074be More nosql 2014-02-02 20:41:14 +01:00
Ruud Burger
e4e7ae3621 Merge pull request #2775 from ressu/fix_rtorrent_connection
Fix rTorrent connectivity
2014-01-31 13:31:32 -08:00
Ruud
63743dd2b6 More NoSQL 2014-01-31 00:38:37 +01:00
Ruud
a254886bad Try NoSQL 2014-01-29 17:49:54 +01:00
Ruud
aab10fb599 Close all 2014-01-28 08:22:06 +01:00
Ruud
9d55ecffe9 Add log var 2014-01-27 21:58:48 +01:00
Ruud
00b613d2e0 Scoped session 2014-01-27 21:56:47 +01:00
Ruud
fe24322f7c Add kwargs to to_dict 2014-01-27 21:56:27 +01:00
mikke89
660e20dada Merge branch 'downloaders_test' into downloaders_test_dev
Conflicts:
	couchpotato/core/downloaders/transmission/main.py
2014-01-26 18:37:18 +01:00
mikke89
18c8e803a4 Fixed 'connection test' for Transmission and Sabnzbd 2014-01-26 18:34:42 +01:00
Sami Haahtinen
15a19949b8 Fix rTorrent connectivity
The combination of cleanHost and rTorrent.connect issues caused rTorrent
connections to fail. This update fixes cleanHost() so that it can
actually cope with SSL based hosts and finishes the migration to
cleanHost() in connect()

Conflicts:
	couchpotato/core/helpers/variable.py
2014-01-26 19:26:15 +02:00
mikke89
ebc5a66375 Fixed 'connection test' for Transmission and Sabnzbd 2014-01-26 18:17:23 +01:00
Ruud
1120b4ab51 Remove Elixir library
Update SQLAlchemy
2014-01-26 16:29:16 +01:00
Ruud
f91081e39c uTorrent hostname hint 2014-01-26 10:52:50 +01:00
Ruud
9e991e1595 Fix Yify proxy check 2014-01-26 10:40:15 +01:00
Ruud
afac06081c Defer settings dom injection 2014-01-26 10:05:05 +01:00
Ruud
b773228719 Merge branch 'refs/heads/commit_rollback' into develop 2014-01-26 09:18:52 +01:00
Ruud
7001ed476d Wrap all commits with try/except 2014-01-26 00:33:21 +01:00
Ruud
31c39650a9 Force default title when none match 2014-01-25 15:26:35 +01:00
Ruud
fbae706b0f Use correct var to shuffle 2014-01-25 15:26:00 +01:00
Ruud
88c328af8e Improved manage scanning
Expire after db get
2014-01-24 22:33:22 +01:00
Ruud
cbd8981ee2 Use helper 2014-01-24 16:33:10 +01:00
Ruud
3101926e9b removeDuplicate helper 2014-01-24 15:42:29 +01:00
Ruud
c9e0910c55 Can't use len() on filter iterator. fix #2762 2014-01-24 15:29:24 +01:00
Ruud
d65667ce16 Don't force add basic auth to url 2014-01-24 14:50:54 +01:00
Ruud
7d7251862c Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-01-23 22:00:40 +01:00
Ruud
4d02a969c2 Merge branch 'refs/heads/georgewhewell-hdbits-api' into develop 2014-01-23 22:00:17 +01:00
Ruud
e20c776364 Use urlopen for HD Bits requests 2014-01-23 22:00:12 +01:00
Ruud
c55404699e Merge branch 'hdbits-api' of git://github.com/georgewhewell/CouchPotatoServer into georgewhewell-hdbits-api 2014-01-23 21:50:31 +01:00
Ruud Burger
6240e4eba0 Merge pull request #2756 from fuzeman/feature/dev_rtorrent
Increased rTorrent load_torrent max waiting time
2014-01-23 12:48:41 -08:00
Ruud
cf86719607 Encode before logging 2014-01-23 00:08:38 +01:00
Ruud
76943b6529 Make sure imdb list_id regex matches whole string.
Thanks @basrieter
2014-01-23 00:00:59 +01:00
Ruud
ca8bbdc293 Allow longer imdb user_id parse 2014-01-22 23:59:25 +01:00
Ruud
8e6f12a897 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-01-22 23:39:13 +01:00
Ruud
52c64c1a6a Get full imdb (watch)list without login. fix #2715 2014-01-22 23:38:18 +01:00
Ruud
ca94d48f8b No need to try and cach htmldata 2014-01-22 23:31:45 +01:00
Dean Gardiner
d860680823 Increased rTorrent load_torrent max waiting time to 10 retries/seconds 2014-01-22 22:27:19 +13:00
Dean Gardiner
d1dbf3745a Updated rtorrent-python library 2014-01-22 22:24:22 +13:00
Ruud Burger
4b1151bda1 Put future import after shebang 2014-01-22 08:48:22 +01:00
Ruud
18c64e493b Don't cache post requests 2014-01-21 23:06:02 +01:00
Ruud
fc6839b441 Force remove duplicate in suggested movies 2014-01-21 22:38:36 +01:00
Ruud
405b63acdd Remove unused CP automation provider 2014-01-21 21:46:03 +01:00
Ruud
f3dee50448 Properly handle and trigger events 2014-01-21 21:29:54 +01:00
Ruud
04e550ebe7 Merge branch 'refs/heads/ressu-fix_log_lines' into develop 2014-01-21 20:29:11 +01:00
Ruud
05b58819d6 Merge branch 'fix_log_lines' of git://github.com/ressu/CouchPotatoServer into ressu-fix_log_lines 2014-01-21 20:27:32 +01:00
georgewhewell
63c72853f4 Change HDBits provider to use API instead of scraping site 2014-01-21 12:07:38 +00:00
mikke89
f20cce0176 Small fix 2014-01-21 01:38:37 +01:00
mikke89
723cbcd8bd Added 'test connection' button for downloaders 2014-01-21 01:30:13 +01:00
mikke89
dfbb84caae Small fix deluge 2014-01-21 01:12:34 +01:00
mikke89
009d6cafaf Added connection test to the rest of downloaders 2014-01-21 00:24:36 +01:00
Ruud
bd9a4289d1 Rename importlib 2014-01-21 00:19:26 +01:00
Ruud
29a34fef8c py3k port helpers 2014-01-20 23:58:54 +01:00
Ruud
08e2a3a883 Import print function 2014-01-20 23:28:48 +01:00
Ruud
2d37022525 Relative import 2014-01-20 23:28:35 +01:00
Ruud
bb3faaf2cd Exception cleanup 2014-01-20 23:27:58 +01:00
Ruud
2c43b9a926 Update six 2014-01-20 23:23:40 +01:00
mikke89
964ed5f497 Added test connection button for uTorrent 2014-01-20 22:09:03 +01:00
Ruud
b47a94852a Update library: tornado 2014-01-20 16:55:58 +01:00
Ruud
f318524070 Update library: html5lib 2014-01-20 16:50:21 +01:00
Ruud
04539edb45 Update library: APScheduler 2014-01-20 16:47:49 +01:00
Ruud
5cf21452c1 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-01-19 19:18:39 +01:00
Ruud
799299c7cc Code cleanup 2014-01-19 19:15:58 +01:00
Ruud Burger
458330d325 Merge pull request #2743 from fuzeman/feature/dev_rtorrent
Fixed bug in rTorrent downloader when file paths start with '/'
2014-01-19 03:03:31 -08:00
Ruud Burger
973bec9e6a Merge pull request #2741 from fuzeman/develop_iptorrents
Fixed IPTorrents provider searching (again)
2014-01-19 03:01:54 -08:00
Dean Gardiner
1f941a5105 Ensure files returned from rTorrent are absolute and inside the torrent directory. 2014-01-19 23:05:05 +13:00
Sami Haahtinen
8217fecb33 Make Log messages pasteable 2014-01-19 12:01:35 +02:00
Dean Gardiner
1dda7edf1c Fixed bug when parsing torrents page in IPT provider 2014-01-19 22:45:37 +13:00
Ruud
2cfff73486 Merge branch 'refs/heads/develop' 2014-01-18 19:54:32 +01:00
Ruud Burger
3c03e400f0 Merge pull request #2732 from mano3m/develop_fixhost
Store username and pass in cleanhost
2014-01-18 10:52:02 -08:00
mano3m
6388d97c5c Store username and pass in cleanhost
Fixes #2727
2014-01-18 12:39:59 +01:00
Ruud
0c7dda8d44 Merge branch 'refs/heads/develop' 2014-01-17 23:17:41 +01:00
Ruud
161e3086fa Force year as int on tmdb info. fix #2725 2014-01-17 23:00:15 +01:00
Ruud
b3f1f938be Speedup automation getinfo 2014-01-17 22:38:38 +01:00
Ruud
082da6e3a6 Don't return .text in urlopen 2014-01-17 22:38:02 +01:00
Ruud
d9b9447242 Change cachekey if info not extended 2014-01-17 22:37:01 +01:00
Ruud
dbaa377770 version.master 2014-01-17 16:29:29 +01:00
Ruud
47d2b81d1c Merge branch 'refs/heads/develop' 2014-01-17 16:28:59 +01:00
Ruud
d743282578 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-01-16 11:48:59 +01:00
Ruud
7eee6f0b96 Set proper branch in version file 2014-01-16 11:48:49 +01:00
Ruud Burger
dda3fca4b4 Merge pull request #2718 from techmunk/deluge_improvements
Deluge: Ignore empty torrent results, select only what is needed
2014-01-15 23:54:39 -08:00
Techmunk
8648b2f948 Only request needed properties from deluge, and fix error when CP asks for torrent hash that is not in deluge. i.e. missing. 2014-01-16 17:13:30 +10:00
Ruud
f52cbd24f8 Remove debug variable 2014-01-15 22:30:00 +01:00
Ruud
5ea13eeffd Catch xbmc turned off error 2014-01-15 21:51:27 +01:00
Ruud
6cc802952f Catch maxretry error
Don't fill logs with duplicate logs
2014-01-15 21:38:00 +01:00
Ruud
190b9db645 Merge branch 'refs/heads/mano3m-develop_cleanhost' into develop 2014-01-15 21:10:14 +01:00
Ruud
81949b9cad Remove prints and actually save deletion 2014-01-15 21:10:06 +01:00
Ruud
894e419f40 Allow config delete 2014-01-15 21:08:19 +01:00
Ruud
cdc6c036aa Merge branch 'develop_cleanhost' of git://github.com/mano3m/CouchPotatoServer into mano3m-develop_cleanhost 2014-01-15 19:31:45 +01:00
Ruud Burger
1e9168f682 Merge pull request #2712 from fuzeman/develop_fix_blackhole
Fixed encoding bug with blackhole downloader
2014-01-15 10:29:53 -08:00
Ruud
790415dd4f Log version at start. fix #2708 2014-01-15 14:25:12 +01:00
Ruud
679e0ea2c3 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-01-15 12:16:38 +01:00
Ruud Burger
bd167403c3 Merge pull request #2711 from fuzeman/develop_iptorrents
Fixed IPTorrents provider searching
2014-01-15 01:14:03 -08:00
Dean Gardiner
13abe62bed Fixed encoding bug that caused the blackhole downloader to fail 2014-01-15 22:01:35 +13:00
Dean Gardiner
4147c5b870 Fixed issue retrieving seeders and leechers which caused searching to fail on IPT 2014-01-15 20:52:27 +13:00
Ruud
37d4755aae Log when there is an actual problem with the filedata download. fix #2705 2014-01-14 15:59:51 +01:00
Ruud
a9f416c4c5 Variable cleanup 2014-01-14 12:06:47 +01:00
Ruud
8a11f246b1 Add group to untag release 2014-01-14 09:31:29 +01:00
Ruud
8d44577dca Update movie info getter with better exception handling 2014-01-13 23:43:10 +01:00
mano3m
72457d8d10 Log with system encoding 2014-01-13 23:15:10 +01:00
mano3m
3bb44f8d9f Migrate rTorrent options 2014-01-13 23:14:18 +01:00
Ruud
279297b8fa Log as debug for file overwrite 2014-01-13 22:30:41 +01:00
Ruud
c71e661daf Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-01-13 21:54:08 +01:00
Ruud
f8820c06fe Normcase in folder compare 2014-01-13 21:53:38 +01:00
Ruud
907b40e3c6 Higher z-index for userscript popup. fix #2703 2014-01-13 16:27:08 +01:00
Ruud
d318e163bb Custom tag was never defined 2014-01-12 21:28:49 +01:00
Ruud
6e9c36a503 Lowercase compare 2014-01-12 20:31:59 +01:00
Ruud
c9e9fe86aa Don't normcase in sp function 2014-01-12 20:25:45 +01:00
Ruud
c4f4e2b524 Split identifier by know tag if possible 2014-01-12 17:42:10 +01:00
Ruud
95246b90f6 Merge branch 'refs/heads/mano3m-develop_newznab' into develop 2014-01-12 17:10:52 +01:00
Ruud
2fad29df51 Style custom tag input
Add description to abr
2014-01-12 17:10:30 +01:00
Ruud
a95320e162 Merge branch 'develop_newznab' of git://github.com/mano3m/CouchPotatoServer into mano3m-develop_newznab 2014-01-12 15:18:41 +01:00
Ruud
31b8805b5e Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-01-12 15:18:34 +01:00
Ruud
9e69d4e153 Queue multiple media refresh 2014-01-12 15:18:13 +01:00
Ruud Burger
aa5ecd7b42 Merge pull request #2687 from mano3m/develop_log_metadata
Log overwriting of metadata files
2014-01-12 00:21:47 -08:00
Ruud
15f90aa503 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-01-12 09:17:40 +01:00
Ruud Burger
ec86bc4a38 Merge pull request #2688 from mano3m/develop_inctran
Add incomplete folder support to Transmission
2014-01-12 00:16:16 -08:00
Ruud Burger
a3efc64901 Merge pull request #2690 from mano3m/develop_renamer
Abort rename when something fails
2014-01-12 00:13:57 -08:00
Ruud Burger
c929ecbac0 Merge pull request #2691 from mano3m/develop_bluray
Fix Bluray.com encoding issue
2014-01-12 00:12:22 -08:00
mano3m
cc32e49060 Fix Bluray.com encoding issue 2014-01-12 01:07:35 +01:00
mano3m
05c41460c2 Downloader cleanHost
Extend the use of clean host (add more checks and features) and make the settings more dummy proof.
2014-01-12 00:52:32 +01:00
mano3m
794efaa209 Abort rename when something fails
And tag the folder with failed_rename so that the release with not be
deleted later on.
2014-01-12 00:41:54 +01:00
mano3m
b0e93ee18c Add custom_tag field to newznab 2014-01-12 00:34:49 +01:00
mano3m
0393b51db6 Add logging 2014-01-11 23:59:16 +01:00
mano3m
464c8ad71c Log overwriting of metadata files
Gives more info for cases like #2641
2014-01-11 23:36:23 +01:00
mano3m
9df0e01874 Add incomplete folder support to Transmission 2014-01-11 23:33:23 +01:00
Ruud
bf2beb2530 Don't fire async event inside an already async event 2014-01-11 20:46:01 +01:00
Ruud
f0b096d41a Don't show empty title on re-add 2014-01-11 20:45:37 +01:00
Ruud
c948f38469 Only add trailer to known quality list. fix #2684 2014-01-11 14:23:35 +01:00
Joel Kåberg
190e1d2c4f Revert "Merge pull request #2596 from WoLpH/linked_file_delete"
This reverts commit a24d4a9e3b, reversing
changes made to b468048d95.
2013-12-19 22:09:35 +01:00
Joel Kåberg
8a822e35e2 Revert "Merge pull request #2560 from coolius/master"
This reverts commit 64a196f21d, reversing
changes made to a24d4a9e3b.
2013-12-19 22:08:54 +01:00
Joel Kåberg
64a196f21d Merge pull request #2560 from coolius/master
Updated YIFY provider to use proxies and magnet links
2013-12-19 12:58:29 -08:00
Joel Kåberg
a24d4a9e3b Merge pull request #2596 from WoLpH/linked_file_delete
Added delete files button
2013-12-19 12:56:45 -08:00
coolius
138a3b1f3c Replaced default YIFY URL with official alternate domain "yify-torrents.im" 2013-12-16 09:21:59 +00:00
WoLpH
9a55961786 Added delete files button 2013-12-12 02:27:09 +01:00
coolius
4eaddadf8c Removed unusable proxy 2013-12-04 13:50:06 +00:00
coolius
9dd98b29be Added proxy options to YIFY provider 2013-12-03 10:32:10 +00:00
coolius
732946d38a Updated YIFY provider to use proxy list 2013-12-03 10:08:14 +00:00
coolius
ca070e67e7 Updated YIFY provider to use proxy and magnet links 2013-12-02 10:53:47 +00:00
Joel Kåberg
b468048d95 directory properly removed 2013-12-01 21:37:07 +01:00
Ruud
029ae20573 Use Object.each for object looping 2013-11-30 11:55:22 +01:00
Ruud
fdcddaaffc Merge branch 'refs/heads/develop' 2013-11-30 11:27:41 +01:00
Ruud
b4275639f5 Merge branch 'refs/heads/develop' 2013-11-19 09:17:24 +01:00
Ruud
f79fcda27f Small one up 2013-11-17 21:22:24 +01:00
Ruud
cdbcad2238 Merge branch 'refs/heads/develop' into desktop 2013-11-17 21:20:30 +01:00
Ruud
d6709469f6 Merge branch 'refs/heads/develop' 2013-11-17 21:20:13 +01:00
Ruud
5d913e87c3 One up! 2013-11-17 20:20:18 +01:00
Ruud
16f02bda27 Merge branch 'refs/heads/develop' into desktop 2013-11-17 20:03:22 +01:00
Ruud
3e43e3fc4c Merge branch 'refs/heads/develop' 2013-11-17 20:02:20 +01:00
Ruud
e622e68701 Merge branch 'refs/heads/develop' 2013-11-17 00:07:01 +01:00
Ruud
a90a4d1bc2 Merge branch 'refs/heads/develop' 2013-11-16 17:24:09 +01:00
Ruud
165676407a Merge branch 'refs/heads/develop' 2013-11-16 14:39:58 +01:00
Ruud
5131cb0ae1 Merge branch 'refs/heads/develop' 2013-11-16 13:32:37 +01:00
Ruud
d023eb8f1f Wrong variable logged in email notification 2013-10-30 23:10:02 +01:00
Ruud Burger
d6fa5c97db Merge pull request #2387 from restanrm/master
Add support for StartTLS and allow modification of SMTP server port
2013-10-30 15:05:34 -07:00
Ruud
3717443e85 Merge branch 'refs/heads/develop' 2013-10-30 21:39:50 +01:00
Adrien RAFFIN
a1ba39b3d3 Add support for starttls and allow modification of SMTP server port 2013-10-23 10:35:32 +02:00
Ruud
b4ad7b459f Merge branch 'refs/heads/develop' 2013-10-22 14:17:45 +02:00
Ruud
5af8fd0b21 Merge branch 'refs/heads/develop' 2013-10-18 17:29:10 +02:00
Ruud
83e7a8d765 Merge branch 'refs/heads/develop' 2013-10-14 21:57:19 +02:00
Ruud
4bdd4eab64 Merge branch 'refs/heads/develop' 2013-10-14 00:02:30 +02:00
Ruud
5e683b5a48 Revert "TorrentBytes login url change. fix #2317"
This reverts commit 95d0dacd28.
2013-10-07 23:43:08 +02:00
Ruud
f178825d21 Merge branch 'refs/heads/develop' 2013-10-07 09:20:57 +02:00
Ruud
fe2290fccb Merge branch 'refs/heads/develop' 2013-09-29 14:00:20 +02:00
Ruud
e4d67645b7 Merge branch 'refs/heads/develop' 2013-09-28 23:43:49 +02:00
Ruud
8d108b92bf One Up 2013-09-23 21:48:12 +02:00
Ruud
46783028b1 Merge branch 'refs/heads/develop' into desktop 2013-09-23 21:36:45 +02:00
Ruud
324415be15 Merge branch 'refs/heads/develop' 2013-09-23 21:35:51 +02:00
Ruud
7c44f9ab13 Merge branch 'refs/heads/develop' 2013-09-23 21:30:19 +02:00
Ruud
628fda2097 Merge branch 'refs/heads/develop' 2013-09-20 18:15:28 +02:00
Ruud
d08c7c57a8 One up! 2013-09-20 17:46:54 +02:00
Ruud
eeeb845ef3 Simplify string before checking on imdb 2013-09-20 17:30:11 +02:00
Ruud
651a063f94 Fix about submenu 2013-09-20 16:33:01 +02:00
Ruud
f20aaa2d9d Hide IE clear button on search 2013-09-20 16:23:42 +02:00
Ruud
ba925ec191 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	couchpotato/core/plugins/suggestion/main.py
2013-09-20 16:12:40 +02:00
Ruud
da6d749072 Merge branch 'refs/heads/develop' 2013-09-19 22:11:21 +02:00
Ruud
bef2b28acc Merge branch 'refs/heads/develop' 2013-09-18 23:06:29 +02:00
Ruud
302f571837 Merge branch 'refs/heads/develop' 2013-09-18 21:45:03 +02:00
Ruud
41dde209d5 Merge branch 'refs/heads/develop' 2013-09-14 11:41:45 +02:00
Ruud
5d350ef5ac Merge branch 'refs/heads/develop' 2013-09-11 09:29:05 +02:00
Ruud
4fd1d986dd Merge branch 'refs/heads/develop'
Conflicts:
	couchpotato/static/style/main.css
2013-09-11 09:11:04 +02:00
Ruud
55d57bc07b Give minified own FileHandler 2013-09-10 23:25:05 +02:00
Ruud
a81a262fb6 Change static path 2013-09-10 23:25:05 +02:00
Ruud
c37360f848 Login styling 2013-09-10 23:25:05 +02:00
Ruud
d7700900db Login base 2013-09-10 23:25:05 +02:00
Ruud
faa136a365 Merge branch 'refs/heads/develop' 2013-09-10 09:39:18 +02:00
Ruud
bd73b94ea4 Merge branch 'refs/heads/develop' 2013-09-09 22:29:00 +02:00
Ruud
d764d0f096 Merge branch 'refs/heads/develop' 2013-09-08 22:17:03 +02:00
Ruud Burger
fc8db130e0 Merge pull request #1947 from iguyking/patch-1
Update contributing.md
2013-07-15 04:17:17 -07:00
iguyking
682d678f91 Update contributing.md
Fixed to say what was intended
2013-07-14 11:49:48 -05:00
Ruud
3b7376fd18 One up 2013-07-06 01:01:26 +02:00
Ruud
06a211a24a Ignore current suggested results 2013-07-06 00:49:26 +02:00
Ruud
c31b10c798 Ignore current suggested results 2013-07-06 00:49:11 +02:00
Ruud
acda664686 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2013-07-05 22:43:54 +02:00
Ruud
55af696b7c Merge branch 'refs/heads/develop'
Conflicts:
	version.py
2013-07-05 22:18:27 +02:00
Ruud
636e9514e8 Merge branch 'refs/heads/develop' 2013-07-05 22:11:10 +02:00
Ruud
47e649643f Merge branch 'refs/heads/develop'
Conflicts:
	couchpotato/core/helpers/request.py
2013-07-01 23:34:40 +02:00
Ruud
bf59d2f357 Allow unknown keywords for all api calls. fix #1881 2013-06-24 21:22:12 +02:00
Ruud
fb90f6591b Get array arguments as list. fix #1875 2013-06-24 00:26:31 +02:00
Ruud
d66722e737 Allow non trailing slash API calls 2013-06-23 23:30:47 +02:00
Ruud
47d37c2ec9 Merge branch 'refs/heads/develop' 2013-06-23 12:24:01 +02:00
Ruud
e2852407ea One up 2013-06-03 22:22:44 +02:00
Ruud
88e738c6cd Don't show double updater name 2013-06-03 22:22:35 +02:00
Ruud
eaae8bdb0b Merge branch 'refs/heads/develop' into desktop 2013-06-03 22:00:21 +02:00
Ruud
40324ee89f Merge branch 'refs/heads/develop' 2013-06-03 21:59:26 +02:00
Ruud
9db1f3430e Append instead of add for subtitle file list 2013-05-29 19:30:40 +02:00
Ruud
ec19932eef Merge branch 'refs/heads/develop' 2013-05-29 19:10:50 +02:00
Ruud
daf31870f3 Merge branch 'refs/heads/develop' 2013-05-29 14:51:48 +02:00
Ruud
35d49f6a5e Merge branch 'refs/heads/develop' 2013-05-28 21:15:04 +02:00
Ruud
5c0d8a7fef Merge branch 'refs/heads/develop' 2013-05-19 01:19:53 +02:00
Ruud
b2ab114b6d Merge branch 'refs/heads/develop' 2013-05-18 17:29:02 +02:00
Ruud
a8523e6d01 Merge branch 'refs/heads/develop' 2013-05-17 15:48:12 +02:00
Ruud
f946389d60 Merge branch 'refs/heads/develop' 2013-05-11 00:08:51 +02:00
Ruud
821f68909d One up 2013-05-05 21:19:10 +02:00
Ruud
2b8dfed475 Merge branch 'refs/heads/master' into desktop
Conflicts:
	version.py
2013-05-05 20:31:28 +02:00
Ruud
0a749ce913 Merge branch 'refs/heads/develop' 2013-05-05 20:24:40 +02:00
Ruud
dfd2c33657 Extend files, not append 2013-05-05 10:15:19 +02:00
Ruud
7aad27c3d2 Last message check 0 after first message 2013-05-03 23:05:17 +02:00
Ruud
7a5588d5de Merge branch 'refs/heads/develop' 2013-05-03 22:51:35 +02:00
Ruud
f1dde5c925 Merge branch 'refs/heads/develop' 2013-04-14 11:09:32 +02:00
Ruud
0eff4f0096 Merge branch 'master' of github.com:RuudBurger/CouchPotatoServer 2013-04-05 23:59:56 +02:00
Ruud
4d7fa08805 Merge branch 'refs/heads/develop' 2013-04-05 23:57:54 +02:00
Ruud
f0af184262 Merge branch 'refs/heads/develop' 2013-04-02 11:32:20 +02:00
Ruud
5a23be2224 Merge branch 'refs/heads/develop' 2013-03-26 21:42:25 +01:00
Ruud
7f87b255f9 Merge branch 'refs/heads/develop' 2013-03-26 21:10:27 +01:00
Ruud
5ac1118db3 Merge branch 'refs/heads/develop' 2013-03-20 20:32:57 +01:00
Ruud
2c46279617 Merge branch 'refs/heads/develop' 2013-03-20 19:37:15 +01:00
Ruud
5d6a9ad2d0 Merge branch 'refs/heads/develop' 2013-03-19 22:55:39 +01:00
Ruud
607b5ea766 Run exe after install 2013-03-19 21:22:07 +01:00
Ruud
88579cd71a One up 2013-03-19 20:52:07 +01:00
Ruud
6c57316ce6 Use https for changelog 2013-03-19 20:46:00 +01:00
Ruud
6702683da3 Merge branch 'refs/heads/develop' into desktop 2013-03-19 20:34:38 +01:00
Ruud
b9c2b42725 Merge branch 'refs/heads/develop' 2013-03-19 20:28:46 +01:00
Ruud
1ed58586a1 Force install install in AppData
Add images to installer
2013-03-18 23:56:54 +01:00
Ruud
a8369b4e93 Merge branch 'refs/heads/develop'
Conflicts:
	version.py
2013-03-18 21:57:58 +01:00
Ruud
f08ccd4fd8 One up installer 2013-03-17 22:34:04 +01:00
Ruud
312562a9f5 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2013-03-17 16:42:53 +01:00
Ruud
fab8e66fe1 One up
Conflicts:
	version.py
2013-03-17 16:40:22 +01:00
Ruud
4db1b57c70 Merge branch 'refs/heads/develop' 2013-03-17 16:31:31 +01:00
Ruud
b06dbd3069 Merge branch 'refs/heads/develop' 2013-03-12 21:12:18 +01:00
Ruud
f84aa8c638 Merge branch 'refs/heads/develop' 2013-03-09 18:15:26 +01:00
Ruud
8e07dfc730 Merge branch 'refs/heads/develop' 2013-03-08 14:46:01 +01:00
Ruud
a49a00a25f Host to 0.0.0.0 2013-02-14 23:02:44 +01:00
Ruud
673843fb66 Merge branch 'refs/heads/develop' 2013-02-12 23:25:11 +01:00
Ruud
811f35b028 Merge branch 'refs/heads/develop' 2013-02-04 23:11:39 +01:00
Ruud
ec6e2c240f Merge branch 'refs/heads/develop' 2013-01-28 23:21:52 +01:00
Ruud
9e260a89af One up 2013-01-26 14:51:39 +01:00
Ruud
d233e4d22e Merge branch 'refs/heads/develop' into desktop 2013-01-26 13:54:56 +01:00
Ruud
23893dbcb9 Merge branch 'refs/heads/develop' into desktop 2013-01-25 20:13:58 +01:00
Ruud
3187a0f820 Merge branch 'refs/heads/develop' 2013-01-25 15:52:54 +01:00
Ruud
f86b9299c4 Merge branch 'refs/heads/develop' 2013-01-25 14:21:11 +01:00
Ruud
d27d0abeb0 Merge branch 'refs/heads/develop'
Conflicts:
	version.py
2013-01-24 23:35:37 +01:00
Ruud
506871b506 One up 2013-01-23 23:10:55 +01:00
Ruud
6115917660 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2013-01-23 22:57:07 +01:00
Ruud
21df8819d3 Merge branch 'refs/heads/develop' into desktop 2013-01-23 22:55:09 +01:00
Ruud
7c59348138 Merge branch 'refs/heads/develop' 2013-01-23 22:54:29 +01:00
Ruud
ab53f44157 Remove non-int backup folders. closes #1298 2013-01-23 22:23:52 +01:00
Ruud
b35f325d94 Merge branch 'refs/heads/develop' 2013-01-23 22:16:26 +01:00
Ruud
fb3f3e11f6 Merge branch 'refs/heads/develop' into desktop 2013-01-22 21:40:40 +01:00
Ruud
178c8942c3 Merge branch 'refs/heads/develop' into desktop 2013-01-14 19:54:22 +01:00
Ruud
393c14de54 Urlencode spotweb id. fix #1213 2013-01-07 23:12:08 +01:00
Ruud
51e747049d One up 2013-01-07 23:10:42 +01:00
Ruud
0582f7d694 Urlencode spotweb id. fix #1213 2013-01-07 23:10:06 +01:00
Ruud
fa7cac7538 Merge branch 'refs/heads/develop' into desktop 2013-01-07 22:41:55 +01:00
Ruud
bff17c0b95 Merge branch 'refs/heads/develop' 2013-01-07 22:40:37 +01:00
Ruud
d172828ac5 Merge branch 'refs/heads/develop' 2013-01-02 14:12:07 +01:00
Ruud
9500ac73fc Link to downloaders 2013-01-02 13:52:44 +01:00
Ruud
e2cf7e4421 Merge branch 'refs/heads/develop' 2013-01-02 13:44:34 +01:00
Ruud
9a314cfbc4 One up 2012-12-29 00:03:45 +01:00
Ruud
5941d0bf77 Add version to update url 2012-12-29 00:03:36 +01:00
Ruud
d326c1c25c Merge branch 'refs/heads/master' into desktop
Conflicts:
	version.py
2012-12-28 23:31:08 +01:00
Ruud
7e6234298d Merge branch 'refs/heads/develop' 2012-12-28 23:25:40 +01:00
Ruud
d4da206f93 Merge branch 'refs/heads/develop' 2012-12-22 16:33:47 +01:00
Ruud
985a168724 Merge branch 'refs/heads/develop' 2012-12-21 23:18:00 +01:00
Ruud
173c6194ed Merge branch 'refs/heads/develop' 2012-12-19 11:12:26 +01:00
Ruud
bcd23ad10c Merge branch 'refs/heads/develop' 2012-12-17 15:13:00 +01:00
Ruud
898e6f487d Merge branch 'refs/heads/develop' 2012-12-16 23:52:06 +01:00
Ruud
96472a9a8f One up 2012-12-16 23:51:58 +01:00
Ruud
27252561e2 Merge branch 'refs/heads/develop' into desktop 2012-12-16 23:51:24 +01:00
Ruud
6618c3927c Merge branch 'refs/heads/develop' 2012-12-11 23:15:06 +01:00
Ruud
c9e732651f One up 2012-12-01 12:16:58 +01:00
Ruud
7849e7170d Uninstall only create files, no wildcard *.* 2012-12-01 12:16:51 +01:00
Ruud
087894eb4e Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2012-12-01 11:50:08 +01:00
Ruud
4b58b40226 Merge branch 'refs/heads/develop' 2012-12-01 11:48:54 +01:00
Ruud
3ecc826629 Merge branch 'refs/heads/develop'
Conflicts:
	version.py
2012-11-11 22:06:48 +01:00
Ruud
25f1b8c7a7 Fedora init fix #1009 2012-11-02 18:32:15 +01:00
Ruud
e71da1f14d Use proper description for binary build. fix #1005 2012-11-02 18:24:13 +01:00
Ruud
938b14ba18 One up installer 2012-10-29 20:45:17 +01:00
Ruud
d6522d8f38 One up installer 2012-10-27 18:49:44 +02:00
Ruud
78eab890e7 Merge branch 'refs/heads/develop' into desktop 2012-10-27 18:25:36 +02:00
Ruud
1a56191f83 Don't unzip 2012-10-27 18:22:50 +02:00
Ruud
41c0f34d95 Properly restart 2012-10-27 18:22:40 +02:00
Ruud
37bf205d7a Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2012-10-27 11:56:57 +02:00
Ruud
32fe3796e4 Merge branch 'refs/heads/develop' 2012-10-26 22:22:47 +02:00
Ruud
359d1aaafa Merge branch 'refs/heads/develop' 2012-10-26 14:54:12 +02:00
Ruud
fb5d336351 Merge branch 'refs/heads/develop' 2012-10-26 14:36:04 +02:00
Ruud
eb30dff986 Merge branch 'refs/heads/develop' 2012-10-13 00:00:44 +02:00
Ruud
9312336962 Merge branch 'refs/heads/develop' 2012-09-24 09:36:59 +02:00
Ruud
aa1fa3eb9a Add description 2012-09-19 15:42:33 +02:00
Ruud
0e2f8a612c Extract zip after build, for testing 2012-09-19 15:29:07 +02:00
Ruud
ade4338ea6 Merge branch 'refs/heads/develop' 2012-09-16 21:32:16 +02:00
Ruud
55b20324c0 Merge branch 'refs/heads/develop' 2012-09-16 12:36:48 +02:00
Ruud
465e7b2abc Merge branch 'refs/heads/develop' into desktop 2012-09-16 12:36:17 +02:00
Ruud
578fb45785 Installer 1 up 2012-09-16 11:35:56 +02:00
Ruud
c0fb28301d Merge branch 'refs/heads/develop'
Conflicts:
	version.py
2012-09-16 10:46:39 +02:00
Ruud
96995bbbe5 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2012-09-16 10:45:19 +02:00
Ruud
4cfdafebbc Merge branch 'refs/heads/develop' into desktop 2012-09-14 13:15:47 +02:00
Ruud
f9c2503f81 Merge branch 'refs/heads/develop' 2012-09-14 13:15:35 +02:00
Ruud
b97acb8ef5 Merge branch 'refs/heads/develop' into desktop 2012-09-14 13:08:19 +02:00
Ruud
5b4cdf05b1 Merge branch 'refs/heads/develop' 2012-09-14 13:06:56 +02:00
Ruud
d68d2dfdb6 Updated installer 2012-09-09 21:48:38 +02:00
Ruud
39b269a454 Merge branch 'refs/heads/develop' into desktop 2012-09-09 17:32:47 +02:00
Ruud
ac081d3e10 Getting ready for build 2012-09-09 17:28:23 +02:00
Ruud
5d4efb60cf Merge branch 'refs/heads/develop' into desktop 2012-09-08 16:01:49 +02:00
Ruud
6f25a6bdfd Merge branch 'refs/heads/develop' 2012-09-03 10:32:09 +02:00
Ruud
23427e95f7 Merge branch 'refs/heads/develop' 2012-08-26 23:09:51 +02:00
Ruud
cc408b980c Merge branch 'refs/heads/develop' into desktop
Conflicts:
	couchpotato/core/_base/updater/main.py
2012-08-05 16:18:35 +02:00
Ruud
90a09e573b Merge branch 'refs/heads/develop'
Conflicts:
	couchpotato/core/_base/updater/main.py
2012-08-05 16:15:53 +02:00
Ruud
e1d7440b9d Wrong branch in master 2012-07-15 00:23:44 +02:00
Ruud
59590b3ac9 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	couchpotato/core/_base/updater/main.py
2012-07-14 00:35:00 +02:00
Ruud
ff759dacf3 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	couchpotato/core/_base/updater/main.py
2012-07-11 22:43:45 +02:00
Ruud
a328e44130 Merge branch 'desktop' of github.com:RuudBurger/CouchPotatoServer into desktop 2012-05-15 23:23:56 +02:00
Ruud
7924cac5f9 Update installer version 2012-05-15 23:21:24 +02:00
Ruud
1cef3b0c93 remove --nogit tag 2012-05-15 23:21:24 +02:00
Ruud
3cd59edc8b Import errors
File icon
2012-05-15 23:21:24 +02:00
Ruud
0d624af01d Working PNG 2012-05-15 23:21:24 +02:00
Ruud
a09132570c Change branch to desktop 2012-05-15 23:21:14 +02:00
Ruud
ee3fc38432 Better setup 2012-05-15 23:21:14 +02:00
Ruud
dbf0192c8e Inno setup, start 2012-05-15 23:21:14 +02:00
Ruud
6962cfc3f5 new Desktop runner 2012-05-15 23:21:14 +02:00
Ruud
e096ec3b5b Desktop files 2012-05-15 23:20:05 +02:00
Ruud
b30a74ae0c Merge branch 'refs/heads/develop' into desktop 2012-05-15 23:15:17 +02:00
Ruud
978eeb16c9 Update installer version 2012-05-15 23:14:20 +02:00
Ruud
e5c9d91657 Merge branch 'refs/heads/develop' into desktop 2012-05-15 22:27:22 +02:00
Ruud
fa81c3a07a Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2012-05-14 22:00:02 +02:00
Ruud
9cdd520d41 Merge branch 'refs/heads/develop' into desktop 2012-05-14 20:22:55 +02:00
Ruud
55d7898771 Merge branch 'refs/heads/develop' into desktop 2012-05-13 12:56:45 +02:00
Ruud
b8256bef97 Merge branch 'refs/heads/develop' into desktop 2012-05-12 00:35:52 +02:00
Ruud
5be9dc0b4a Merge branch 'refs/heads/develop' into desktop 2012-05-09 22:20:53 +02:00
Ruud
7d0be0cefb remove --nogit tag 2012-05-07 22:55:54 +02:00
Ruud
f7ce1edb13 Merge branch 'refs/heads/develop' into desktop 2012-05-07 22:44:01 +02:00
Ruud
5ad9280b60 Merge branch 'refs/heads/develop' into desktop 2012-05-07 22:27:55 +02:00
Ruud
2b353f1b20 Merge branch 'refs/heads/develop' into desktop 2012-05-04 17:29:15 +02:00
Ruud
75ab90b87b Merge branch 'refs/heads/develop' into desktop 2012-05-02 21:40:19 +02:00
Ruud
0219296120 Import errors
File icon
2012-05-02 21:34:45 +02:00
Ruud
20032b3a31 Working PNG 2012-05-01 07:35:44 +02:00
Ruud
ea9e9a8c90 Updater base 2012-05-01 07:35:27 +02:00
Ruud
f7b0ee145b Change branch to desktop 2012-04-30 21:37:04 +02:00
Ruud
cc866738ee Merge branch 'refs/heads/develop' into desktop 2012-04-30 21:32:56 +02:00
Ruud
eadccf6e33 Merge branch 'refs/heads/develop' into desktop 2012-04-29 00:00:25 +02:00
Ruud
b70b66e567 Merge branch 'refs/heads/develop' into desktop 2012-04-28 23:14:59 +02:00
Ruud
5b6792dc20 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	CouchPotato.py
	couchpotato/core/plugins/renamer/main.py
	couchpotato/core/plugins/trailer/__init__.py
2012-04-07 21:35:36 +02:00
Ruud
f498e7343a Better setup 2012-02-25 01:48:58 +01:00
Ruud
6962f441e6 Inno setup, start 2012-02-21 18:50:34 +01:00
Ruud
1def62b1b1 new Desktop runner 2012-02-19 17:13:37 +01:00
Ruud
a4a4a6a185 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	CouchPotato.py
2012-02-19 13:14:56 +01:00
Ruud
d4c9469c1a Remove nfo when not renaming as .orig.nfo 2012-02-19 12:53:55 +01:00
Ruud
3e2d4c5d7b Initial trailer support 2012-02-19 12:48:54 +01:00
Ruud
d03f711d69 kwargs in file.download for urlopen 2012-02-19 12:45:22 +01:00
Ruud
44dd8d9b96 Merge lists, not overwrite 2012-02-19 12:37:25 +01:00
Ruud
549a3be0d8 Merge branch 'refs/heads/develop' into desktop 2012-02-12 00:10:56 +01:00
Ruud
1bb2edf8ec Merge branch 'refs/heads/develop' into desktop 2012-02-11 23:33:14 +01:00
Ruud
84c6f36315 Desktop files 2012-02-11 23:06:14 +01:00
797 changed files with 32759 additions and 103538 deletions

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env python
from __future__ import print_function
from logging import handlers
from os.path import dirname
import logging
@@ -18,7 +19,12 @@ base_path = dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(base_path, 'libs'))
from couchpotato.environment import Env
from couchpotato.core.helpers.variable import getDataDir
from couchpotato.core.helpers.variable import getDataDir, removePyc
# Remove pyc files before dynamic load (sees .pyc files regular .py modules)
removePyc(base_path)
class Loader(object):
@@ -28,7 +34,7 @@ class Loader(object):
# Get options via arg
from couchpotato.runner import getOptions
self.options = getOptions(base_path, sys.argv[1:])
self.options = getOptions(sys.argv[1:])
# Load settings
settings = Env.get('settings')
@@ -49,7 +55,7 @@ class Loader(object):
# Create logging dir
self.log_dir = os.path.join(self.data_dir, 'logs');
if not os.path.isdir(self.log_dir):
os.mkdir(self.log_dir)
os.makedirs(self.log_dir)
# Logging
from couchpotato.core.logger import CPLog
@@ -66,10 +72,11 @@ class Loader(object):
signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1))
from couchpotato.core.event import addEvent
addEvent('app.after_shutdown', self.afterShutdown)
addEvent('app.do_shutdown', self.setRestart)
def afterShutdown(self, restart):
def setRestart(self, restart):
self.do_restart = restart
return True
def onExit(self, signal, frame):
from couchpotato.core.event import fireEvent
@@ -97,7 +104,6 @@ class Loader(object):
# Release log files and shutdown logger
logging.shutdown()
time.sleep(3)
args = [sys.executable] + [os.path.join(base_path, os.path.basename(__file__))] + sys.argv[1:]
subprocess.Popen(args)
@@ -132,14 +138,15 @@ if __name__ == '__main__':
pass
except SystemExit:
raise
except socket.error as (nr, msg):
except socket.error as e:
# log when socket receives SIGINT, but continue.
# previous code would have skipped over other types of IO errors too.
nr, msg = e
if nr != 4:
try:
l.log.critical(traceback.format_exc())
except:
print traceback.format_exc()
print(traceback.format_exc())
raise
except:
try:
@@ -148,7 +155,7 @@ if __name__ == '__main__':
if l:
l.log.critical(traceback.format_exc())
else:
print traceback.format_exc()
print(traceback.format_exc())
except:
print traceback.format_exc()
print(traceback.format_exc())
raise

235
Desktop.py Normal file
View File

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

View File

@@ -1,4 +1,4 @@
CouchPotato Server
CouchPotato
=====
CouchPotato (CP) is an automatic NZB and torrent downloader. You can keep a "movies I want"-list and it will search for NZBs/torrents of these movies every X hours.
@@ -7,7 +7,7 @@ Once a movie is found, it will send it to SABnzbd or download the torrent to a s
## Running from Source
CouchPotatoServer can be run from source. This will use *git* as updater, so make sure that is installed also.
CouchPotatoServer can be run from source. This will use *git* as updater, so make sure that is installed.
Windows, see [the CP forum](http://couchpota.to/forum/showthread.php?tid=14) for more details:
@@ -17,9 +17,9 @@ 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/`
* Your browser should open up, but if it doesn't go to `http://localhost:5050/`
OSx:
OS X:
* If you're on Leopard (10.5) install Python 2.6+: [Python 2.6.5](http://www.python.org/download/releases/2.6.5/)
* Install [GIT](http://git-scm.com/)
@@ -27,20 +27,21 @@ 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/`
* Your browser should open up, but if it doesn't go to `http://localhost:5050/`
Linux (ubuntu / debian):
Linux (Ubuntu / Debian):
* Install [GIT](http://git-scm.com/) with `apt-get install git-core`
* '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. `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/`
* To run on boot copy the init script `sudo cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato`
* Copy the default paths file `sudo cp CouchPotatoServer/init/ubuntu.default /etc/default/couchpotato`
* Change the paths inside the default file `sudo nano /etc/default/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/`
FreeBSD :
@@ -56,7 +57,7 @@ FreeBSD :
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
* Then run `sudo python CouchPotatoServer/CouchPotato.py` to start for the first time
* To run on boot copy the init script. `sudo cp CouchPotatoServer/init/freebsd /etc/rc.d/couchpotato`
* Change the paths inside the init script. `sudo vim /etc/init.d/couchpotato`
* Change the paths inside the init script. `sudo vim /etc/rc.d/couchpotato`
* Make init script executable. `sudo chmod +x /etc/rc.d/couchpotato`
* Add init to startup. `sudo echo 'couchpotato_enable="YES"' >> /etc/rc.conf`
* Open your browser and go to: `http://server:5050/`

View File

@@ -1,25 +1,36 @@
## Got a issue/feature request or submitting a pull request?
# Contributing to CouchPotatoServer
Make sure you think of the following things:
1. [Contributing](#contributing)
2. [Submitting an Issue](#issues)
3. [Submitting a Pull Request](#pull-requests)
## Issue
* 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.
* Also check the logs before submitting, obvious errors like permission or http errors are often not related to CP.
* What is the movie + quality you are searching for?
* What are you're settings for the specific problem?
* What providers are you using? (While you're logs include these, scanning through hundred of lines of log isn't our hobby)
* Post the logs from config directory, please do not copy paste the UI. Use pastebin to store these logs!
## Contributing
Thank you for your interest in contributing to CouchPotato. There are several ways to help out, even if you've never worked on an open source project before.
If you've found a bug or want to request a feature, you can report it by [posting an issue](https://github.com/RuudBurger/CouchPotatoServer/issues/new) - be sure to read the [guidelines](#issues) first!
If you want to contribute your own work, please read the [guidelines](#pull-requests) for submitting a pull request.
Lastly, for anything related to CouchPotato, feel free to stop by the [forum](http://couchpota.to/forum/) or the [#couchpotato](http://webchat.freenode.net/?channels=couchpotato) IRC channel at irc.freenode.net.
## Issues
Issues are intended for reporting bugs and weird behaviour or suggesting improvements to CouchPotatoServer.
Before you submit an issue, please go through the following checklist:
* Search through existing issues (*including closed issues!*) first: you might be able to get your answer there.
* Double check your issue manually, because it could be an external issue.
* Post logs with your issue: Without seeing what is going on, the developers can't reproduce the error.
* Check the logs yourself before submitting them. Obvious errors like permission or HTTP errors are often not related to CouchPotato.
* What movie and quality are you searching for?
* What are your settings for the specific problem?
* What providers are you using? (While your logs include these, scanning through hundreds of lines of logs isn't our hobby)
* Post the logs from the *config* directory, please do not copy paste the UI. Use pastebin to store these logs!
* Give a short step by step of how to reproduce the error.
* 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 "why closed" if it clearly says the issue in the tag ;)
* If you're running on a NAS (QNAP, Austor etc..) with pre-made packages, make sure these are setup to use our source repo (RuudBurger/CouchPotatoServer) and nothing else!!
* What hardware / OS are you using and what are its limitations? For example: NAS can be slow and maybe have a different version of python installed than when you use CP on OS X or Windows.
* Your issue might be marked with the "can't reproduce" tag. Don't ask why your issue was closed if it says so in the tag.
* If you're running on a NAS (QNAP, Austor, Synology etc.) with pre-made packages, make sure these are set up to use our source repository (RuudBurger/CouchPotatoServer) and nothing else!
## Pull Request
* Make sure you're pull request is made for develop branch (or relevant feature branch)
The more relevant information you provide, the more likely that your issue will be resolved.
## Pull Requests
Pull requests are intended for contributing code or documentation to the project. Before you submit a pull request, consider the following:
* Make sure your pull request is made for the *develop* branch (or relevant feature branch).
* Have you tested your PR? If not, why?
* Are there any limitations of your PR we should know of?
* Make sure to keep you're PR up-to-date with the branch you're trying to push into.
**If we don't get enough info, the chance of the issue getting closed is a lot bigger ;)**
* Does your PR have any limitations we should know of?
* Is your PR up-to-date with the branch you're trying to push into?

View File

@@ -1,3 +1,7 @@
import os
import time
import traceback
from couchpotato.api import api_docs, api_docs_missing, api
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.variable import md5, tryInt
@@ -5,13 +9,10 @@ from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
from tornado import template
from tornado.web import RequestHandler, authenticated
import os
import time
import traceback
log = CPLog(__name__)
views = {}
template_loader = template.Loader(os.path.join(os.path.dirname(__file__), 'templates'))
@@ -24,9 +25,10 @@ class BaseHandler(RequestHandler):
if username and password:
return self.get_secure_cookie('user')
else: # Login when no username or password are set
else: # Login when no username or password are set
return True
# Main web handler
class WebHandler(BaseHandler):
@@ -43,11 +45,13 @@ class WebHandler(BaseHandler):
log.error("Failed doing web request '%s': %s", (route, traceback.format_exc()))
self.write({'success': False, 'error': 'Failed returning results'})
def addView(route, func, static = False):
def addView(route, func):
views[route] = func
def get_session(engine = None):
return Env.getSession(engine)
def get_db():
return Env.get('db')
# Web view
@@ -55,12 +59,10 @@ def index():
return template_loader.load('index.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env)
addView('', index)
# API docs
def apiDocs():
routes = []
for route in api.iterkeys():
routes.append(route)
routes = list(api.keys())
if api_docs.get(''):
del api_docs['']
@@ -70,21 +72,30 @@ def apiDocs():
addView('docs', apiDocs)
# Database debug manager
def databaseManage():
return template_loader.load('database.html').generate(fireEvent = fireEvent, Env = Env)
addView('database', databaseManage)
# Make non basic auth option to get api key
class KeyHandler(RequestHandler):
def get(self, *args, **kwargs):
api = None
api_key = None
try:
username = Env.setting('username')
password = Env.setting('password')
if (self.get_argument('u') == md5(username) or not username) and (self.get_argument('p') == password or not password):
api = Env.setting('api_key')
api_key = Env.setting('api_key')
self.write({
'success': api is not None,
'api_key': api
'success': api_key is not None,
'api_key': api_key
})
except:
log.error('Failed doing key request: %s', (traceback.format_exc()))
@@ -102,20 +113,21 @@ class LoginHandler(BaseHandler):
def post(self, *args, **kwargs):
api = None
api_key = None
username = Env.setting('username')
password = Env.setting('password')
if (self.get_argument('username') == username or not username) and (md5(self.get_argument('password')) == password or not password):
api = Env.setting('api_key')
api_key = Env.setting('api_key')
if api:
if api_key:
remember_me = tryInt(self.get_argument('remember_me', default = 0))
self.set_secure_cookie('user', api, expires_days = 30 if remember_me > 0 else None)
self.set_secure_cookie('user', api_key, expires_days = 30 if remember_me > 0 else None)
self.redirect(Env.get('web_base'))
class LogoutHandler(BaseHandler):
def get(self, *args, **kwargs):
@@ -136,4 +148,3 @@ def page_not_found(rh):
rh.set_status(404)
rh.write('Wrong API key used')

View File

@@ -1,15 +1,15 @@
from couchpotato.core.helpers.request import getParams
from couchpotato.core.logger import CPLog
from functools import wraps
from threading import Thread
from tornado.gen import coroutine
from tornado.web import RequestHandler, asynchronous
import json
import threading
import tornado
import traceback
import urllib
from couchpotato.core.helpers.request import getParams
from couchpotato.core.logger import CPLog
from tornado.web import RequestHandler, asynchronous
log = CPLog(__name__)
@@ -20,15 +20,25 @@ api_nonblock = {}
api_docs = {}
api_docs_missing = []
def run_async(func):
@wraps(func)
def async_func(*args, **kwargs):
func_hl = Thread(target = func, args = args, kwargs = kwargs)
func_hl.start()
return func_hl
return async_func
@run_async
def run_handler(route, kwargs, callback = None):
try:
res = api[route](**kwargs)
callback(res, route)
except:
log.error('Failed doing api request "%s": %s', (route, traceback.format_exc()))
callback({'success': False, 'error': 'Failed returning results'}, route)
# NonBlock API handler
class NonBlockHandler(RequestHandler):
@@ -61,6 +71,7 @@ class NonBlockHandler(RequestHandler):
self.stopper = None
def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
api_nonblock[route] = func_tuple
@@ -69,16 +80,22 @@ def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
else:
api_docs_missing.append(route)
# Blocking API handler
class ApiHandler(RequestHandler):
@coroutine
@asynchronous
def get(self, route, *args, **kwargs):
route = route.strip('/')
if not api.get(route):
self.write('API call doesn\'t seem to exist')
self.finish()
return
# Create lock if it doesn't exist
if route in api_locks and not api_locks.get(route):
api_locks[route] = threading.Lock()
api_locks[route].acquire()
try:
@@ -89,39 +106,51 @@ class ApiHandler(RequestHandler):
# Split array arguments
kwargs = getParams(kwargs)
kwargs['_request'] = self
# Remove t random string
try: del kwargs['t']
except: pass
# Add async callback handler
@run_async
def run_handler(callback):
try:
result = api[route](**kwargs)
callback(result)
except:
log.error('Failed doing api request "%s": %s', (route, traceback.format_exc()))
callback({'success': False, 'error': 'Failed returning results'})
result = yield tornado.gen.Task(run_handler)
# Check JSONP callback
jsonp_callback = self.get_argument('callback_func', default = None)
if jsonp_callback:
self.write(str(jsonp_callback) + '(' + json.dumps(result) + ')')
self.set_header("Content-Type", "text/javascript")
elif isinstance(result, tuple) and result[0] == 'redirect':
self.redirect(result[1])
else:
self.write(result)
run_handler(route, kwargs, callback = self.taskFinished)
except:
log.error('Failed doing api request "%s": %s', (route, traceback.format_exc()))
self.write({'success': False, 'error': 'Failed returning results'})
try:
self.write({'success': False, 'error': 'Failed returning results'})
self.finish()
except:
log.error('Failed write error "%s": %s', (route, traceback.format_exc()))
api_locks[route].release()
post = get
def taskFinished(self, result, route):
if not self.request.connection.stream.closed():
try:
# Check JSONP callback
jsonp_callback = self.get_argument('callback_func', default = None)
if jsonp_callback:
self.write(str(jsonp_callback) + '(' + json.dumps(result) + ')')
self.set_header("Content-Type", "text/javascript")
self.finish()
elif isinstance(result, tuple) and result[0] == 'redirect':
self.redirect(result[1])
else:
self.write(result)
self.finish()
except:
log.debug('Failed doing request, probably already closed: %s', (traceback.format_exc()))
try: self.finish({'success': False, 'error': 'Failed returning results'})
except: pass
api_locks[route].release()
def addApiView(route, func, static = False, docs = None, **kwargs):
if static: func(route)

View File

@@ -1,10 +1,3 @@
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.variable import cleanHost, md5
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
from tornado.ioloop import IOLoop
from uuid import uuid4
import os
import platform
@@ -13,8 +6,19 @@ import time
import traceback
import webbrowser
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.variable import cleanHost, md5, isSubFolder
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
from tornado.ioloop import IOLoop
log = CPLog(__name__)
autoload = 'Core'
class Core(Plugin):
@@ -47,6 +51,7 @@ class Core(Plugin):
addEvent('app.api_url', self.createApiUrl)
addEvent('app.version', self.version)
addEvent('app.load', self.checkDataDir)
addEvent('app.load', self.cleanUpFolders)
addEvent('setting.save.core.password', self.md5Password)
addEvent('setting.save.core.api_key', self.checkApikey)
@@ -66,11 +71,15 @@ class Core(Plugin):
return value if value and len(value) > 3 else uuid4().hex
def checkDataDir(self):
if Env.get('app_dir') in Env.get('data_dir'):
if isSubFolder(Env.get('data_dir'), Env.get('app_dir')):
log.error('You should NOT use your CouchPotato directory to save your settings in. Files will get overwritten or be deleted.')
return True
def cleanUpFolders(self):
only_clean = ['couchpotato', 'libs', 'init']
self.deleteEmptyFolder(Env.get('app_dir'), show_error = False, only_clean = only_clean)
def available(self, **kwargs):
return {
'success': True
@@ -82,7 +91,11 @@ class Core(Plugin):
def shutdown():
self.initShutdown()
IOLoop.current().add_callback(shutdown)
if IOLoop.current()._closing:
shutdown()
else:
IOLoop.current().add_callback(shutdown)
return 'shutdown'
@@ -105,7 +118,7 @@ class Core(Plugin):
self.shutdown_started = True
fireEvent('app.do_shutdown')
fireEvent('app.do_shutdown', restart = restart)
log.debug('Every plugin got shutdown event')
loop = True
@@ -117,7 +130,7 @@ class Core(Plugin):
if len(still_running) == 0:
break
elif starttime < time.time() - 30: # Always force break after 30s wait
elif starttime < time.time() - 30: # Always force break after 30s wait
break
running = list(set(still_running) - set(self.ignore_restart))
@@ -130,8 +143,11 @@ class Core(Plugin):
log.debug('Safe to shutdown/restart')
loop = IOLoop.current()
try:
IOLoop.current().stop()
if not loop._closing:
loop.stop()
except RuntimeError:
pass
except:
@@ -181,8 +197,104 @@ class Core(Plugin):
def signalHandler(self):
if Env.get('daemonized'): return
def signal_handler(signal, frame):
def signal_handler(*args, **kwargs):
fireEvent('app.shutdown', single = True)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
config = [{
'name': 'core',
'order': 1,
'groups': [
{
'tab': 'general',
'name': 'basics',
'description': 'Needs restart before changes take effect.',
'wizard': True,
'options': [
{
'name': 'username',
'default': '',
},
{
'name': 'password',
'default': '',
'type': 'password',
},
{
'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,
'type': 'bool',
'description': 'Launch the browser when I start.',
'wizard': True,
},
],
},
{
'tab': 'general',
'name': 'advanced',
'description': "For those who know what they're doing",
'advanced': True,
'options': [
{
'name': 'api_key',
'default': uuid4().hex,
'readonly': 1,
'description': 'Let 3rd party app do stuff. <a target="_self" href="../../docs/">Docs</a>',
},
{
'name': 'debug',
'default': 0,
'type': 'bool',
'description': 'Enable debugging.',
},
{
'name': 'development',
'default': 0,
'type': 'bool',
'description': 'Enable this if you\'re developing, and NOT in any other case, thanks.',
},
{
'name': 'data_dir',
'type': 'directory',
'description': 'Where cache/logs/etc are stored. Keep empty for defaults.',
},
{
'name': 'url_base',
'default': '',
'description': 'When using mod_proxy use this to append the url with this.',
},
{
'name': 'permission_folder',
'default': '0755',
'label': 'Folder CHMOD',
'description': 'Can be either decimal (493) or octal (leading zero: 0755)',
},
{
'name': 'permission_file',
'default': '0755',
'label': 'File CHMOD',
'description': 'Same as Folder CHMOD but for files',
},
],
},
],
}]

View File

@@ -1,100 +0,0 @@
from .main import Core
from uuid import uuid4
def start():
return Core()
config = [{
'name': 'core',
'order': 1,
'groups': [
{
'tab': 'general',
'name': 'basics',
'description': 'Needs restart before changes take effect.',
'wizard': True,
'options': [
{
'name': 'username',
'default': '',
},
{
'name': 'password',
'default': '',
'type': 'password',
},
{
'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,
'type': 'bool',
'description': 'Launch the browser when I start.',
'wizard': True,
},
],
},
{
'tab': 'general',
'name': 'advanced',
'description': "For those who know what they're doing",
'advanced': True,
'options': [
{
'name': 'api_key',
'default': uuid4().hex,
'readonly': 1,
'description': 'Let 3rd party app do stuff. <a target="_self" href="../../docs/">Docs</a>',
},
{
'name': 'debug',
'default': 0,
'type': 'bool',
'description': 'Enable debugging.',
},
{
'name': 'development',
'default': 0,
'type': 'bool',
'description': 'Enable this if you\'re developing, and NOT in any other case, thanks.',
},
{
'name': 'data_dir',
'type': 'directory',
'description': 'Where cache/logs/etc are stored. Keep empty for defaults.',
},
{
'name': 'url_base',
'default': '',
'description': 'When using mod_proxy use this to append the url with this.',
},
{
'name': 'permission_folder',
'default': '0755',
'label': 'Folder CHMOD',
'description': 'Can be either decimal (493) or octal (leading zero: 0755)',
},
{
'name': 'permission_file',
'default': '0755',
'label': 'File CHMOD',
'description': 'Same as Folder CHMOD but for files',
},
],
},
],
}]

View File

@@ -1,3 +1,7 @@
import os
import re
import traceback
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.variable import tryInt
@@ -7,12 +11,12 @@ from couchpotato.environment import Env
from minify.cssmin import cssmin
from minify.jsmin import jsmin
from tornado.web import StaticFileHandler
import os
import re
import traceback
log = CPLog(__name__)
autoload = 'ClientScript'
class ClientScript(Plugin):
@@ -45,20 +49,17 @@ class ClientScript(Plugin):
'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': {}, }
urls = {'style': {}, 'script': {}}
minified = {'style': {}, 'script': {}}
paths = {'style': {}, 'script': {}}
comment = {
'style': '/*** %s:%d ***/\n',
'script': '// %s:%d\n'
'style': '/*** %s:%d ***/\n',
'script': '// %s:%d\n'
}
html = {
@@ -90,7 +91,6 @@ class ClientScript(Plugin):
else:
self.registerStyle(core_url, file_path, position = 'front')
def minify(self):
# Create cache dir
@@ -124,7 +124,7 @@ class ClientScript(Plugin):
data = cssmin(data)
data = data.replace('../images/', '../static/images/')
data = data.replace('../fonts/', '../static/fonts/')
data = data.replace('../../static/', '../static/') # Replace inside plugins
data = data.replace('../../static/', '../static/') # Replace inside plugins
raw.append({'file': file_path, 'date': int(os.path.getmtime(file_path)), 'data': data})
@@ -187,6 +187,7 @@ class ClientScript(Plugin):
prefix_properties = ['border-radius', 'transform', 'transition', 'box-shadow']
prefix_tags = ['ms', 'moz', 'webkit']
def prefix(self, data):
trimmed_data = re.sub('(\t|\n|\r)+', '', data)

View File

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

View File

@@ -5,6 +5,9 @@ from couchpotato.environment import Env
log = CPLog(__name__)
autoload = 'Desktop'
if Env.get('desktop'):
class Desktop(Plugin):

View File

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

View File

@@ -0,0 +1,20 @@
from .main import Downloader
def autoload():
return Downloader()
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,37 +1,51 @@
from base64 import b32decode, b16encode
import random
import re
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import Provider
import random
import re
from couchpotato.core.media._base.providers.base import Provider
from couchpotato.core.plugins.base import Plugin
log = CPLog(__name__)
class Downloader(Provider):
## This is here to load the static files
class Downloader(Plugin):
pass
class DownloaderBase(Provider):
protocol = []
http_time_between_calls = 0
status_support = True
torrent_sources = [
'https://zoink.it/torrent/%s.torrent',
'http://torrage.com/torrent/%s.torrent',
'https://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://fr33domtracker.h33t.com:3310/announce',
'http://tracker.publicbt.com/announce',
'udp://tracker.publicbt.com:80/announce',
'http://tracker.ccc.de/announce',
'udp://tracker.ccc.de:80/announce',
'http://exodus.desync.com/announce',
'http://exodus.desync.com:6969/announce',
'http://tracker.publichd.eu/announce',
'udp://tracker.publichd.eu:80/announce',
'http://tracker.openbittorrent.com/announce',
'udp://tracker.openbittorrent.com/announce',
'udp://tracker.openbittorrent.com:80/announce',
'udp://open.demonii.com:1337/announce',
]
def __init__(self):
@@ -42,6 +56,7 @@ class Downloader(Provider):
addEvent('download.remove_failed', self._removeFailed)
addEvent('download.pause', self._pause)
addEvent('download.process_complete', self._processComplete)
addApiView('download.%s.test' % self.getName().lower(), self._test)
def getEnabledProtocol(self):
for download_protocol in self.protocol:
@@ -58,6 +73,9 @@ class Downloader(Provider):
return
return self.download(data = data, media = media, filedata = filedata)
def download(self, *args, **kwargs):
return False
def _getAllDownloadStatus(self, download_ids):
if self.isDisabled(manual = True, data = {}):
return
@@ -154,10 +172,19 @@ class Downloader(Provider):
if not data: data = {}
d_manual = self.conf('manual', default = False)
return super(Downloader, self).isEnabled() and \
return super(DownloaderBase, self).isEnabled() and \
(d_manual and manual or d_manual is False) and \
(not data or self.isCorrectProtocol(data.get('protocol')))
def _test(self, **kwargs):
t = self.test()
if isinstance(t, tuple):
return {'success': t[0], 'msg': t[1]}
return {'success': t}
def test(self):
return False
def _pause(self, release_download, pause = True):
if self.isDisabled(manual = True, data = {}):
return
@@ -171,6 +198,7 @@ class Downloader(Provider):
def pause(self, release_download, pause):
return
class ReleaseDownloadList(list):
provider = None
@@ -197,7 +225,7 @@ class ReleaseDownloadList(list):
'status': 'busy',
'downloader': self.provider.getName(),
'folder': '',
'files': '',
'files': [],
}
return mergeDicts(defaults, result)

View File

@@ -0,0 +1,76 @@
var DownloadersBase = new Class({
Implements: [Events],
initialize: function(){
var self = this;
// Add test buttons to settings page
App.addEvent('loadSettings', self.addTestButtons.bind(self));
},
// Downloaders setting tests
addTestButtons: function(){
var self = this;
var setting_page = App.getPage('Settings');
setting_page.addEvent('create', function(){
Object.each(setting_page.tabs.downloaders.groups, self.addTestButton.bind(self))
})
},
addTestButton: function(fieldset, plugin_name){
var self = this,
button_name = self.testButtonName(fieldset);
if(button_name.contains('Downloaders')) return;
new Element('.ctrlHolder.test_button').adopt(
new Element('a.button', {
'text': button_name,
'events': {
'click': function(){
var button = fieldset.getElement('.test_button .button');
button.set('text', 'Connecting...');
Api.request('download.'+plugin_name+'.test', {
'onComplete': function(json){
button.set('text', button_name);
var message;
if(json.success){
message = new Element('span.success', {
'text': 'Connection successful'
}).inject(button, 'after')
}
else {
var msg_text = 'Connection failed. Check logs for details.';
if(json.hasOwnProperty('msg')) msg_text = json.msg;
message = new Element('span.failed', {
'text': msg_text
}).inject(button, 'after')
}
(function(){
message.destroy();
}).delay(3000)
}
});
}
}
})
).inject(fieldset);
},
testButtonName: function(fieldset){
var name = String(fieldset.getElement('h2').innerHTML).substring(0,String(fieldset.getElement('h2').innerHTML).indexOf("<span"));
return 'Test '+name;
}
});
var Downloaders = new DownloadersBase();

View File

@@ -5,6 +5,8 @@ from couchpotato.core.plugins.base import Plugin
log = CPLog(__name__)
autoload = 'Scheduler'
class Scheduler(Plugin):
@@ -17,6 +19,7 @@ class Scheduler(Plugin):
addEvent('schedule.cron', self.cron)
addEvent('schedule.interval', self.interval)
addEvent('schedule.remove', self.remove)
addEvent('schedule.queue', self.queue)
self.sched = Sched(misfire_grace_time = 60)
self.sched.start()
@@ -30,9 +33,9 @@ class Scheduler(Plugin):
except:
pass
def doShutdown(self):
def doShutdown(self, *args, **kwargs):
self.stop()
return super(Scheduler, self).doShutdown()
return super(Scheduler, self).doShutdown(*args, **kwargs)
def stop(self):
if self.started:
@@ -64,3 +67,16 @@ class Scheduler(Plugin):
'seconds': seconds,
'job': self.sched.add_interval_job(handle, hours = hours, minutes = minutes, seconds = seconds)
}
return True
def queue(self, handlers = None):
if not handlers: handlers = []
for h in handlers:
h()
if self.shuttingDown():
break
return True

View File

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

View File

@@ -1,8 +1,10 @@
from .main import Updater
from couchpotato.environment import Env
import os
def start():
from .main import Updater
from couchpotato.environment import Env
def autoload():
return Updater()
config = [{

View File

@@ -1,20 +1,25 @@
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import ss
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
from datetime import datetime
from dateutil.parser import parse
from git.repository import LocalRepository
import json
import os
import shutil
import tarfile
import time
import traceback
import version
import zipfile
from datetime import datetime
from threading import RLock
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import sp
from couchpotato.core.helpers.variable import removePyc
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
from dateutil.parser import parse
from git.repository import LocalRepository
import version
from six.moves import filter
log = CPLog(__name__)
@@ -22,6 +27,7 @@ log = CPLog(__name__)
class Updater(Plugin):
available_notified = False
_lock = RLock()
def __init__(self):
@@ -32,6 +38,7 @@ class Updater(Plugin):
else:
self.updater = SourceUpdater()
addEvent('app.load', self.logVersion, priority = 10000)
addEvent('app.load', self.setCrons)
addEvent('updater.info', self.info)
@@ -53,12 +60,16 @@ class Updater(Plugin):
addEvent('setting.save.updater.enabled.after', self.setCrons)
def logVersion(self):
info = self.info()
log.info('=== VERSION %s, using %s ===', (info.get('version', {}).get('repr', 'UNKNOWN'), self.updater.getName()))
def setCrons(self):
fireEvent('schedule.remove', 'updater.check', single = True)
if self.isEnabled():
fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6)
self.autoUpdate() # Check after enabling
self.autoUpdate() # Check after enabling
def autoUpdate(self):
if self.isEnabled() and self.check() and self.conf('automatic') and not self.updater.update_failed:
@@ -94,7 +105,17 @@ class Updater(Plugin):
return False
def info(self, **kwargs):
return self.updater.info()
self._lock.acquire()
info = {}
try:
info = self.updater.info()
except:
log.error('Failed getting updater info: %s', traceback.format_exc())
self._lock.release()
return info
def checkView(self, **kwargs):
return {
@@ -121,6 +142,12 @@ class Updater(Plugin):
'success': success
}
def doShutdown(self, *args, **kwargs):
if not Env.get('dev') and not Env.get('desktop'):
removePyc(Env.get('app_dir'), show_logs = False)
return super(Updater, self).doShutdown(*args, **kwargs)
class BaseUpdater(Plugin):
@@ -138,42 +165,23 @@ class BaseUpdater(Plugin):
pass
def info(self):
current_version = self.getVersion()
return {
'last_check': self.last_check,
'update_version': self.update_version,
'version': self.getVersion(),
'version': current_version,
'repo_name': '%s/%s' % (self.repo_user, self.repo_name),
'branch': self.branch,
'branch': current_version.get('branch', self.branch),
}
def getVersion(self):
pass
def check(self):
pass
def deletePyc(self, only_excess = True):
for root, dirs, files in os.walk(ss(Env.get('app_dir'))):
pyc_files = filter(lambda filename: filename.endswith('.pyc'), files)
py_files = set(filter(lambda filename: filename.endswith('.py'), files))
excess_pyc_files = filter(lambda pyc_filename: pyc_filename[:-1] not in py_files, pyc_files) if only_excess else pyc_files
for excess_pyc_file in excess_pyc_files:
full_path = os.path.join(root, excess_pyc_file)
log.debug('Removing old PYC file: %s', full_path)
try:
os.remove(full_path)
except:
log.error('Couldn\'t remove %s: %s', (full_path, traceback.format_exc()))
for dir_name in dirs:
full_path = os.path.join(root, dir_name)
if len(os.listdir(full_path)) == 0:
try:
os.rmdir(full_path)
except:
log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc()))
class GitUpdater(BaseUpdater):
@@ -186,9 +194,6 @@ class GitUpdater(BaseUpdater):
log.info('Updating to latest version')
self.repo.pull()
# Delete leftover .pyc files
self.deletePyc()
return True
except:
log.error('Failed updating via GIT: %s', traceback.format_exc())
@@ -201,14 +206,16 @@ class GitUpdater(BaseUpdater):
if not self.version:
try:
output = self.repo.getHead() # Yes, please
output = self.repo.getHead() # Yes, please
log.debug('Git version output: %s', output.hash)
self.version = {
'repr': 'git:(%s:%s % s) %s (%s)' % (self.repo_user, self.repo_name, self.repo.getCurrentBranch().name or self.branch, output.hash[:8], datetime.fromtimestamp(output.getDate())),
'hash': output.hash[:8],
'date': output.getDate(),
'type': 'git',
'branch': self.repo.getCurrentBranch().name
}
except Exception, e:
except Exception as e:
log.error('Failed using GIT updater, running from source, you need to have GIT installed. %s', e)
return 'No GIT'
@@ -231,7 +238,7 @@ class GitUpdater(BaseUpdater):
local = self.repo.getHead()
remote = branch.getHead()
log.info('Versions, local:%s, remote:%s', (local.hash[:8], remote.hash[:8]))
log.debug('Versions, local:%s, remote:%s', (local.hash[:8], remote.hash[:8]))
if local.getDate() < remote.getDate():
self.update_version = {
@@ -244,7 +251,6 @@ class GitUpdater(BaseUpdater):
return False
class SourceUpdater(BaseUpdater):
def __init__(self):
@@ -270,9 +276,9 @@ class SourceUpdater(BaseUpdater):
# Extract
if download_data.get('type') == 'zip':
zip = zipfile.ZipFile(destination)
zip.extractall(extracted_path)
zip.close()
zip_file = zipfile.ZipFile(destination)
zip_file.extractall(extracted_path)
zip_file.close()
else:
tar = tarfile.open(destination)
tar.extractall(path = extracted_path)
@@ -294,11 +300,12 @@ class SourceUpdater(BaseUpdater):
return False
def replaceWith(self, path):
app_dir = ss(Env.get('app_dir'))
data_dir = ss(Env.get('data_dir'))
path = sp(path)
app_dir = Env.get('app_dir')
data_dir = Env.get('data_dir')
# Get list of files we want to overwrite
self.deletePyc()
removePyc(app_dir)
existing_files = []
for root, subfiles, filenames in os.walk(app_dir):
for filename in filenames:
@@ -339,13 +346,12 @@ class SourceUpdater(BaseUpdater):
return True
def removeDir(self, path):
try:
if os.path.isdir(path):
shutil.rmtree(path)
except OSError, inst:
os.chmod(inst.filename, 0777)
except OSError as inst:
os.chmod(inst.filename, 0o777)
self.removeDir(path)
def getVersion(self):
@@ -359,7 +365,8 @@ class SourceUpdater(BaseUpdater):
log.debug('Source version output: %s', output)
self.version = output
self.version['type'] = 'source'
except Exception, e:
self.version['repr'] = 'source:(%s:%s % s) %s (%s)' % (self.repo_user, self.repo_name, self.branch, output.get('hash', '')[:8], datetime.fromtimestamp(output.get('date', 0)))
except Exception as e:
log.error('Failed using source updater. %s', e)
return {}
@@ -389,7 +396,7 @@ class SourceUpdater(BaseUpdater):
return {
'hash': commit['sha'],
'date': int(time.mktime(parse(commit['commit']['committer']['date']).timetuple())),
'date': int(time.mktime(parse(commit['commit']['committer']['date']).timetuple())),
}
except:
log.error('Failed getting latest request from github: %s', traceback.format_exc())
@@ -434,7 +441,7 @@ class DesktopUpdater(BaseUpdater):
if latest and latest != current_version.get('hash'):
self.update_version = {
'hash': latest,
'date': None,
'date': None,
'changelog': self.desktop._changelogURL,
}
@@ -446,6 +453,7 @@ class DesktopUpdater(BaseUpdater):
def getVersion(self):
return {
'repr': 'desktop: %s' % self.desktop._esky.active_version,
'hash': self.desktop._esky.active_version,
'date': None,
'type': 'desktop',

View File

@@ -5,7 +5,7 @@ var UpdaterBase = new Class({
initialize: function(){
var self = this;
App.addEvent('load', self.info.bind(self, 2000))
App.addEvent('load', self.info.bind(self, 2000));
App.addEvent('unload', function(){
if(self.timer)
clearTimeout(self.timer);
@@ -24,7 +24,7 @@ var UpdaterBase = new Class({
self.doUpdate();
else {
App.unBlockPage();
App.on('message', 'No updates available');
App.trigger('message', ['No updates available']);
}
}
})
@@ -66,7 +66,7 @@ var UpdaterBase = new Class({
var changelog = 'https://github.com/'+data.repo_name+'/compare/'+data.version.hash+'...'+data.branch;
if(data.update_version.changelog)
changelog = data.update_version.changelog + '#' + data.version.hash+'...'+data.update_version.hash
changelog = data.update_version.changelog + '#' + data.version.hash+'...'+data.update_version.hash;
self.message = new Element('div.message.update').adopt(
new Element('span', {

View File

@@ -0,0 +1,607 @@
import json
import os
import time
import traceback
from CodernityDB.database import RecordNotFound
from CodernityDB.index import IndexException, IndexNotFoundException, IndexConflict
from couchpotato import CPLog
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import toUnicode, sp
from couchpotato.core.helpers.variable import getImdb, tryInt
log = CPLog(__name__)
class Database(object):
indexes = None
db = None
def __init__(self):
self.indexes = {}
addApiView('database.list_documents', self.listDocuments)
addApiView('database.reindex', self.reindex)
addApiView('database.compact', self.compact)
addApiView('database.document.update', self.updateDocument)
addApiView('database.document.delete', self.deleteDocument)
addEvent('database.setup.after', self.startup_compact)
addEvent('database.setup_index', self.setupIndex)
addEvent('app.migrate', self.migrate)
addEvent('app.after_shutdown', self.close)
def getDB(self):
if not self.db:
from couchpotato import get_db
self.db = get_db()
return self.db
def close(self, **kwargs):
self.getDB().close()
def setupIndex(self, index_name, klass):
self.indexes[index_name] = klass
db = self.getDB()
# Category index
index_instance = klass(db.path, index_name)
try:
# Make sure store and bucket don't exist
exists = []
for x in ['buck', 'stor']:
full_path = os.path.join(db.path, '%s_%s' % (index_name, x))
if os.path.exists(full_path):
exists.append(full_path)
if index_name not in db.indexes_names:
# Remove existing buckets if index isn't there
for x in exists:
os.unlink(x)
# Add index (will restore buckets)
db.add_index(index_instance)
db.reindex_index(index_name)
else:
# Previous info
previous = db.indexes_names[index_name]
previous_version = previous._version
current_version = klass._version
# Only edit index if versions are different
if previous_version < current_version:
log.debug('Index "%s" already exists, updating and reindexing', index_name)
db.destroy_index(previous)
db.add_index(index_instance)
db.reindex_index(index_name)
except:
log.error('Failed adding index %s: %s', (index_name, traceback.format_exc()))
def deleteDocument(self, **kwargs):
db = self.getDB()
try:
document_id = kwargs.get('_request').get_argument('id')
document = db.get('id', document_id)
db.delete(document)
return {
'success': True
}
except:
return {
'success': False,
'error': traceback.format_exc()
}
def updateDocument(self, **kwargs):
db = self.getDB()
try:
document = json.loads(kwargs.get('_request').get_argument('document'))
d = db.update(document)
document.update(d)
return {
'success': True,
'document': document
}
except:
return {
'success': False,
'error': traceback.format_exc()
}
def listDocuments(self, **kwargs):
db = self.getDB()
results = {
'unknown': []
}
for document in db.all('id'):
key = document.get('_t', 'unknown')
if kwargs.get('show') and key != kwargs.get('show'):
continue
if not results.get(key):
results[key] = []
results[key].append(document)
return results
def reindex(self, **kwargs):
success = True
try:
db = self.getDB()
db.reindex()
except:
log.error('Failed index: %s', traceback.format_exc())
success = False
return {
'success': success
}
def compact(self, try_repair = True, **kwargs):
success = False
db = self.getDB()
# Removing left over compact files
db_path = sp(db.path)
for f in os.listdir(sp(db.path)):
for x in ['_compact_buck', '_compact_stor']:
if f[-len(x):] == x:
os.unlink(os.path.join(db_path, f))
try:
start = time.time()
size = float(db.get_db_details().get('size', 0))
log.debug('Compacting database, current size: %sMB', round(size/1048576, 2))
db.compact()
new_size = float(db.get_db_details().get('size', 0))
log.debug('Done compacting database in %ss, new size: %sMB, saved: %sMB', (round(time.time()-start, 2), round(new_size/1048576, 2), round((size-new_size)/1048576, 2)))
success = True
except (IndexException, AttributeError):
if try_repair:
log.error('Something wrong with indexes, trying repair')
# Remove all indexes
old_indexes = self.indexes.keys()
for index_name in old_indexes:
try:
db.destroy_index(index_name)
except IndexNotFoundException:
pass
except:
log.error('Failed removing old index %s', index_name)
# Add them again
for index_name in self.indexes:
klass = self.indexes[index_name]
# Category index
index_instance = klass(db.path, index_name)
try:
db.add_index(index_instance)
db.reindex_index(index_name)
except IndexConflict:
pass
except:
log.error('Failed adding index %s', index_name)
raise
self.compact(try_repair = False)
else:
log.error('Failed compact: %s', traceback.format_exc())
except:
log.error('Failed compact: %s', traceback.format_exc())
return {
'success': success
}
# Compact on start
def startup_compact(self):
from couchpotato import Env
db = self.getDB()
# Try fix for migration failures on desktop
if Env.get('desktop'):
try:
list(db.all('profile', with_doc = True))
except RecordNotFound:
failed_location = '%s_failed' % db.path
old_db = os.path.join(Env.get('data_dir'), 'couchpotato.db.old')
if not os.path.isdir(failed_location) and os.path.isfile(old_db):
log.error('Corrupt database, trying migrate again')
db.close()
# Rename database folder
os.rename(db.path, '%s_failed' % db.path)
# Rename .old database to try another migrate
os.rename(old_db, old_db[:-4])
fireEventAsync('app.restart')
else:
log.error('Migration failed and couldn\'t recover database. Please report on GitHub, with this message.')
db.reindex()
return
# Check size and compact if needed
size = db.get_db_details().get('size')
prop_name = 'last_db_compact'
last_check = int(Env.prop(prop_name, default = 0))
if size > 26214400 and last_check < time.time()-604800: # 25MB / 7 days
self.compact()
Env.prop(prop_name, value = int(time.time()))
def migrate(self):
from couchpotato import Env
old_db = os.path.join(Env.get('data_dir'), 'couchpotato.db')
if not os.path.isfile(old_db): return
log.info('=' * 30)
log.info('Migrating database, hold on..')
time.sleep(1)
if os.path.isfile(old_db):
migrate_start = time.time()
import sqlite3
conn = sqlite3.connect(old_db)
migrate_list = {
'category': ['id', 'label', 'order', 'required', 'preferred', 'ignored', 'destination'],
'profile': ['id', 'label', 'order', 'core', 'hide'],
'profiletype': ['id', 'order', 'finish', 'wait_for', 'quality_id', 'profile_id'],
'quality': ['id', 'identifier', 'order', 'size_min', 'size_max'],
'movie': ['id', 'last_edit', 'library_id', 'status_id', 'profile_id', 'category_id'],
'library': ['id', 'identifier', 'info'],
'librarytitle': ['id', 'title', 'default', 'libraries_id'],
'library_files__file_library': ['library_id', 'file_id'],
'release': ['id', 'identifier', 'movie_id', 'status_id', 'quality_id', 'last_edit'],
'releaseinfo': ['id', 'identifier', 'value', 'release_id'],
'release_files__file_release': ['release_id', 'file_id'],
'status': ['id', 'identifier'],
'properties': ['id', 'identifier', 'value'],
'file': ['id', 'path', 'type_id'],
'filetype': ['identifier', 'id']
}
migrate_data = {}
c = conn.cursor()
for ml in migrate_list:
migrate_data[ml] = {}
rows = migrate_list[ml]
try:
c.execute('SELECT %s FROM `%s`' % ('`' + '`,`'.join(rows) + '`', ml))
except:
# ignore faulty destination_id database
if ml == 'category':
migrate_data[ml] = {}
else:
raise
for p in c.fetchall():
columns = {}
for row in migrate_list[ml]:
columns[row] = p[rows.index(row)]
if not migrate_data[ml].get(p[0]):
migrate_data[ml][p[0]] = columns
else:
if not isinstance(migrate_data[ml][p[0]], list):
migrate_data[ml][p[0]] = [migrate_data[ml][p[0]]]
migrate_data[ml][p[0]].append(columns)
conn.close()
log.info('Getting data took %s', time.time() - migrate_start)
db = self.getDB()
if not db.opened:
return
# Use properties
properties = migrate_data['properties']
log.info('Importing %s properties', len(properties))
for x in properties:
property = properties[x]
Env.prop(property.get('identifier'), property.get('value'))
# Categories
categories = migrate_data.get('category', [])
log.info('Importing %s categories', len(categories))
category_link = {}
for x in categories:
c = categories[x]
new_c = db.insert({
'_t': 'category',
'order': c.get('order', 999),
'label': toUnicode(c.get('label', '')),
'ignored': toUnicode(c.get('ignored', '')),
'preferred': toUnicode(c.get('preferred', '')),
'required': toUnicode(c.get('required', '')),
'destination': toUnicode(c.get('destination', '')),
})
category_link[x] = new_c.get('_id')
# Profiles
log.info('Importing profiles')
new_profiles = db.all('profile', with_doc = True)
new_profiles_by_label = {}
for x in new_profiles:
# Remove default non core profiles
if not x['doc'].get('core'):
db.delete(x['doc'])
else:
new_profiles_by_label[x['doc']['label']] = x['_id']
profiles = migrate_data['profile']
profile_link = {}
for x in profiles:
p = profiles[x]
exists = new_profiles_by_label.get(p.get('label'))
# Update existing with order only
if exists and p.get('core'):
profile = db.get('id', exists)
profile['order'] = tryInt(p.get('order'))
profile['hide'] = p.get('hide') in [1, True, 'true', 'True']
db.update(profile)
profile_link[x] = profile.get('_id')
else:
new_profile = {
'_t': 'profile',
'label': p.get('label'),
'order': int(p.get('order', 999)),
'core': p.get('core', False),
'qualities': [],
'wait_for': [],
'finish': []
}
types = migrate_data['profiletype']
for profile_type in types:
p_type = types[profile_type]
if types[profile_type]['profile_id'] == p['id']:
if p_type['quality_id']:
new_profile['finish'].append(p_type['finish'])
new_profile['wait_for'].append(p_type['wait_for'])
new_profile['qualities'].append(migrate_data['quality'][p_type['quality_id']]['identifier'])
if len(new_profile['qualities']) > 0:
new_profile.update(db.insert(new_profile))
profile_link[x] = new_profile.get('_id')
else:
log.error('Corrupt profile list for "%s", using default.', p.get('label'))
# Qualities
log.info('Importing quality sizes')
new_qualities = db.all('quality', with_doc = True)
new_qualities_by_identifier = {}
for x in new_qualities:
new_qualities_by_identifier[x['doc']['identifier']] = x['_id']
qualities = migrate_data['quality']
quality_link = {}
for x in qualities:
q = qualities[x]
q_id = new_qualities_by_identifier[q.get('identifier')]
quality = db.get('id', q_id)
quality['order'] = q.get('order')
quality['size_min'] = tryInt(q.get('size_min'))
quality['size_max'] = tryInt(q.get('size_max'))
db.update(quality)
quality_link[x] = quality
# Titles
titles = migrate_data['librarytitle']
titles_by_library = {}
for x in titles:
title = titles[x]
if title.get('default'):
titles_by_library[title.get('libraries_id')] = title.get('title')
# Releases
releaseinfos = migrate_data['releaseinfo']
for x in releaseinfos:
info = releaseinfos[x]
# Skip if release doesn't exist for this info
if not migrate_data['release'].get(info.get('release_id')):
continue
if not migrate_data['release'][info.get('release_id')].get('info'):
migrate_data['release'][info.get('release_id')]['info'] = {}
migrate_data['release'][info.get('release_id')]['info'][info.get('identifier')] = info.get('value')
releases = migrate_data['release']
releases_by_media = {}
for x in releases:
release = releases[x]
if not releases_by_media.get(release.get('movie_id')):
releases_by_media[release.get('movie_id')] = []
releases_by_media[release.get('movie_id')].append(release)
# Type ids
types = migrate_data['filetype']
type_by_id = {}
for t in types:
type = types[t]
type_by_id[type.get('id')] = type
# Media
log.info('Importing %s media items', len(migrate_data['movie']))
statuses = migrate_data['status']
libraries = migrate_data['library']
library_files = migrate_data['library_files__file_library']
releases_files = migrate_data['release_files__file_release']
all_files = migrate_data['file']
poster_type = migrate_data['filetype']['poster']
medias = migrate_data['movie']
for x in medias:
m = medias[x]
status = statuses.get(m['status_id']).get('identifier')
l = libraries.get(m['library_id'])
# Only migrate wanted movies, Skip if no identifier present
if not l or not getImdb(l.get('identifier')): continue
profile_id = profile_link.get(m['profile_id'])
category_id = category_link.get(m['category_id'])
title = titles_by_library.get(m['library_id'])
releases = releases_by_media.get(x, [])
info = json.loads(l.get('info', ''))
files = library_files.get(m['library_id'], [])
if not isinstance(files, list):
files = [files]
added_media = fireEvent('movie.add', {
'info': info,
'identifier': l.get('identifier'),
'profile_id': profile_id,
'category_id': category_id,
'title': title
}, force_readd = False, search_after = False, update_after = False, notify_after = False, status = status, single = True)
if not added_media:
log.error('Failed adding media %s: %s', (l.get('identifier'), info))
continue
added_media['files'] = added_media.get('files', {})
for f in files:
ffile = all_files[f.get('file_id')]
# Only migrate posters
if ffile.get('type_id') == poster_type.get('id'):
if ffile.get('path') not in added_media['files'].get('image_poster', []) and os.path.isfile(ffile.get('path')):
added_media['files']['image_poster'] = [ffile.get('path')]
break
if 'image_poster' in added_media['files']:
db.update(added_media)
for rel in releases:
empty_info = False
if not rel.get('info'):
empty_info = True
rel['info'] = {}
quality = quality_link.get(rel.get('quality_id'))
if not quality:
continue
release_status = statuses.get(rel.get('status_id')).get('identifier')
if rel['info'].get('download_id'):
status_support = rel['info'].get('download_status_support', False) in [True, 'true', 'True']
rel['info']['download_info'] = {
'id': rel['info'].get('download_id'),
'downloader': rel['info'].get('download_downloader'),
'status_support': status_support,
}
# Add status to keys
rel['info']['status'] = release_status
if not empty_info:
fireEvent('release.create_from_search', [rel['info']], added_media, quality, single = True)
else:
release = {
'_t': 'release',
'identifier': rel.get('identifier'),
'media_id': added_media.get('_id'),
'quality': quality.get('identifier'),
'status': release_status,
'last_edit': int(time.time()),
'files': {}
}
# Add downloader info if provided
try:
release['download_info'] = rel['info']['download_info']
del rel['download_info']
except:
pass
# Add files
release_files = releases_files.get(rel.get('id'), [])
if not isinstance(release_files, list):
release_files = [release_files]
if len(release_files) == 0:
continue
for f in release_files:
rfile = all_files[f.get('file_id')]
file_type = type_by_id.get(rfile.get('type_id')).get('identifier')
if not release['files'].get(file_type):
release['files'][file_type] = []
release['files'][file_type].append(rfile.get('path'))
try:
rls = db.get('release_identifier', rel.get('identifier'), with_doc = True)['doc']
rls.update(release)
db.update(rls)
except:
db.insert(release)
log.info('Total migration took %s', time.time() - migrate_start)
log.info('=' * 30)
# rename old database
log.info('Renaming old database to %s ', old_db + '.old')
os.rename(old_db, old_db + '.old')
if os.path.isfile(old_db + '-wal'):
os.rename(old_db + '-wal', old_db + '-wal.old')
if os.path.isfile(old_db + '-shm'):
os.rename(old_db + '-shm', old_db + '-shm.old')

View File

@@ -1,13 +0,0 @@
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,14 +1,20 @@
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
from couchpotato.core._base.downloader.main import DownloaderBase
from couchpotato.core.helpers.encoding import sp
from couchpotato.core.helpers.variable import getDownloadDir
from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
log = CPLog(__name__)
autoload = 'Blackhole'
class Blackhole(Downloader):
class Blackhole(DownloaderBase):
protocol = ['nzb', 'torrent', 'torrent_magnet']
status_support = False
@@ -67,6 +73,20 @@ class Blackhole(Downloader):
return False
def test(self):
directory = self.conf('directory')
if directory and os.path.isdir(directory):
test_file = sp(os.path.join(directory, 'couchpotato_test.txt'))
# Check if folder is writable
self.createFile(test_file, 'This is a test file')
if os.path.isfile(test_file):
os.remove(test_file)
return True
return False
def getEnabledProtocol(self):
if self.conf('use_for') == 'both':
return super(Blackhole, self).getEnabledProtocol()
@@ -85,3 +105,54 @@ class Blackhole(Downloader):
return super(Blackhole, self).isEnabled(manual, data) and \
((self.conf('use_for') in for_protocol))
config = [{
'name': 'blackhole',
'order': 30,
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'blackhole',
'label': 'Black hole',
'description': 'Download the NZB/Torrent to a specific folder. <em>Note: Seeding and copying/linking features do <strong>not</strong> work with Black hole</em>.',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': True,
'type': 'enabler',
'radio_group': 'nzb,torrent',
},
{
'name': 'directory',
'type': 'directory',
'description': 'Directory where the .nzb (or .torrent) file is saved to.',
'default': getDownloadDir()
},
{
'name': 'use_for',
'label': 'Use for',
'default': 'both',
'type': 'dropdown',
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
},
{
'name': 'create_subdir',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Create a sub directory when saving the .nzb (or .torrent).',
},
{
'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

@@ -1,55 +0,0 @@
from .main import Blackhole
from couchpotato.core.helpers.variable import getDownloadDir
def start():
return Blackhole()
config = [{
'name': 'blackhole',
'order': 30,
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'blackhole',
'label': 'Black hole',
'description': 'Download the NZB/Torrent to a specific folder. <em>Note: Seeding and copying/linking features do <strong>not</strong> work with Black hole</em>.',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': True,
'type': 'enabler',
'radio_group': 'nzb,torrent',
},
{
'name': 'directory',
'type': 'directory',
'description': 'Directory where the .nzb (or .torrent) file is saved to.',
'default': getDownloadDir()
},
{
'name': 'use_for',
'label': 'Use for',
'default': 'both',
'type': 'dropdown',
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
},
{
'name': 'create_subdir',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Create a sub directory when saving the .nzb (or .torrent).',
},
{
'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

@@ -1,33 +1,37 @@
from base64 import b64encode, b16encode, b32decode
from bencode import bencode as benc, bdecode
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
from couchpotato.core.helpers.encoding import isInt, sp
from couchpotato.core.helpers.variable import tryFloat
from couchpotato.core.logger import CPLog
from datetime import timedelta
from hashlib import sha1
from synchronousdeluge import DelugeClient
import os.path
import re
import traceback
from bencode import bencode as benc, bdecode
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
from couchpotato.core.helpers.encoding import isInt, sp
from couchpotato.core.helpers.variable import tryFloat, cleanHost
from couchpotato.core.logger import CPLog
from synchronousdeluge import DelugeClient
log = CPLog(__name__)
autoload = 'Deluge'
class Deluge(Downloader):
class Deluge(DownloaderBase):
protocol = ['torrent', 'torrent_magnet']
log = CPLog(__name__)
drpc = None
def connect(self):
def connect(self, reconnect = False):
# Load host from config and split out port.
host = self.conf('host').split(':')
host = cleanHost(self.conf('host'), protocol = False).split(':')
if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.')
return False
if not self.drpc:
if not self.drpc or reconnect:
self.drpc = DelugeRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
return self.drpc
@@ -86,6 +90,11 @@ class Deluge(Downloader):
log.info('Torrent sent to Deluge successfully.')
return self.downloadReturnId(remote_torrent)
def test(self):
if self.connect(True) and self.drpc.test():
return True
return False
def getAllDownloadStatus(self, ids):
log.debug('Checking Deluge download status.')
@@ -103,8 +112,13 @@ class Deluge(Downloader):
for torrent_id in queue:
torrent = queue[torrent_id]
log.debug('name=%s / id=%s / save_path=%s / move_completed_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / stop_ratio=%s / is_seed=%s / is_finished=%s / paused=%s', (torrent['name'], torrent['hash'], torrent['save_path'], torrent['move_completed_path'], torrent['hash'], torrent['progress'], torrent['state'], torrent['eta'], torrent['ratio'], torrent['stop_ratio'], torrent['is_seed'], torrent['is_finished'], torrent['paused']))
if not 'hash' in torrent:
# When given a list of ids, deluge will return an empty item for a non-existant torrent.
continue
log.debug('name=%s / id=%s / save_path=%s / move_on_completed=%s / move_completed_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / stop_ratio=%s / is_seed=%s / is_finished=%s / paused=%s', (torrent['name'], torrent['hash'], torrent['save_path'], torrent['move_on_completed'], torrent['move_completed_path'], torrent['hash'], torrent['progress'], torrent['state'], torrent['eta'], torrent['ratio'], torrent['stop_ratio'], torrent['is_seed'], torrent['is_finished'], torrent['paused']))
# Deluge has no easy way to work out if a torrent is stalled or failing.
#status = 'failed'
status = 'busy'
@@ -120,11 +134,11 @@ class Deluge(Downloader):
download_dir = sp(torrent['save_path'])
if torrent['move_on_completed']:
download_dir = torrent['move_completed_path']
torrent_files = []
for file_item in torrent['files']:
torrent_files.append(sp(os.path.join(download_dir, file_item['path'])))
release_downloads.append({
'id': torrent['hash'],
'name': torrent['name'],
@@ -133,7 +147,7 @@ class Deluge(Downloader):
'seed_ratio': torrent['ratio'],
'timeleft': str(timedelta(seconds = torrent['eta'])),
'folder': sp(download_dir if len(torrent_files) == 1 else os.path.join(download_dir, torrent['name'])),
'files': '|'.join(torrent_files),
'files': torrent_files,
})
return release_downloads
@@ -152,6 +166,7 @@ class Deluge(Downloader):
log.debug('Requesting Deluge to remove the torrent %s%s.', (release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
return self.drpc.remove_torrent(release_download['id'], remove_local_data = delete_files)
class DelugeRPC(object):
host = 'localhost'
@@ -172,6 +187,13 @@ class DelugeRPC(object):
self.client = DelugeClient()
self.client.connect(self.host, int(self.port), self.username, self.password)
def test(self):
try:
self.connect()
except:
return False
return True
def add_torrent_magnet(self, torrent, options):
torrent_id = False
try:
@@ -182,7 +204,7 @@ class DelugeRPC(object):
if torrent_id and options['label']:
self.client.label.set_torrent(torrent_id, options['label']).get()
except Exception, err:
except Exception as err:
log.error('Failed to add torrent magnet %s: %s %s', (torrent, err, traceback.format_exc()))
finally:
if self.client:
@@ -200,7 +222,7 @@ class DelugeRPC(object):
if torrent_id and options['label']:
self.client.label.set_torrent(torrent_id, options['label']).get()
except Exception, err:
except Exception as err:
log.error('Failed to add torrent file %s: %s %s', (filename, err, traceback.format_exc()))
finally:
if self.client:
@@ -212,8 +234,8 @@ class DelugeRPC(object):
ret = False
try:
self.connect()
ret = self.client.core.get_torrents_status({'id': ids}, {}).get()
except Exception, err:
ret = self.client.core.get_torrents_status({'id': ids}, ('name', 'hash', 'save_path', 'move_completed_path', 'progress', 'state', 'eta', 'ratio', 'stop_ratio', 'is_seed', 'is_finished', 'paused', 'move_on_completed', 'files')).get()
except Exception as err:
log.error('Failed to get all torrents: %s %s', (err, traceback.format_exc()))
finally:
if self.client:
@@ -224,7 +246,7 @@ class DelugeRPC(object):
try:
self.connect()
self.client.core.pause_torrent(torrent_ids).get()
except Exception, err:
except Exception as err:
log.error('Failed to pause torrent: %s %s', (err, traceback.format_exc()))
finally:
if self.client:
@@ -234,7 +256,7 @@ class DelugeRPC(object):
try:
self.connect()
self.client.core.resume_torrent(torrent_ids).get()
except Exception, err:
except Exception as err:
log.error('Failed to resume torrent: %s %s', (err, traceback.format_exc()))
finally:
if self.client:
@@ -245,7 +267,7 @@ class DelugeRPC(object):
try:
self.connect()
ret = self.client.core.remove_torrent(torrent_id, remove_local_data).get()
except Exception, err:
except Exception as err:
log.error('Failed to remove torrent: %s %s', (err, traceback.format_exc()))
finally:
if self.client:
@@ -273,3 +295,90 @@ class DelugeRPC(object):
return torrent_hash
return False
config = [{
'name': 'deluge',
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'deluge',
'label': 'Deluge',
'description': 'Use <a href="http://www.deluge-torrent.org/" target="_blank">Deluge</a> to download torrents.',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
'radio_group': 'torrent',
},
{
'name': 'host',
'default': 'localhost:58846',
'description': 'Hostname with port. Usually <strong>localhost:58846</strong>',
},
{
'name': 'username',
},
{
'name': 'password',
'type': 'password',
},
{
'name': 'directory',
'type': 'directory',
'description': 'Download to this directory. Keep empty for default Deluge download directory.',
},
{
'name': 'completed_directory',
'type': 'directory',
'description': 'Move completed torrent to this directory. Keep empty for default Deluge options.',
'advanced': True,
},
{
'name': 'label',
'description': 'Label to add to torrents in the Deluge UI.',
},
{
'name': 'remove_complete',
'label': 'Remove torrent',
'type': 'bool',
'default': True,
'advanced': True,
'description': 'Remove the torrent from Deluge after it has finished seeding.',
},
{
'name': 'delete_files',
'label': 'Remove files',
'default': True,
'type': 'bool',
'advanced': True,
'description': 'Also remove the leftover files.',
},
{
'name': 'paused',
'type': 'bool',
'advanced': True,
'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.',
},
{
'name': 'delete_failed',
'default': True,
'advanced': True,
'type': 'bool',
'description': 'Delete a release after the download has failed.',
},
],
}
],
}]

View File

@@ -1,90 +0,0 @@
from .main import Deluge
def start():
return Deluge()
config = [{
'name': 'deluge',
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'deluge',
'label': 'Deluge',
'description': 'Use <a href="http://www.deluge-torrent.org/" target="_blank">Deluge</a> to download torrents.',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
'radio_group': 'torrent',
},
{
'name': 'host',
'default': 'localhost:58846',
'description': 'Hostname with port. Usually <strong>localhost:58846</strong>',
},
{
'name': 'username',
},
{
'name': 'password',
'type': 'password',
},
{
'name': 'directory',
'type': 'directory',
'description': 'Download to this directory. Keep empty for default Deluge download directory.',
},
{
'name': 'completed_directory',
'type': 'directory',
'description': 'Move completed torrent to this directory. Keep empty for default Deluge options.',
'advanced': True,
},
{
'name': 'label',
'description': 'Label to add to torrents in the Deluge UI.',
},
{
'name': 'remove_complete',
'label': 'Remove torrent',
'type': 'bool',
'default': True,
'advanced': True,
'description': 'Remove the torrent from Deluge after it has finished seeding.',
},
{
'name': 'delete_files',
'label': 'Remove files',
'default': True,
'type': 'bool',
'advanced': True,
'description': 'Also remove the leftover files.',
},
{
'name': 'paused',
'type': 'bool',
'advanced': True,
'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.',
},
{
'name': 'delete_failed',
'default': True,
'advanced': True,
'type': 'bool',
'description': 'Delete a release after the download has failed.',
},
],
}
],
}]

View File

@@ -1,8 +1,4 @@
from base64 import standard_b64encode
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
from couchpotato.core.helpers.encoding import ss, sp
from couchpotato.core.helpers.variable import tryInt, md5
from couchpotato.core.logger import CPLog
from datetime import timedelta
import re
import shutil
@@ -10,14 +6,21 @@ import socket
import traceback
import xmlrpclib
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
from couchpotato.core.helpers.encoding import ss, sp
from couchpotato.core.helpers.variable import tryInt, md5, cleanHost
from couchpotato.core.logger import CPLog
log = CPLog(__name__)
autoload = 'NZBGet'
class NZBGet(Downloader):
class NZBGet(DownloaderBase):
protocol = ['nzb']
url = '%(protocol)s://%(username)s:%(password)s@%(host)s/xmlrpc'
rpc = 'xmlrpc'
def download(self, data = None, media = None, filedata = None):
if not media: media = {}
@@ -29,10 +32,10 @@ class NZBGet(Downloader):
log.info('Sending "%s" to NZBGet.', data.get('name'))
url = self.url % {'protocol': 'https' if self.conf('ssl') else 'http', 'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
nzb_name = ss('%s.nzb' % self.createNzbName(data, media))
rpc = xmlrpclib.ServerProxy(url)
rpc = self.getRPC()
try:
if rpc.writelog('INFO', 'CouchPotato connected to drop off %s.' % nzb_name):
log.debug('Successfully connected to NZBGet')
@@ -41,7 +44,7 @@ class NZBGet(Downloader):
except socket.error:
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
return False
except xmlrpclib.ProtocolError, e:
except xmlrpclib.ProtocolError as e:
if e.errcode == 401:
log.error('Password is incorrect.')
else:
@@ -55,7 +58,7 @@ class NZBGet(Downloader):
if xml_response:
log.info('NZB sent successfully to NZBGet')
nzb_id = md5(data['url']) # about as unique as they come ;)
nzb_id = md5(data['url']) # about as unique as they come ;)
couchpotato_id = "couchpotato=" + nzb_id
groups = rpc.listgroups()
file_id = [item['LastID'] for item in groups if item['NZBFilename'] == nzb_name]
@@ -67,13 +70,32 @@ class NZBGet(Downloader):
log.error('NZBGet could not add %s to the queue.', nzb_name)
return False
def test(self):
rpc = self.getRPC()
try:
if rpc.writelog('INFO', 'CouchPotato connected to test connection'):
log.debug('Successfully connected to NZBGet')
else:
log.info('Successfully connected to NZBGet, but unable to send a message')
except socket.error:
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
return False
except xmlrpclib.ProtocolError as e:
if e.errcode == 401:
log.error('Password is incorrect.')
else:
log.error('Protocol Error: %s', e)
return False
return True
def getAllDownloadStatus(self, ids):
log.debug('Checking NZBGet download status.')
url = self.url % {'protocol': 'https' if self.conf('ssl') else 'http', 'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
rpc = self.getRPC()
rpc = xmlrpclib.ServerProxy(url)
try:
if rpc.writelog('INFO', 'CouchPotato connected to check status'):
log.debug('Successfully connected to NZBGet')
@@ -82,7 +104,7 @@ class NZBGet(Downloader):
except socket.error:
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
return []
except xmlrpclib.ProtocolError, e:
except xmlrpclib.ProtocolError as e:
if e.errcode == 401:
log.error('Password is incorrect.')
else:
@@ -115,7 +137,7 @@ class NZBGet(Downloader):
timeleft = str(timedelta(seconds = nzb['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20))
except:
pass
release_downloads.append({
'id': nzb_id,
'name': nzb['NZBFilename'],
@@ -124,7 +146,7 @@ class NZBGet(Downloader):
'timeleft': timeleft,
})
for nzb in queue: # 'Parameters' is not passed in rpc.postqueue
for nzb in queue: # 'Parameters' is not passed in rpc.postqueue
if nzb['NZBID'] in ids:
log.debug('Found %s in NZBGet postprocessing queue', nzb['NZBFilename'])
release_downloads.append({
@@ -157,9 +179,8 @@ class NZBGet(Downloader):
log.info('%s failed downloading, deleting...', release_download['name'])
url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
rpc = self.getRPC()
rpc = xmlrpclib.ServerProxy(url)
try:
if rpc.writelog('INFO', 'CouchPotato connected to delete some history'):
log.debug('Successfully connected to NZBGet')
@@ -168,7 +189,7 @@ class NZBGet(Downloader):
except socket.error:
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
return False
except xmlrpclib.ProtocolError, e:
except xmlrpclib.ProtocolError as e:
if e.errcode == 401:
log.error('Password is incorrect.')
else:
@@ -193,3 +214,80 @@ class NZBGet(Downloader):
return False
return True
def getRPC(self):
url = cleanHost(host = self.conf('host'), ssl = self.conf('ssl'), username = self.conf('username'), password = self.conf('password')) + self.rpc
return xmlrpclib.ServerProxy(url)
config = [{
'name': 'nzbget',
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'nzbget',
'label': 'NZBGet',
'description': 'Use <a href="http://nzbget.sourceforge.net/Main_Page" target="_blank">NZBGet</a> to download NZBs.',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
'radio_group': 'nzb',
},
{
'name': 'host',
'default': 'localhost:6789',
'description': 'Hostname with port. Usually <strong>localhost:6789</strong>',
},
{
'name': 'ssl',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
},
{
'name': 'username',
'default': 'nzbget',
'advanced': True,
'description': 'Set a different username to connect. Default: nzbget',
},
{
'name': 'password',
'type': 'password',
'description': 'Default NZBGet password is <i>tegbzn6789</i>',
},
{
'name': 'category',
'default': 'Movies',
'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
},
{
'name': 'priority',
'advanced': True,
'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,
'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,
'advanced': True,
'type': 'bool',
'description': 'Delete a release after the download has failed.',
},
],
}
],
}]

View File

@@ -1,76 +0,0 @@
from .main import NZBGet
def start():
return NZBGet()
config = [{
'name': 'nzbget',
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'nzbget',
'label': 'NZBGet',
'description': 'Use <a href="http://nzbget.sourceforge.net/Main_Page" target="_blank">NZBGet</a> to download NZBs.',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
'radio_group': 'nzb',
},
{
'name': 'host',
'default': 'localhost:6789',
'description': 'Hostname with port. Usually <strong>localhost:6789</strong>',
},
{
'name': 'ssl',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
},
{
'name': 'username',
'default': 'nzbget',
'advanced': True,
'description': 'Set a different username to connect. Default: nzbget',
},
{
'name': 'password',
'type': 'password',
'description': 'Default NZBGet password is <i>tegbzn6789</i>',
},
{
'name': 'category',
'default': 'Movies',
'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
},
{
'name': 'priority',
'advanced': True,
'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,
'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,
'advanced': True,
'type': 'bool',
'description': 'Delete a release after the download has failed.',
},
],
}
],
}]

View File

@@ -1,8 +1,4 @@
from base64 import b64encode
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
from couchpotato.core.helpers.encoding import tryUrlencode, sp
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog
from urllib2 import URLError
from uuid import uuid4
import hashlib
@@ -16,10 +12,18 @@ import time
import traceback
import urllib2
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
from couchpotato.core.helpers.encoding import tryUrlencode, sp
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog
log = CPLog(__name__)
autoload = 'NZBVortex'
class NZBVortex(Downloader):
class NZBVortex(DownloaderBase):
protocol = ['nzb']
api_level = None
@@ -36,12 +40,20 @@ class NZBVortex(Downloader):
time.sleep(10)
raw_statuses = self.call('nzb')
nzb_id = [nzb['id'] for nzb in raw_statuses.get('nzbs', []) if os.path.basename(item['nzbFileName']) == nzb_filename][0]
nzb_id = [nzb['id'] for nzb in raw_statuses.get('nzbs', []) if os.path.basename(nzb['nzbFileName']) == nzb_filename][0]
return self.downloadReturnId(nzb_id)
except:
log.error('Something went wrong sending the NZB file: %s', traceback.format_exc())
return False
def test(self):
try:
login_result = self.login()
except:
return False
return login_result
def getAllDownloadStatus(self, ids):
raw_statuses = self.call('nzb')
@@ -56,13 +68,13 @@ class NZBVortex(Downloader):
status = 'completed'
elif nzb['state'] in [21, 22, 24]:
status = 'failed'
release_downloads.append({
'id': nzb['id'],
'name': nzb['uiTitle'],
'status': status,
'original_status': nzb['state'],
'timeleft':-1,
'timeleft': -1,
'folder': sp(nzb['destinationPath']),
})
@@ -102,7 +114,6 @@ class NZBVortex(Downloader):
log.error('Login failed, please check you api-key')
return False
def call(self, call, parameters = None, repeat = False, auth = True, *args, **kwargs):
# Login first
@@ -116,14 +127,14 @@ class NZBVortex(Downloader):
params = tryUrlencode(parameters)
url = cleanHost(self.conf('host')) + 'api/' + call
url = cleanHost(self.conf('host'), ssl = self.conf('ssl')) + 'api/' + call
try:
data = self.urlopen('%s?%s' % (url, params), *args, **kwargs)
if data:
return json.loads(data)
except URLError, e:
except URLError as e:
if hasattr(e, 'code') and e.code == 403:
# Try login and do again
if not repeat:
@@ -145,7 +156,7 @@ class NZBVortex(Downloader):
try:
data = self.urlopen(url, show_error = False)
self.api_level = float(json.loads(data).get('apilevel'))
except URLError, e:
except URLError as 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:
@@ -175,6 +186,60 @@ class HTTPSConnection(httplib.HTTPSConnection):
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)
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': 'localhost:4321',
'description': 'Hostname with port. Usually <strong>localhost:4321</strong>',
},
{
'name': 'ssl',
'default': 1,
'type': 'bool',
'advanced': True,
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
},
{
'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,
'advanced': True,
'type': 'bool',
'description': 'Delete a release after the download has failed.',
},
],
}
],
}]

View File

@@ -1,48 +0,0 @@
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,
'advanced': True,
'type': 'bool',
'description': 'Delete a release after the download has failed.',
},
],
}
],
}]

View File

@@ -0,0 +1,111 @@
from __future__ import with_statement
import os
import traceback
from couchpotato.core._base.downloader.main import DownloaderBase
from couchpotato.core.helpers.encoding import sp
from couchpotato.core.logger import CPLog
log = CPLog(__name__)
autoload = 'Pneumatic'
class Pneumatic(DownloaderBase):
protocol = ['nzb']
strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s'
status_support = False
def download(self, data = None, media = None, filedata = None):
if not media: media = {}
if not data: data = {}
directory = self.conf('directory')
if not directory or not os.path.isdir(directory):
log.error('No directory set for .strm downloads.')
else:
try:
if not filedata or len(filedata) < 50:
log.error('No nzb available!')
return False
full_path = os.path.join(directory, self.createFileName(data, filedata, media))
try:
if not os.path.isfile(full_path):
log.info('Downloading %s to %s.', (data.get('protocol'), full_path))
with open(full_path, 'wb') as f:
f.write(filedata)
nzb_name = self.createNzbName(data, media)
strm_path = os.path.join(directory, nzb_name)
strm_file = open(strm_path + '.strm', 'wb')
strmContent = self.strm_syntax % (full_path, nzb_name)
strm_file.write(strmContent)
strm_file.close()
return self.downloadReturnId('')
else:
log.info('File %s already exists.', full_path)
return self.downloadReturnId('')
except:
log.error('Failed to download .strm: %s', traceback.format_exc())
pass
except:
log.info('Failed to download file %s: %s', (data.get('name'), traceback.format_exc()))
return False
return False
def test(self):
directory = self.conf('directory')
if directory and os.path.isdir(directory):
test_file = sp(os.path.join(directory, 'couchpotato_test.txt'))
# Check if folder is writable
self.createFile(test_file, 'This is a test file')
if os.path.isfile(test_file):
os.remove(test_file)
return True
return False
config = [{
'name': 'pneumatic',
'order': 30,
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'pneumatic',
'label': 'Pneumatic',
'description': 'Use <a href="http://forum.xbmc.org/showthread.php?tid=97657" target="_blank">Pneumatic</a> to download .strm files.',
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
},
{
'name': 'directory',
'type': 'directory',
'description': 'Directory where the .strm file is saved to.',
},
{
'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

@@ -1,37 +0,0 @@
from .main import Pneumatic
def start():
return Pneumatic()
config = [{
'name': 'pneumatic',
'order': 30,
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'pneumatic',
'label': 'Pneumatic',
'description': 'Use <a href="http://forum.xbmc.org/showthread.php?tid=97657" target="_blank">Pneumatic</a> to download .strm files.',
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
},
{
'name': 'directory',
'type': 'directory',
'description': 'Directory where the .strm file is saved to.',
},
{
'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

@@ -1,58 +0,0 @@
from __future__ import with_statement
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.logger import CPLog
import os
import traceback
log = CPLog(__name__)
class Pneumatic(Downloader):
protocol = ['nzb']
strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s'
status_support = False
def download(self, data = None, media = None, filedata = None):
if not media: media = {}
if not data: data = {}
directory = self.conf('directory')
if not directory or not os.path.isdir(directory):
log.error('No directory set for .strm downloads.')
else:
try:
if not filedata or len(filedata) < 50:
log.error('No nzb available!')
return False
fullPath = os.path.join(directory, self.createFileName(data, filedata, media))
try:
if not os.path.isfile(fullPath):
log.info('Downloading %s to %s.', (data.get('protocol'), fullPath))
with open(fullPath, 'wb') as f:
f.write(filedata)
nzb_name = self.createNzbName(data, media)
strm_path = os.path.join(directory, nzb_name)
strm_file = open(strm_path + '.strm', 'wb')
strmContent = self.strm_syntax % (fullPath, nzb_name)
strm_file.write(strmContent)
strm_file.close()
return self.downloadReturnId('')
else:
log.info('File %s already exists.', fullPath)
return self.downloadReturnId('')
except:
log.error('Failed to download .strm: %s', traceback.format_exc())
pass
except:
log.info('Failed to download file %s: %s', (data.get('name'), traceback.format_exc()))
return False
return False

View File

@@ -0,0 +1,245 @@
from base64 import b16encode, b32decode
from hashlib import sha1
import os
from bencode import bencode, bdecode
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
from couchpotato.core.helpers.encoding import sp
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog
from qbittorrent.client import QBittorrentClient
log = CPLog(__name__)
autoload = 'qBittorrent'
class qBittorrent(DownloaderBase):
protocol = ['torrent', 'torrent_magnet']
qb = None
def __init__(self):
super(qBittorrent, self).__init__()
def connect(self):
if self.qb is not None:
return self.qb
url = cleanHost(self.conf('host'), protocol = True, ssl = False)
if self.conf('username') and self.conf('password'):
self.qb = QBittorrentClient(
url,
username = self.conf('username'),
password = self.conf('password')
)
else:
self.qb = QBittorrentClient(url)
return self.qb
def test(self):
if self.connect():
return True
return False
def download(self, data = None, media = None, filedata = None):
if not media: media = {}
if not data: data = {}
log.debug('Sending "%s" to qBittorrent.', (data.get('name')))
if not self.connect():
return False
if not filedata and data.get('protocol') == 'torrent':
log.error('Failed sending torrent, no data')
return False
if data.get('protocol') == 'torrent_magnet':
filedata = self.magnetToTorrent(data.get('url'))
if filedata is False:
return False
data['protocol'] = 'torrent'
info = bdecode(filedata)["info"]
torrent_hash = sha1(bencode(info)).hexdigest()
# Convert base 32 to hex
if len(torrent_hash) == 32:
torrent_hash = b16encode(b32decode(torrent_hash))
# Send request to qBittorrent
try:
self.qb.add_file(filedata)
return self.downloadReturnId(torrent_hash)
except Exception as e:
log.error('Failed to send torrent to qBittorrent: %s', e)
return False
def getTorrentStatus(self, torrent):
if torrent.state in ('uploading', 'queuedUP', 'stalledUP'):
return 'seeding'
if torrent.progress == 1:
return 'completed'
return 'busy'
def getAllDownloadStatus(self, ids):
log.debug('Checking qBittorrent download status.')
if not self.connect():
return []
try:
torrents = self.qb.get_torrents()
release_downloads = ReleaseDownloadList(self)
for torrent in torrents:
if torrent.hash in ids:
torrent.update_general() # get extra info
torrent_filelist = torrent.get_files()
torrent_files = []
torrent_dir = os.path.join(torrent.save_path, torrent.name)
if os.path.isdir(torrent_dir):
torrent.save_path = torrent_dir
if len(torrent_filelist) > 1 and os.path.isdir(torrent_dir): # multi file torrent, path.isdir check makes sure we're not in the root download folder
for root, _, files in os.walk(torrent.save_path):
for f in files:
torrent_files.append(sp(os.path.join(root, f)))
else: # multi or single file placed directly in torrent.save_path
for f in torrent_filelist:
file_path = os.path.join(torrent.save_path, f.name)
if os.path.isfile(file_path):
torrent_files.append(sp(file_path))
release_downloads.append({
'id': torrent.hash,
'name': torrent.name,
'status': self.getTorrentStatus(torrent),
'seed_ratio': torrent.ratio,
'original_status': torrent.state,
'timeleft': torrent.progress * 100 if torrent.progress else -1, # percentage
'folder': sp(torrent.save_path),
'files': torrent_files
})
return release_downloads
except Exception as e:
log.error('Failed to get status from qBittorrent: %s', e)
return []
def pause(self, release_download, pause = True):
if not self.connect():
return False
torrent = self.qb.get_torrent(release_download['id'])
if torrent is None:
return False
if pause:
return torrent.pause()
return torrent.resume()
def removeFailed(self, release_download):
log.info('%s failed downloading, deleting...', release_download['name'])
return self.processComplete(release_download, delete_files = True)
def processComplete(self, release_download, delete_files):
log.debug('Requesting qBittorrent to remove the torrent %s%s.',
(release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
if not self.connect():
return False
torrent = self.qb.find_torrent(release_download['id'])
if torrent is None:
return False
if delete_files:
torrent.delete() # deletes torrent with data
else:
torrent.remove() # just removes the torrent, doesn't delete data
return True
config = [{
'name': 'qbittorrent',
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'qbittorrent',
'label': 'qbittorrent',
'description': '',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
'radio_group': 'torrent',
},
{
'name': 'host',
'default': 'http://localhost:8080/',
'description': 'RPC Communication URI. Usually <strong>http://localhost:8080/</strong>'
},
{
'name': 'username',
},
{
'name': 'password',
'type': 'password',
},
{
'name': 'remove_complete',
'label': 'Remove torrent',
'default': False,
'advanced': True,
'type': 'bool',
'description': 'Remove the torrent after it finishes seeding.',
},
{
'name': 'delete_files',
'label': 'Remove files',
'default': True,
'type': 'bool',
'advanced': True,
'description': 'Also remove the leftover files.',
},
{
'name': 'paused',
'type': 'bool',
'advanced': True,
'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

@@ -1,78 +0,0 @@
from .main import rTorrent
def start():
return rTorrent()
config = [{
'name': 'rtorrent',
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'rtorrent',
'label': 'rTorrent',
'description': '',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
'radio_group': 'torrent',
},
{
'name': 'url',
'default': 'http://localhost:80/RPC2',
'description': 'XML-RPC Endpoint URI. Usually <strong>scgi://localhost:5000</strong> '
'or <strong>http://localhost:80/RPC2</strong>'
},
{
'name': 'username',
},
{
'name': 'password',
'type': 'password',
},
{
'name': 'label',
'description': 'Label to apply on added torrents.',
},
{
'name': 'directory',
'type': 'directory',
'description': 'Download to this directory. Keep empty for default rTorrent download directory.',
},
{
'name': 'remove_complete',
'label': 'Remove torrent',
'default': False,
'advanced': True,
'type': 'bool',
'description': 'Remove the torrent after it finishes seeding.',
},
{
'name': 'delete_files',
'label': 'Remove files',
'default': True,
'type': 'bool',
'advanced': True,
'description': 'Also remove the leftover files.',
},
{
'name': 'paused',
'type': 'bool',
'advanced': True,
'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

@@ -1,229 +0,0 @@
from base64 import b16encode, b32decode
from bencode import bencode, bdecode
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
from couchpotato.core.helpers.encoding import sp
from couchpotato.core.logger import CPLog
from datetime import timedelta
from hashlib import sha1
from rtorrent import RTorrent
from rtorrent.err import MethodError
import os
log = CPLog(__name__)
class rTorrent(Downloader):
protocol = ['torrent', 'torrent_magnet']
rt = None
def connect(self):
# Already connected?
if self.rt is not None:
return self.rt
# Ensure url is set
if not self.conf('url'):
log.error('Config properties are not filled in correctly, url is missing.')
return False
if self.conf('username') and self.conf('password'):
self.rt = RTorrent(
self.conf('url'),
self.conf('username'),
self.conf('password')
)
else:
self.rt = RTorrent(self.conf('url'))
return self.rt
def _update_provider_group(self, name, data):
if data.get('seed_time'):
log.info('seeding time ignored, not supported')
if not name:
return False
if not self.connect():
return False
views = self.rt.get_views()
if name not in views:
self.rt.create_group(name)
group = self.rt.get_group(name)
try:
if data.get('seed_ratio'):
ratio = int(float(data.get('seed_ratio')) * 100)
log.debug('Updating provider ratio to %s, group name: %s', (ratio, name))
# Explicitly set all group options to ensure it is setup correctly
group.set_upload('1M')
group.set_min(ratio)
group.set_max(ratio)
group.set_command('d.stop')
group.enable()
else:
# Reset group action and disable it
group.set_command()
group.disable()
except MethodError, err:
log.error('Unable to set group options: %s', err.msg)
return False
return True
def download(self, data = None, media = None, filedata = None):
if not media: media = {}
if not data: data = {}
log.debug('Sending "%s" to rTorrent.', (data.get('name')))
if not self.connect():
return False
group_name = 'cp_' + data.get('provider').lower()
if not self._update_provider_group(group_name, data):
return False
torrent_params = {}
if self.conf('label'):
torrent_params['label'] = self.conf('label')
if not filedata and data.get('protocol') == 'torrent':
log.error('Failed sending torrent, no data')
return False
# Try download magnet torrents
if data.get('protocol') == 'torrent_magnet':
filedata = self.magnetToTorrent(data.get('url'))
if filedata is False:
return False
data['protocol'] = 'torrent'
info = bdecode(filedata)["info"]
torrent_hash = sha1(bencode(info)).hexdigest().upper()
# Convert base 32 to hex
if len(torrent_hash) == 32:
torrent_hash = b16encode(b32decode(torrent_hash))
# Send request to rTorrent
try:
# Send torrent to rTorrent
torrent = self.rt.load_torrent(filedata)
if not torrent:
log.error('Unable to find the torrent, did it fail to load?')
return False
# Set label
if self.conf('label'):
torrent.set_custom(1, self.conf('label'))
if self.conf('directory'):
torrent.set_directory(self.conf('directory'))
# Set Ratio Group
torrent.set_visible(group_name)
# Start torrent
if not self.conf('paused', default = 0):
torrent.start()
return self.downloadReturnId(torrent_hash)
except Exception, err:
log.error('Failed to send torrent to rTorrent: %s', err)
return False
def getAllDownloadStatus(self, ids):
log.debug('Checking rTorrent download status.')
if not self.connect():
return []
try:
torrents = self.rt.get_torrents()
release_downloads = ReleaseDownloadList(self)
for torrent in torrents:
if torrent.info_hash in ids:
torrent_files = []
for file_item in torrent.get_files():
torrent_files.append(sp(os.path.join(torrent.directory, file_item.path)))
status = 'busy'
if torrent.complete:
if torrent.active:
status = 'seeding'
else:
status = 'completed'
release_downloads.append({
'id': torrent.info_hash,
'name': torrent.name,
'status': status,
'seed_ratio': torrent.ratio,
'original_status': torrent.state,
'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1,
'folder': sp(torrent.directory),
'files': '|'.join(torrent_files)
})
return release_downloads
except Exception, err:
log.error('Failed to get status from rTorrent: %s', err)
return []
def pause(self, release_download, pause = True):
if not self.connect():
return False
torrent = self.rt.find_torrent(release_download['id'])
if torrent is None:
return False
if pause:
return torrent.pause()
return torrent.resume()
def removeFailed(self, release_download):
log.info('%s failed downloading, deleting...', release_download['name'])
return self.processComplete(release_download, delete_files = True)
def processComplete(self, release_download, delete_files):
log.debug('Requesting rTorrent to remove the torrent %s%s.',
(release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
if not self.connect():
return False
torrent = self.rt.find_torrent(release_download['id'])
if torrent is None:
return False
if delete_files:
for file_item in torrent.get_files(): # will only delete files, not dir/sub-dir
os.unlink(os.path.join(torrent.directory, file_item.path))
if torrent.is_multi_file() and torrent.directory.endswith(torrent.name):
# Remove empty directories bottom up
try:
for path, _, _ in os.walk(torrent.directory, topdown = False):
os.rmdir(path)
except OSError:
log.info('Directory "%s" contains extra files, unable to remove', torrent.directory)
torrent.erase() # just removes the torrent, doesn't delete data
return True

View File

@@ -0,0 +1,335 @@
from base64 import b16encode, b32decode
from datetime import timedelta
from hashlib import sha1
from urlparse import urlparse
import os
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import sp
from couchpotato.core.helpers.variable import cleanHost, splitString
from couchpotato.core.logger import CPLog
from bencode import bencode, bdecode
from rtorrent import RTorrent
log = CPLog(__name__)
autoload = 'rTorrent'
class rTorrent(DownloaderBase):
protocol = ['torrent', 'torrent_magnet']
rt = None
error_msg = ''
# Migration url to host options
def __init__(self):
super(rTorrent, self).__init__()
addEvent('app.load', self.migrate)
addEvent('setting.save.rtorrent.*.after', self.settingsChanged)
def migrate(self):
url = self.conf('url')
if url:
host_split = splitString(url.split('://')[-1], split_on = '/')
self.conf('ssl', value = url.startswith('https'))
self.conf('host', value = host_split[0].strip())
self.conf('rpc_url', value = '/'.join(host_split[1:]))
self.deleteConf('url')
def settingsChanged(self):
# Reset active connection if settings have changed
if self.rt:
log.debug('Settings have changed, closing active connection')
self.rt = None
return True
def connect(self, reconnect = False):
# Already connected?
if not reconnect and self.rt is not None:
return self.rt
url = cleanHost(self.conf('host'), protocol = True, ssl = self.conf('ssl'))
# Automatically add '+https' to 'httprpc' protocol if SSL is enabled
if self.conf('ssl') and url.startswith('httprpc://'):
url = url.replace('httprpc://', 'httprpc+https://')
parsed = urlparse(url)
# rpc_url is only used on http/https scgi pass-through
if parsed.scheme in ['http', 'https']:
url += self.conf('rpc_url')
self.rt = RTorrent(
url,
self.conf('username'),
self.conf('password')
)
self.error_msg = ''
try:
self.rt._verify_conn()
except AssertionError as e:
self.error_msg = e.message
self.rt = None
return self.rt
def test(self):
if self.connect(True):
return True
if self.error_msg:
return False, 'Connection failed: ' + self.error_msg
return False
def download(self, data = None, media = None, filedata = None):
if not media: media = {}
if not data: data = {}
log.debug('Sending "%s" to rTorrent.', (data.get('name')))
if not self.connect():
return False
torrent_params = {}
if self.conf('label'):
torrent_params['label'] = self.conf('label')
if not filedata and data.get('protocol') == 'torrent':
log.error('Failed sending torrent, no data')
return False
# Try download magnet torrents
if data.get('protocol') == 'torrent_magnet':
filedata = self.magnetToTorrent(data.get('url'))
if filedata is False:
return False
data['protocol'] = 'torrent'
info = bdecode(filedata)["info"]
torrent_hash = sha1(bencode(info)).hexdigest().upper()
# Convert base 32 to hex
if len(torrent_hash) == 32:
torrent_hash = b16encode(b32decode(torrent_hash))
# Send request to rTorrent
try:
# Send torrent to rTorrent
torrent = self.rt.load_torrent(filedata, verify_retries=10)
if not torrent:
log.error('Unable to find the torrent, did it fail to load?')
return False
# Set label
if self.conf('label'):
torrent.set_custom(1, self.conf('label'))
if self.conf('directory'):
torrent.set_directory(self.conf('directory'))
# Start torrent
if not self.conf('paused', default = 0):
torrent.start()
return self.downloadReturnId(torrent_hash)
except Exception as err:
log.error('Failed to send torrent to rTorrent: %s', err)
return False
def getTorrentStatus(self, torrent):
if not torrent.complete:
return 'busy'
if torrent.open:
return 'seeding'
return 'completed'
def getAllDownloadStatus(self, ids):
log.debug('Checking rTorrent download status.')
if not self.connect():
return []
try:
torrents = self.rt.get_torrents()
release_downloads = ReleaseDownloadList(self)
for torrent in torrents:
if torrent.info_hash in ids:
torrent_directory = os.path.normpath(torrent.directory)
torrent_files = []
for file in torrent.get_files():
if not os.path.normpath(file.path).startswith(torrent_directory):
file_path = os.path.join(torrent_directory, file.path.lstrip('/'))
else:
file_path = file.path
torrent_files.append(sp(file_path))
release_downloads.append({
'id': torrent.info_hash,
'name': torrent.name,
'status': self.getTorrentStatus(torrent),
'seed_ratio': torrent.ratio,
'original_status': torrent.state,
'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1,
'folder': sp(torrent.directory),
'files': torrent_files
})
return release_downloads
except Exception as err:
log.error('Failed to get status from rTorrent: %s', err)
return []
def pause(self, release_download, pause = True):
if not self.connect():
return False
torrent = self.rt.find_torrent(release_download['id'])
if torrent is None:
return False
if pause:
return torrent.pause()
return torrent.resume()
def removeFailed(self, release_download):
log.info('%s failed downloading, deleting...', release_download['name'])
return self.processComplete(release_download, delete_files = True)
def processComplete(self, release_download, delete_files):
log.debug('Requesting rTorrent to remove the torrent %s%s.',
(release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
if not self.connect():
return False
torrent = self.rt.find_torrent(release_download['id'])
if torrent is None:
return False
if delete_files:
for file_item in torrent.get_files(): # will only delete files, not dir/sub-dir
os.unlink(os.path.join(torrent.directory, file_item.path))
if torrent.is_multi_file() and torrent.directory.endswith(torrent.name):
# Remove empty directories bottom up
try:
for path, _, _ in os.walk(sp(torrent.directory), topdown = False):
os.rmdir(path)
except OSError:
log.info('Directory "%s" contains extra files, unable to remove', torrent.directory)
torrent.erase() # just removes the torrent, doesn't delete data
return True
config = [{
'name': 'rtorrent',
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'rtorrent',
'label': 'rTorrent',
'description': '',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
'radio_group': 'torrent',
},
{
'name': 'host',
'default': 'localhost:80',
'description': 'RPC Communication URI. Usually <strong>scgi://localhost:5000</strong>, '
'<strong>httprpc://localhost/rutorrent</strong> or <strong>localhost:80</strong>'
},
{
'name': 'ssl',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
},
{
'name': 'rpc_url',
'type': 'string',
'default': 'RPC2',
'advanced': True,
'description': 'Change if your RPC mount is at a different path.',
},
{
'name': 'username',
},
{
'name': 'password',
'type': 'password',
},
{
'name': 'label',
'description': 'Label to apply on added torrents.',
},
{
'name': 'directory',
'type': 'directory',
'description': 'Download to this directory. Keep empty for default rTorrent download directory.',
},
{
'name': 'remove_complete',
'label': 'Remove torrent',
'default': False,
'advanced': True,
'type': 'bool',
'description': 'Remove the torrent after it finishes seeding.',
},
{
'name': 'delete_files',
'label': 'Remove files',
'default': True,
'type': 'bool',
'advanced': True,
'description': 'Also remove the leftover files.',
},
{
'name': 'paused',
'type': 'bool',
'advanced': True,
'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

@@ -1,18 +1,22 @@
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
from couchpotato.core.helpers.encoding import tryUrlencode, ss, sp
from couchpotato.core.helpers.variable import cleanHost, mergeDicts
from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
from datetime import timedelta
from urllib2 import URLError
import json
import os
import traceback
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
from couchpotato.core.helpers.encoding import tryUrlencode, ss, sp
from couchpotato.core.helpers.variable import cleanHost, mergeDicts
from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
log = CPLog(__name__)
autoload = 'Sabnzbd'
class Sabnzbd(Downloader):
class Sabnzbd(DownloaderBase):
protocol = ['nzb']
@@ -64,6 +68,26 @@ class Sabnzbd(Downloader):
log.error('Error getting data from SABNZBd: %s', sab_data)
return False
def test(self):
try:
sab_data = self.call({
'mode': 'version',
})
v = sab_data.split('.')
if int(v[0]) == 0 and int(v[1]) < 7:
return False, 'Your Sabnzbd client is too old, please update to newest version.'
# the version check will work even with wrong api key, so we need the next check as well
sab_data = self.call({
'mode': 'qstatus',
})
if not sab_data:
return False
except:
return False
return True
def getAllDownloadStatus(self, ids):
log.debug('Checking SABnzbd download status.')
@@ -95,7 +119,7 @@ class Sabnzbd(Downloader):
status = 'busy'
if 'ENCRYPTED / ' in nzb['filename']:
status = 'failed'
release_downloads.append({
'id': nzb['nzo_id'],
'name': nzb['filename'],
@@ -112,7 +136,7 @@ class Sabnzbd(Downloader):
status = 'failed'
elif nzb['status'] == 'Completed':
status = 'completed'
release_downloads.append({
'id': nzb['nzo_id'],
'name': nzb['name'],
@@ -165,9 +189,9 @@ class Sabnzbd(Downloader):
def call(self, request_params, use_json = True, **kwargs):
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(request_params, {
'apikey': self.conf('api_key'),
'output': 'json'
url = cleanHost(self.conf('host'), ssl = self.conf('ssl')) + 'api?' + tryUrlencode(mergeDicts(request_params, {
'apikey': self.conf('api_key'),
'output': 'json'
}))
data = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()}, **kwargs)
@@ -181,3 +205,77 @@ class Sabnzbd(Downloader):
else:
return data
config = [{
'name': 'sabnzbd',
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'sabnzbd',
'label': 'Sabnzbd',
'description': 'Use <a href="http://sabnzbd.org/" target="_blank">SABnzbd</a> (0.7+) to download NZBs.',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
'radio_group': 'nzb',
},
{
'name': 'host',
'default': 'localhost:8080',
},
{
'name': 'ssl',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
},
{
'name': 'api_key',
'label': 'Api Key',
'description': 'Used for all calls to Sabnzbd.',
},
{
'name': 'category',
'label': 'Category',
'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
},
{
'name': 'priority',
'label': 'Priority',
'type': 'dropdown',
'default': '0',
'advanced': True,
'values': [('Paused', -2), ('Low', -1), ('Normal', 0), ('High', 1), ('Forced', 2)],
'description': 'Add to the queue with this priority.',
},
{
'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': 'remove_complete',
'advanced': True,
'label': 'Remove NZB',
'default': False,
'type': 'bool',
'description': 'Remove the NZB from history after it completed.',
},
{
'name': 'delete_failed',
'default': True,
'advanced': True,
'type': 'bool',
'description': 'Delete a release after the download has failed.',
},
],
}
],
}]

View File

@@ -1,71 +0,0 @@
from .main import Sabnzbd
def start():
return Sabnzbd()
config = [{
'name': 'sabnzbd',
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'sabnzbd',
'label': 'Sabnzbd',
'description': 'Use <a href="http://sabnzbd.org/" target="_blank">SABnzbd</a> (0.7+) to download NZBs.',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
'radio_group': 'nzb',
},
{
'name': 'host',
'default': 'localhost:8080',
},
{
'name': 'api_key',
'label': 'Api Key',
'description': 'Used for all calls to Sabnzbd.',
},
{
'name': 'category',
'label': 'Category',
'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
},
{
'name': 'priority',
'label': 'Priority',
'type': 'dropdown',
'default': '0',
'advanced': True,
'values': [('Paused', -2), ('Low', -1), ('Normal', 0), ('High', 1), ('Forced', 2)],
'description': 'Add to the queue with this priority.',
},
{
'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': 'remove_complete',
'advanced': True,
'label': 'Remove NZB',
'default': False,
'type': 'bool',
'description': 'Remove the NZB from history after it completed.',
},
{
'name': 'delete_failed',
'default': True,
'advanced': True,
'type': 'bool',
'description': 'Delete a release after the download has failed.',
},
],
}
],
}]

View File

@@ -1,14 +1,19 @@
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import isInt
from couchpotato.core.logger import CPLog
import json
import requests
import traceback
from couchpotato.core._base.downloader.main import DownloaderBase
from couchpotato.core.helpers.encoding import isInt
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog
import requests
log = CPLog(__name__)
autoload = 'Synology'
class Synology(Downloader):
class Synology(DownloaderBase):
protocol = ['nzb', 'torrent', 'torrent_magnet']
status_support = False
@@ -21,14 +26,14 @@ class Synology(Downloader):
log.error('Sending "%s" (%s) to Synology.', (data['name'], data['protocol']))
# Load host from config and split out port.
host = self.conf('host').split(':')
host = cleanHost(self.conf('host'), protocol = False).split(':')
if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.')
return False
try:
# Send request to Synology
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'), self.conf('destination'))
if data['protocol'] == 'torrent_magnet':
log.info('Adding torrent URL %s', data['url'])
response = srpc.create_task(url = data['url'])
@@ -44,6 +49,16 @@ class Synology(Downloader):
finally:
return self.downloadReturnId('') if response else False
def test(self):
host = cleanHost(self.conf('host'), protocol = False).split(':')
try:
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
test_result = srpc.test()
except:
return False
return test_result
def getEnabledProtocol(self):
if self.conf('use_for') == 'both':
return super(Synology, self).getEnabledProtocol()
@@ -64,18 +79,21 @@ class Synology(Downloader):
return super(Synology, self).isEnabled(manual, data) and\
((self.conf('use_for') in for_protocol))
class SynologyRPC(object):
"""SynologyRPC lite library"""
def __init__(self, host = 'localhost', port = 5000, username = None, password = None):
def __init__(self, host = 'localhost', port = 5000, username = None, password = None, destination = 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.sid = None
self.username = username
self.password = password
self.destination = destination
self.session_name = 'DownloadStation'
def _login(self):
@@ -106,11 +124,11 @@ class SynologyRPC(object):
if response['success']:
log.info('Synology action successfull')
return response
except requests.ConnectionError, err:
except requests.ConnectionError as err:
log.error('Synology connection error, check your config %s', err)
except requests.HTTPError, err:
except requests.HTTPError as err:
log.error('SynologyRPC HTTPError: %s', err)
except Exception, err:
except Exception as err:
log.error('Exception: %s', err)
finally:
return response
@@ -128,6 +146,10 @@ class SynologyRPC(object):
'version': '1',
'method': 'create',
'_sid': self.sid}
if self.destination and len(self.destination) > 0:
args['destination'] = self.destination
if url:
log.info('Login success, adding torrent URI')
args['uri'] = url
@@ -145,3 +167,60 @@ class SynologyRPC(object):
self._logout()
return result
def test(self):
return bool(self._login())
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': 'nzb,torrent',
},
{
'name': 'host',
'default': 'localhost:5000',
'description': 'Hostname with port. Usually <strong>localhost:5000</strong>',
},
{
'name': 'username',
},
{
'name': 'password',
'type': 'password',
},
{
'name': 'destination',
'description': 'Specify <strong>existing</strong> destination share to where your files will be downloaded, usually <strong>Downloads</strong>',
'advanced': True,
},
{
'name': 'use_for',
'label': 'Use for',
'default': 'both',
'type': 'dropdown',
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
},
{
'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

@@ -1,52 +0,0 @@
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': 'nzb,torrent',
},
{
'name': 'host',
'default': 'localhost:5000',
'description': 'Hostname with port. Usually <strong>localhost:5000</strong>',
},
{
'name': 'username',
},
{
'name': 'password',
'type': 'password',
},
{
'name': 'use_for',
'label': 'Use for',
'default': 'both',
'type': 'dropdown',
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
},
{
'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

@@ -1,8 +1,4 @@
from base64 import b64encode
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
from couchpotato.core.helpers.encoding import isInt, sp
from couchpotato.core.helpers.variable import tryInt, tryFloat
from couchpotato.core.logger import CPLog
from datetime import timedelta
import httplib
import json
@@ -10,24 +6,32 @@ import os.path
import re
import urllib2
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
from couchpotato.core.helpers.encoding import isInt, sp
from couchpotato.core.helpers.variable import tryInt, tryFloat, cleanHost
from couchpotato.core.logger import CPLog
log = CPLog(__name__)
autoload = 'Transmission'
class Transmission(Downloader):
class Transmission(DownloaderBase):
protocol = ['torrent', 'torrent_magnet']
log = CPLog(__name__)
trpc = None
def connect(self):
def connect(self, reconnect = False):
# Load host from config and split out port.
host = self.conf('host').split(':')
host = cleanHost(self.conf('host'), protocol = False).split(':')
if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.')
return False
if not self.trpc:
self.trpc = TransmissionRPC(host[0], port = host[1], rpc_url = self.conf('rpc_url'), username = self.conf('username'), password = self.conf('password'))
if not self.trpc or reconnect:
self.trpc = TransmissionRPC(host[0], port = host[1], rpc_url = self.conf('rpc_url').strip('/ '), username = self.conf('username'), password = self.conf('password'))
return self.trpc
@@ -83,6 +87,11 @@ class Transmission(Downloader):
log.info('Torrent sent to Transmission successfully.')
return self.downloadReturnId(remote_torrent['torrent-added']['hashString'])
def test(self):
if self.connect(True) and self.trpc.get_session():
return True
return False
def getAllDownloadStatus(self, ids):
log.debug('Checking Transmission download status.')
@@ -96,6 +105,7 @@ class Transmission(Downloader):
'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isStalled', 'isFinished', 'downloadDir', 'uploadRatio', 'secondsSeeding', 'seedIdleLimit', 'files']
}
session = self.trpc.get_session()
queue = self.trpc.get_alltorrents(return_params)
if not (queue and queue.get('torrents')):
log.debug('Nothing in queue or error')
@@ -103,13 +113,9 @@ class Transmission(Downloader):
for torrent in queue['torrents']:
if torrent['hashString'] in ids:
log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / isStalled=%s / eta=%s / uploadRatio=%s / isFinished=%s',
(torrent['name'], torrent['id'], torrent['downloadDir'], torrent['hashString'], torrent['percentDone'], torrent['status'], torrent.get('isStalled', 'N/A'), torrent['eta'], torrent['uploadRatio'], torrent['isFinished']))
torrent_files = []
for file_item in torrent['files']:
torrent_files.append(sp(os.path.join(torrent['downloadDir'], file_item['name'])))
log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / isStalled=%s / eta=%s / uploadRatio=%s / isFinished=%s / incomplete-dir-enabled=%s / incomplete-dir=%s',
(torrent['name'], torrent['id'], torrent['downloadDir'], torrent['hashString'], torrent['percentDone'], torrent['status'], torrent.get('isStalled', 'N/A'), torrent['eta'], torrent['uploadRatio'], torrent['isFinished'], session['incomplete-dir-enabled'], session['incomplete-dir']))
status = 'busy'
if torrent.get('isStalled') and not torrent['percentDone'] == 1 and self.conf('stalled_as_failed'):
status = 'failed'
@@ -117,7 +123,16 @@ class Transmission(Downloader):
status = 'completed'
elif torrent['status'] in [5, 6]:
status = 'seeding'
if session['incomplete-dir-enabled'] and status == 'busy':
torrent_folder = session['incomplete-dir']
else:
torrent_folder = torrent['downloadDir']
torrent_files = []
for file_item in torrent['files']:
torrent_files.append(sp(os.path.join(torrent_folder, file_item['name'])))
release_downloads.append({
'id': torrent['hashString'],
'name': torrent['name'],
@@ -125,8 +140,8 @@ class Transmission(Downloader):
'original_status': torrent['status'],
'seed_ratio': torrent['uploadRatio'],
'timeleft': str(timedelta(seconds = torrent['eta'])),
'folder': sp(torrent['downloadDir'] if len(torrent_files) == 1 else os.path.join(torrent['downloadDir'], torrent['name'])),
'files': '|'.join(torrent_files)
'folder': sp(torrent_folder if len(torrent_files) == 1 else os.path.join(torrent_folder, torrent['name'])),
'files': torrent_files
})
return release_downloads
@@ -145,6 +160,7 @@ class Transmission(Downloader):
log.debug('Requesting Transmission to remove the torrent %s%s.', (release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
return self.trpc.remove_torrent(release_download['id'], delete_files)
class TransmissionRPC(object):
"""TransmissionRPC lite library"""
@@ -158,8 +174,8 @@ class TransmissionRPC(object):
self.session = {}
if username and password:
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
password_manager.add_password(realm = None, uri = self.url, user = username, passwd = password)
opener = urllib2.build_opener(urllib2.HTTPBasicAuthHandler(password_manager), urllib2.HTTPDigestAuthHandler(password_manager))
password_manager.add_password(realm = 'Transmission', uri = self.url, user = username, passwd = password)
opener = urllib2.build_opener(urllib2.HTTPBasicAuthHandler(password_manager))
opener.addheaders = [('User-agent', 'couchpotato-transmission-client/1.0')]
urllib2.install_opener(opener)
elif username or password:
@@ -181,10 +197,10 @@ class TransmissionRPC(object):
else:
log.debug('Unknown failure sending command to Transmission. Return text is: %s', response['result'])
return False
except httplib.InvalidURL, err:
except httplib.InvalidURL as err:
log.error('Invalid Transmission host, check your config %s', err)
return False
except urllib2.HTTPError, err:
except urllib2.HTTPError as err:
if err.code == 401:
log.error('Invalid Transmission Username or Password, check your config')
return False
@@ -202,7 +218,7 @@ class TransmissionRPC(object):
log.error('Unable to get Transmission Session-Id %s', err)
else:
log.error('TransmissionRPC HTTPError: %s', err)
except urllib2.URLError, err:
except urllib2.URLError as err:
log.error('Unable to connect to Transmission %s', err)
def get_session(self):
@@ -240,3 +256,93 @@ class TransmissionRPC(object):
post_data = {'arguments': {'ids': torrent_id, 'delete-local-data': delete_local_data}, 'method': 'torrent-remove', 'tag': self.tag}
return self._request(post_data)
config = [{
'name': 'transmission',
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'transmission',
'label': 'Transmission',
'description': 'Use <a href="http://www.transmissionbt.com/" target="_blank">Transmission</a> to download torrents.',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
'radio_group': 'torrent',
},
{
'name': 'host',
'default': 'localhost:9091',
'description': 'Hostname with port. Usually <strong>localhost:9091</strong>',
},
{
'name': 'rpc_url',
'type': 'string',
'default': 'transmission',
'advanced': True,
'description': 'Change if you don\'t run Transmission RPC at the default url.',
},
{
'name': 'username',
},
{
'name': 'password',
'type': 'password',
},
{
'name': 'directory',
'type': 'directory',
'description': 'Download to this directory. Keep empty for default Transmission download directory.',
},
{
'name': 'remove_complete',
'label': 'Remove torrent',
'default': True,
'advanced': True,
'type': 'bool',
'description': 'Remove the torrent from Transmission after it finished seeding.',
},
{
'name': 'delete_files',
'label': 'Remove files',
'default': True,
'type': 'bool',
'advanced': True,
'description': 'Also remove the leftover files.',
},
{
'name': 'paused',
'type': 'bool',
'advanced': True,
'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.',
},
{
'name': 'stalled_as_failed',
'default': True,
'advanced': True,
'type': 'bool',
'description': 'Consider a stalled torrent as failed',
},
{
'name': 'delete_failed',
'default': True,
'advanced': True,
'type': 'bool',
'description': 'Delete a release after the download has failed.',
},
],
}
],
}]

View File

@@ -1,94 +0,0 @@
from .main import Transmission
def start():
return Transmission()
config = [{
'name': 'transmission',
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'transmission',
'label': 'Transmission',
'description': 'Use <a href="http://www.transmissionbt.com/" target="_blank">Transmission</a> to download torrents.',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
'radio_group': 'torrent',
},
{
'name': 'host',
'default': 'localhost:9091',
'description': 'Hostname with port. Usually <strong>localhost:9091</strong>',
},
{
'name': 'rpc_url',
'type': 'string',
'default': 'transmission',
'advanced': True,
'description': 'Change if you don\'t run Transmission RPC at the default url.',
},
{
'name': 'username',
},
{
'name': 'password',
'type': 'password',
},
{
'name': 'directory',
'type': 'directory',
'description': 'Download to this directory. Keep empty for default Transmission download directory.',
},
{
'name': 'remove_complete',
'label': 'Remove torrent',
'default': True,
'advanced': True,
'type': 'bool',
'description': 'Remove the torrent from Transmission after it finished seeding.',
},
{
'name': 'delete_files',
'label': 'Remove files',
'default': True,
'type': 'bool',
'advanced': True,
'description': 'Also remove the leftover files.',
},
{
'name': 'paused',
'type': 'bool',
'advanced': True,
'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.',
},
{
'name': 'stalled_as_failed',
'default': True,
'advanced': True,
'type': 'bool',
'description': 'Consider a stalled torrent as failed',
},
{
'name': 'delete_failed',
'default': True,
'advanced': True,
'type': 'bool',
'description': 'Delete a release after the download has failed.',
},
],
}
],
}]

View File

@@ -1,12 +1,6 @@
from base64 import b16encode, b32decode
from bencode import bencode as benc, bdecode
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
from couchpotato.core.helpers.encoding import isInt, ss, sp
from couchpotato.core.helpers.variable import tryInt, tryFloat
from couchpotato.core.logger import CPLog
from datetime import timedelta
from hashlib import sha1
from multipartpost import MultipartPostHandler
import cookielib
import httplib
import json
@@ -17,27 +11,37 @@ import time
import urllib
import urllib2
from bencode import bencode as benc, bdecode
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
from couchpotato.core.helpers.encoding import isInt, ss, sp
from couchpotato.core.helpers.variable import tryInt, tryFloat, cleanHost
from couchpotato.core.logger import CPLog
from multipartpost import MultipartPostHandler
log = CPLog(__name__)
autoload = 'uTorrent'
class uTorrent(Downloader):
class uTorrent(DownloaderBase):
protocol = ['torrent', 'torrent_magnet']
utorrent_api = None
status_flags = {
'STARTED' : 1,
'CHECKING' : 2,
'CHECK-START' : 4,
'CHECKED' : 8,
'ERROR' : 16,
'PAUSED' : 32,
'QUEUED' : 64,
'LOADED' : 128
'STARTED': 1,
'CHECKING': 2,
'CHECK-START': 4,
'CHECKED': 8,
'ERROR': 16,
'PAUSED': 32,
'QUEUED': 64,
'LOADED': 128
}
def connect(self):
# Load host from config and split out port.
host = self.conf('host').split(':')
host = cleanHost(self.conf('host'), protocol = False).split(':')
if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.')
return False
@@ -66,7 +70,7 @@ class uTorrent(Downloader):
new_settings['seed_prio_limitul_flag'] = True
log.info('Updated uTorrent settings to set a torrent to complete after it the seeding requirements are met.')
if settings.get('bt.read_only_on_complete'): #This doesn't work as this option seems to be not available through the api. Mitigated with removeReadOnly function
if settings.get('bt.read_only_on_complete'): #This doesn't work as this option seems to be not available through the api. Mitigated with removeReadOnly function
new_settings['bt.read_only_on_complete'] = False
log.info('Updated uTorrent settings to not set the files to read only after completing.')
@@ -115,6 +119,17 @@ class uTorrent(Downloader):
return self.downloadReturnId(torrent_hash)
def test(self):
if self.connect():
build_version = self.utorrent_api.get_build()
if not build_version:
return False
if build_version < 25406: # This build corresponds to version 3.0.0 stable
return False, 'Your uTorrent client is too old, please update to newest version.'
return True
return False
def getAllDownloadStatus(self, ids):
log.debug('Checking uTorrent download status.')
@@ -149,18 +164,18 @@ class uTorrent(Downloader):
torrent_files = [sp(os.path.join(torrent[26], torrent_file[0])) for torrent_file in torrent_files['files'][1]]
except:
log.debug('Failed getting files from torrent: %s', torrent[2])
status = 'busy'
if (torrent[1] & self.status_flags['STARTED'] or torrent[1] & self.status_flags['QUEUED']) and torrent[4] == 1000:
status = 'seeding'
elif (torrent[1] & self.status_flags['ERROR']):
elif torrent[1] & self.status_flags['ERROR']:
status = 'failed'
elif torrent[4] == 1000:
status = 'completed'
if not status == 'busy':
self.removeReadOnly(torrent_files)
release_downloads.append({
'id': torrent[0],
'name': torrent[2],
@@ -169,7 +184,7 @@ class uTorrent(Downloader):
'original_status': torrent[1],
'timeleft': str(timedelta(seconds = torrent[10])),
'folder': sp(torrent[26]),
'files': '|'.join(torrent_files)
'files': torrent_files
})
return release_downloads
@@ -214,7 +229,6 @@ class uTorrentAPI(object):
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()
@@ -231,14 +245,14 @@ class uTorrentAPI(object):
return response
else:
log.debug('Unknown failure sending command to uTorrent. Return text is: %s', response)
except httplib.InvalidURL, err:
except httplib.InvalidURL as err:
log.error('Invalid uTorrent host, check your config %s', err)
except urllib2.HTTPError, err:
except urllib2.HTTPError as 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:
except urllib2.URLError as err:
log.error('Unable to connect to uTorrent %s', err)
return False
@@ -261,7 +275,7 @@ class uTorrentAPI(object):
def set_torrent(self, hash, params):
action = 'action=setprops&hash=%s' % hash
for k, v in params.iteritems():
for k, v in params.items():
action += '&s=%s&v=%s' % (k, v)
return self._request(action)
@@ -304,7 +318,7 @@ class uTorrentAPI(object):
#log.debug('uTorrent settings: %s', settings_dict)
except Exception, err:
except Exception as err:
log.error('Failed to get settings from uTorrent: %s', err)
return settings_dict
@@ -322,3 +336,86 @@ class uTorrentAPI(object):
def get_files(self, hash):
action = 'action=getfiles&hash=%s' % hash
return self._request(action)
def get_build(self):
data = self._request('')
if not data:
return False
response = json.loads(data)
return int(response.get('build'))
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> (3.0+) to download torrents.',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
'radio_group': 'torrent',
},
{
'name': 'host',
'default': 'localhost:8000',
'description': 'Port can be found in settings when enabling WebUI.',
},
{
'name': 'username',
},
{
'name': 'password',
'type': 'password',
},
{
'name': 'label',
'description': 'Label to add torrent as.',
},
{
'name': 'remove_complete',
'label': 'Remove torrent',
'default': True,
'advanced': True,
'type': 'bool',
'description': 'Remove the torrent from uTorrent after it finished seeding.',
},
{
'name': 'delete_files',
'label': 'Remove files',
'default': True,
'type': 'bool',
'advanced': True,
'description': 'Also remove the leftover files.',
},
{
'name': 'paused',
'type': 'bool',
'advanced': True,
'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.',
},
{
'name': 'delete_failed',
'default': True,
'advanced': True,
'type': 'bool',
'description': 'Delete a release after the download has failed.',
},
],
}
],
}]

View File

@@ -1,79 +0,0 @@
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> (3.0+) 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': 'remove_complete',
'label': 'Remove torrent',
'default': True,
'advanced': True,
'type': 'bool',
'description': 'Remove the torrent from uTorrent after it finished seeding.',
},
{
'name': 'delete_files',
'label': 'Remove files',
'default': True,
'type': 'bool',
'advanced': True,
'description': 'Also remove the leftover files.',
},
{
'name': 'paused',
'type': 'bool',
'advanced': True,
'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.',
},
{
'name': 'delete_failed',
'default': True,
'advanced': True,
'type': 'bool',
'description': 'Delete a release after the download has failed.',
},
],
}
],
}]

View File

@@ -1,12 +1,15 @@
from axl.axel import Event
from couchpotato.core.helpers.variable import mergeDicts, natcmp
from couchpotato.core.logger import CPLog
import threading
import traceback
from axl.axel import Event
from couchpotato.core.helpers.variable import mergeDicts, natsortKey
from couchpotato.core.logger import CPLog
log = CPLog(__name__)
events = {}
def runHandler(name, handler, *args, **kwargs):
try:
return handler(*args, **kwargs)
@@ -14,6 +17,7 @@ def runHandler(name, handler, *args, **kwargs):
from couchpotato.environment import Env
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 not events.get(name):
@@ -27,7 +31,7 @@ def addEvent(name, handler, priority = 100):
has_parent = hasattr(handler, 'im_self')
parent = None
if has_parent:
parent = handler.im_self
parent = handler.__self__
bc = hasattr(parent, 'beforeCall')
if bc: parent.beforeCall(handler)
@@ -48,22 +52,19 @@ def addEvent(name, handler, priority = 100):
'priority': priority,
})
def removeEvent(name, handler):
e = events[name]
e -= handler
def fireEvent(name, *args, **kwargs):
if not events.has_key(name): return
if name not in events: return
#log.debug('Firing event %s', name)
try:
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
'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
}
# Do options
@@ -101,11 +102,14 @@ def fireEvent(name, *args, **kwargs):
# Fire
result = e(*args, **kwargs)
result_keys = result.keys()
result_keys.sort(key = natsortKey)
if options['single'] and not options['merge']:
results = None
# Loop over results, stop when first not None result is found.
for r_key in sorted(result.iterkeys(), cmp = natcmp):
for r_key in result_keys:
r = result[r_key]
if r[0] is True and r[1] is not None:
results = r[1]
@@ -117,7 +121,7 @@ def fireEvent(name, *args, **kwargs):
else:
results = []
for r_key in sorted(result.iterkeys(), cmp = natcmp):
for r_key in result_keys:
r = result[r_key]
if r[0] == True and r[1]:
results.append(r[1])
@@ -160,18 +164,21 @@ def fireEvent(name, *args, **kwargs):
except Exception:
log.error('%s: %s', (name, traceback.format_exc()))
def fireEventAsync(*args, **kwargs):
try:
t = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
t.setDaemon(True)
t.start()
return True
except Exception, e:
except Exception as e:
log.error('%s: %s', (args[0], e))
def errorHandler(error):
etype, value, tb = error
log.error(''.join(traceback.format_exception(etype, value, tb)))
def getEvent(name):
return events[name]

View File

@@ -1,4 +1,3 @@
from couchpotato.core.logger import CPLog
from string import ascii_letters, digits
from urllib import quote_plus
import os
@@ -6,28 +5,34 @@ import re
import traceback
import unicodedata
from couchpotato.core.logger import CPLog
import six
log = CPLog(__name__)
def toSafeString(original):
valid_chars = "-_.() %s%s" % (ascii_letters, digits)
cleanedFilename = unicodedata.normalize('NFKD', toUnicode(original)).encode('ASCII', 'ignore')
valid_string = ''.join(c for c in cleanedFilename if c in valid_chars)
cleaned_filename = unicodedata.normalize('NFKD', toUnicode(original)).encode('ASCII', 'ignore')
valid_string = ''.join(c for c in cleaned_filename if c in valid_chars)
return ' '.join(valid_string.split())
def simplifyString(original):
string = stripAccents(original.lower())
string = toSafeString(' '.join(re.split('\W+', string)))
split = re.split('\W+|_', string.lower())
return toUnicode(' '.join(split))
def toUnicode(original, *args):
try:
if isinstance(original, unicode):
return original
else:
try:
return unicode(original, *args)
return six.text_type(original, *args)
except:
try:
return ek(original, *args)
@@ -38,16 +43,18 @@ def toUnicode(original, *args):
ascii_text = str(original).encode('string_escape')
return toUnicode(ascii_text)
def ss(original, *args):
u_original = toUnicode(original, *args)
try:
from couchpotato.environment import Env
return u_original.encode(Env.get('encoding'))
except Exception, e:
except Exception as e:
log.debug('Failed ss encoding char, force UTF8: %s', e)
return u_original.encode('UTF-8')
def sp(path, *args):
# Standardise encoding, normalise case, path and strip trailing '/' or '\'
@@ -58,7 +65,7 @@ def sp(path, *args):
if os.path.sep == '/' and '\\' in path:
path = '/' + path.replace(':', '').replace('\\', '/')
path = os.path.normcase(os.path.normpath(ss(path, *args)))
path = os.path.normpath(ss(path, *args))
# Remove any trailing path separators
if path != os.path.sep:
@@ -73,6 +80,7 @@ def sp(path, *args):
return path
def ek(original, *args):
if isinstance(original, (str, unicode)):
try:
@@ -83,6 +91,7 @@ def ek(original, *args):
return original
def isInt(value):
try:
int(value)
@@ -90,14 +99,16 @@ def isInt(value):
except ValueError:
return False
def stripAccents(s):
return ''.join((c for c in unicodedata.normalize('NFD', toUnicode(s)) if unicodedata.category(c) != 'Mn'))
def tryUrlencode(s):
new = u''
new = six.u('')
if isinstance(s, dict):
for key, value in s.iteritems():
new += u'&%s=%s' % (key, tryUrlencode(value))
for key, value in s.items():
new += six.u('&%s=%s') % (key, tryUrlencode(value))
return new[1:]
else:

View File

@@ -1,15 +1,21 @@
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import natcmp
from urllib import unquote
import re
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import natsortKey
def getParams(params):
reg = re.compile('^[a-z0-9_\.]+$')
# Sort keys
param_keys = params.keys()
param_keys.sort(key = natsortKey)
temp = {}
for param, value in sorted(params.iteritems()):
for param in param_keys:
value = params[param]
nest = re.split("([\[\]]+)", param)
if len(nest) > 1:
@@ -37,13 +43,26 @@ def getParams(params):
return dictToList(temp)
non_decimal = re.compile(r'[^\d.]+')
def dictToList(params):
if type(params) is dict:
new = {}
for x, value in params.iteritems():
for x, value in params.items():
try:
new_value = [dictToList(value[k]) for k in sorted(value.iterkeys(), cmp = natcmp)]
convert = lambda text: int(text) if text.isdigit() else text.lower()
alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)]
sorted_keys = sorted(value.keys(), key = alphanum_key)
all_ints = 0
for pnr in sorted_keys:
all_ints += 1 if non_decimal.sub('', pnr) == pnr else 0
if all_ints == len(sorted_keys):
new_value = [dictToList(value[k]) for k in sorted_keys]
else:
new_value = value
except:
new_value = value

View File

@@ -1,8 +1,11 @@
from couchpotato.core.logger import CPLog
import xml.etree.ElementTree as XMLTree
from couchpotato.core.logger import CPLog
log = CPLog(__name__)
class RSS(object):
def getTextElements(self, xml, path):
@@ -46,6 +49,6 @@ class RSS(object):
def getItems(self, data, path = 'channel/item'):
try:
return XMLTree.parse(data).findall(path)
except Exception, e:
except Exception as e:
log.error('Error parsing RSS. %s', e)
return []

View File

@@ -1,6 +1,5 @@
from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss
from couchpotato.core.logger import CPLog
import collections
import ctypes
import hashlib
import os
import platform
@@ -8,26 +7,37 @@ import random
import re
import string
import sys
import traceback
from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss, sp
from couchpotato.core.logger import CPLog
import six
from six.moves import map, zip, filter
log = CPLog(__name__)
def fnEscape(pattern):
return pattern.replace('[','[[').replace(']','[]]').replace('[[','[[]')
return pattern.replace('[', '[[').replace(']', '[]]').replace('[[', '[[]')
def link(src, dst):
if os.name == 'nt':
import ctypes
if ctypes.windll.kernel32.CreateHardLinkW(unicode(dst), unicode(src), 0) == 0: raise ctypes.WinError()
if ctypes.windll.kernel32.CreateHardLinkW(six.text_type(dst), six.text_type(src), 0) == 0: raise ctypes.WinError()
else:
os.link(src, dst)
def symlink(src, dst):
if os.name == 'nt':
import ctypes
if ctypes.windll.kernel32.CreateSymbolicLinkW(unicode(dst), unicode(src), 1 if os.path.isdir(src) else 0) in [0, 1280]: raise ctypes.WinError()
if ctypes.windll.kernel32.CreateSymbolicLinkW(six.text_type(dst), six.text_type(src), 1 if os.path.isdir(src) else 0) in [0, 1280]: raise ctypes.WinError()
else:
os.symlink(src, dst)
def getUserDir():
try:
import pwd
@@ -37,6 +47,7 @@ def getUserDir():
return os.path.expanduser('~')
def getDownloadDir():
user_dir = getUserDir()
@@ -49,6 +60,7 @@ def getDownloadDir():
return user_dir
def getDataDir():
# Windows
@@ -68,8 +80,10 @@ def getDataDir():
# Linux
return os.path.join(user_dir, '.couchpotato')
def isDict(object):
return isinstance(object, dict)
def isDict(obj):
return isinstance(obj, dict)
def mergeDicts(a, b, prepend_list = False):
assert isDict(a), isDict(b)
@@ -91,6 +105,7 @@ def mergeDicts(a, b, prepend_list = False):
current_dst[key] = current_src[key]
return dst
def removeListDuplicates(seq):
checked = []
for e in seq:
@@ -98,35 +113,73 @@ def removeListDuplicates(seq):
checked.append(e)
return checked
def flattenList(l):
if isinstance(l, list):
return sum(map(flattenList, l))
else:
return l
def md5(text):
return hashlib.md5(ss(text)).hexdigest()
def sha1(text):
return hashlib.sha1(text).hexdigest()
def isLocalIP(ip):
ip = ip.lstrip('htps:/')
regex = '/(^127\.)|(^192\.168\.)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^::1)$/'
return re.search(regex, ip) is not None or 'localhost' in ip or ip[:4] == '127.'
def getExt(filename):
return os.path.splitext(filename)[1][1:]
def cleanHost(host):
if not host.startswith(('http://', 'https://')):
host = 'http://' + host
host = host.rstrip('/')
host += '/'
def cleanHost(host, protocol = True, ssl = False, username = None, password = None):
"""Return a cleaned up host with given url options set
Changes protocol to https if ssl is set to True and http if ssl is set to false.
>>> cleanHost("localhost:80", ssl=True)
'https://localhost:80/'
>>> cleanHost("localhost:80", ssl=False)
'http://localhost:80/'
Username and password is managed with the username and password variables
>>> cleanHost("localhost:80", username="user", password="passwd")
'http://user:passwd@localhost:80/'
Output without scheme (protocol) can be forced with protocol=False
>>> cleanHost("localhost:80", protocol=False)
'localhost:80'
"""
if not '://' in host and protocol:
host = ('https://' if ssl else 'http://') + host
if not protocol:
host = host.split('://', 1)[-1]
if protocol and username and password:
try:
auth = re.findall('^(?:.+?//)(.+?):(.+?)@(?:.+)$', host)
if auth:
log.error('Cleanhost error: auth already defined in url: %s, please remove BasicAuth from url.', host)
else:
host = host.replace('://', '://%s:%s@' % (username, password), 1)
except:
pass
host = host.rstrip('/ ')
if protocol:
host += '/'
return host
def getImdb(txt, check_inside = False, multiple = False):
if not check_inside:
@@ -143,7 +196,7 @@ def getImdb(txt, check_inside = False, multiple = False):
ids = re.findall('(tt\d{4,7})', txt)
if multiple:
return list(set(['tt%07d' % tryInt(x[2:]) for x in ids])) if len(ids) > 0 else []
return removeDuplicate(['tt%07d' % tryInt(x[2:]) for x in ids]) if len(ids) > 0 else []
return 'tt%07d' % tryInt(ids[0][2:])
except IndexError:
@@ -151,10 +204,12 @@ def getImdb(txt, check_inside = False, multiple = False):
return False
def tryInt(s, default = 0):
try: return int(s)
except: return default
def tryFloat(s):
try:
if isinstance(s, str):
@@ -163,39 +218,43 @@ def tryFloat(s):
return float(s)
except: return 0
def natsortKey(s):
return map(tryInt, re.findall(r'(\d+|\D+)', s))
def natcmp(a, b):
return cmp(natsortKey(a), natsortKey(b))
def natsortKey(string_):
"""See http://www.codinghorror.com/blog/archives/001018.html"""
return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', string_)]
def toIterable(value):
if isinstance(value, collections.Iterable):
return value
return [value]
def getTitle(library_dict):
def getIdentifier(media):
return media.get('identifier') or media.get('identifiers', {}).get('imdb')
def getTitle(media_dict):
try:
try:
return library_dict['titles'][0]['title']
return media_dict['title']
except:
try:
for title in library_dict.titles:
if title.default:
return title.title
return media_dict['titles'][0]
except:
try:
return library_dict['info']['titles'][0]
return media_dict['info']['titles'][0]
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
try:
return media_dict['media']['info']['titles'][0]
except:
log.error('Could not get title for %s', getIdentifier(media_dict))
return None
except:
log.error('Could not get title for library item: %s', library_dict)
log.error('Could not get title for library item: %s', media_dict)
return None
def possibleTitles(raw_title):
titles = [
@@ -208,18 +267,116 @@ def possibleTitles(raw_title):
new_title = raw_title.replace('&', 'and')
titles.append(simplifyString(new_title))
return list(set(titles))
return removeDuplicate(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 = ',', clean = True):
list = [x.strip() for x in str.split(split_on)] if str else []
return filter(None, list) if clean else list
l = [x.strip() for x in str.split(split_on)] if str else []
return removeEmpty(l) if clean else l
def removeEmpty(l):
return list(filter(None, l))
def removeDuplicate(l):
seen = set()
return [x for x in l if x not in seen and not seen.add(x)]
def dictIsSubset(a, b):
return all([k in b and b[k] == v for k, v in a.items()])
# Returns True if sub_folder is the same as or inside base_folder
def isSubFolder(sub_folder, base_folder):
# Returns True is sub_folder is the same as or in base_folder
return base_folder.rstrip(os.path.sep) + os.path.sep in sub_folder.rstrip(os.path.sep) + os.path.sep
if base_folder and sub_folder:
base = sp(os.path.realpath(base_folder)) + os.path.sep
subfolder = sp(os.path.realpath(sub_folder)) + os.path.sep
return os.path.commonprefix([subfolder, base]) == base
return False
# From SABNZBD
re_password = [re.compile(r'(.+){{([^{}]+)}}$'), re.compile(r'(.+)\s+password\s*=\s*(.+)$', re.I)]
def scanForPassword(name):
m = None
for reg in re_password:
m = reg.search(name)
if m: break
if m:
return m.group(1).strip('. '), m.group(2).strip()
under_pat = re.compile(r'_([a-z])')
def underscoreToCamel(name):
return under_pat.sub(lambda x: x.group(1).upper(), name)
def removePyc(folder, only_excess = True, show_logs = True):
folder = sp(folder)
for root, dirs, files in os.walk(folder):
pyc_files = filter(lambda filename: filename.endswith('.pyc'), files)
py_files = set(filter(lambda filename: filename.endswith('.py'), files))
excess_pyc_files = filter(lambda pyc_filename: pyc_filename[:-1] not in py_files, pyc_files) if only_excess else pyc_files
for excess_pyc_file in excess_pyc_files:
full_path = os.path.join(root, excess_pyc_file)
if show_logs: log.debug('Removing old PYC file: %s', full_path)
try:
os.remove(full_path)
except:
log.error('Couldn\'t remove %s: %s', (full_path, traceback.format_exc()))
for dir_name in dirs:
full_path = os.path.join(root, dir_name)
if len(os.listdir(full_path)) == 0:
try:
os.rmdir(full_path)
except:
log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc()))
def getFreeSpace(directories):
single = not isinstance(directories, (tuple, list))
if single:
directories = [directories]
free_space = {}
for folder in 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()
return [total.value, free.value]
else:
s = os.statvfs(folder)
size = [s.f_blocks * s.f_frsize / (1024 * 1024), (s.f_bavail * s.f_frsize) / (1024 * 1024)]
if single: return size
free_space[folder] = size
return free_space

View File

@@ -1,30 +1,33 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.logger import CPLog
from importlib import import_module
import os
import sys
import traceback
from couchpotato.core.event import fireEvent
from couchpotato.core.logger import CPLog
from importhelper import import_module
import six
log = CPLog(__name__)
class Loader(object):
plugins = {}
providers = {}
modules = {}
def __init__(self):
self.plugins = {}
self.providers = {}
self.modules = {}
self.paths = {}
def preload(self, root = ''):
core = os.path.join(root, 'couchpotato', 'core')
self.paths = {
self.paths.update({
'core': (0, 'couchpotato.core._base', os.path.join(core, '_base')),
'plugin': (1, 'couchpotato.core.plugins', os.path.join(core, 'plugins')),
'notifications': (20, 'couchpotato.core.notifications', os.path.join(core, 'notifications')),
'downloaders': (20, 'couchpotato.core.downloaders', os.path.join(core, 'downloaders')),
}
# Add providers to loader
self.addPath(root, ['couchpotato', 'core', 'providers'], 25, recursive = False)
})
# Add media to loader
self.addPath(root, ['couchpotato', 'core', 'media'], 25, recursive = True)
@@ -37,7 +40,7 @@ class Loader(object):
self.paths['custom_plugins'] = (30, '', custom_plugin_dir)
# Loop over all paths and add to module list
for plugin_type, plugin_tuple in self.paths.iteritems():
for plugin_type, plugin_tuple in self.paths.items():
priority, module, dir_name = plugin_tuple
self.addFromDir(plugin_type, priority, module, dir_name)
@@ -45,7 +48,7 @@ class Loader(object):
did_save = 0
for priority in sorted(self.modules):
for module_name, plugin in sorted(self.modules[priority].iteritems()):
for module_name, plugin in sorted(self.modules[priority].items()):
# Load module
try:
@@ -56,12 +59,10 @@ class Loader(object):
if m is None:
continue
log.info('Loading %s: %s', (plugin['type'], plugin['name']))
# Save default settings for plugin/provider
did_save += self.loadSettings(m, module_name, save = False)
self.loadPlugins(m, plugin.get('name'))
self.loadPlugins(m, plugin.get('type'), plugin.get('name'))
except ImportError as e:
# todo:: subclass ImportError for missing requirements.
if e.message.lower().startswith("missing"):
@@ -81,7 +82,7 @@ class Loader(object):
for filename in os.listdir(root_path):
path = os.path.join(root_path, filename)
if os.path.isdir(path) and filename[:2] != '__':
if u'__init__.py' in os.listdir(path):
if six.u('__init__.py') in os.listdir(path):
new_base_path = ''.join(s + '.' for s in base_path) + filename
self.paths[new_base_path.replace('.', '_')] = (priority, new_base_path, path)
@@ -95,14 +96,19 @@ class Loader(object):
self.addModule(priority, plugin_type, module, os.path.basename(dir_name))
for name in os.listdir(dir_name):
if os.path.isdir(os.path.join(dir_name, name)) and name != 'static' and os.path.isfile(os.path.join(dir_name, name, '__init__.py')):
path = os.path.join(dir_name, name)
ext = os.path.splitext(path)[1]
ext_length = len(ext)
if name != 'static' and ((os.path.isdir(path) and os.path.isfile(os.path.join(path, '__init__.py')))
or (os.path.isfile(path) and ext == '.py')):
name = name[:-ext_length] if ext_length > 0 else name
module_name = '%s.%s' % (module, name)
self.addModule(priority, plugin_type, module_name, name)
def loadSettings(self, module, name, save = True):
if not hasattr(module, 'config'):
log.debug('Skip loading settings for plugin %s as it has no config section' % module.__file__)
#log.debug('Skip loading settings for plugin %s as it has no config section' % module.__file__)
return False
try:
@@ -118,13 +124,20 @@ class Loader(object):
log.debug('Failed loading settings for "%s": %s', (name, traceback.format_exc()))
return False
def loadPlugins(self, module, name):
def loadPlugins(self, module, type, name):
if not hasattr(module, 'start'):
log.debug('Skip startup for plugin %s as it has no start section' % module.__file__)
if not hasattr(module, 'autoload'):
#log.debug('Skip startup for plugin %s as it has no start section' % module.__file__)
return False
try:
module.start()
# Load single file plugin
if isinstance(module.autoload, (str, unicode)):
getattr(module, module.autoload)()
# Load folder plugin
else:
module.autoload()
log.info('Loaded %s: %s', (type, name))
return True
except:
log.error('Failed loading plugin "%s": %s', (module.__file__, traceback.format_exc()))
@@ -136,6 +149,9 @@ class Loader(object):
self.modules[priority] = {}
module = module.lstrip('.')
if plugin_type.startswith('couchpotato_core'):
plugin_type = plugin_type[17:]
self.modules[priority][module] = {
'priority': priority,
'module': module,

View File

@@ -1,11 +1,15 @@
import logging
import re
class CPLog(object):
context = ''
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key', 'passkey']
Env = None
is_develop = False
def __init__(self, context = ''):
if context.endswith('.main'):
context = context[:-5]
@@ -13,6 +17,20 @@ class CPLog(object):
self.context = context
self.logger = logging.getLogger()
def setup(self):
if not self.Env:
from couchpotato.environment import Env
self.Env = Env
self.is_develop = Env.get('dev')
from couchpotato.core.event import addEvent
addEvent('app.after_shutdown', self.close)
def close(self, *args, **kwargs):
logging.shutdown()
def info(self, msg, replace_tuple = ()):
self.logger.info(self.addContext(msg, replace_tuple))
@@ -36,8 +54,7 @@ class CPLog(object):
def safeMessage(self, msg, replace_tuple = ()):
from couchpotato.environment import Env
from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.encoding import ss, toUnicode
msg = ss(msg)
@@ -49,10 +66,11 @@ class CPLog(object):
msg = msg % tuple([ss(x) for x in list(replace_tuple)])
else:
msg = msg % ss(replace_tuple)
except Exception, e:
self.logger.error(u'Failed encoding stuff to log "%s": %s' % (msg, e))
except Exception as e:
self.logger.error('Failed encoding stuff to log "%s": %s' % (msg, e))
if not Env.get('dev'):
self.setup()
if not self.is_develop:
for replace in self.replace_private:
msg = re.sub('(\?%s=)[^\&]+' % replace, '?%s=xxx' % replace, msg)
@@ -60,10 +78,10 @@ class CPLog(object):
# Replace api key
try:
api_key = Env.setting('api_key')
api_key = self.Env.setting('api_key')
if api_key:
msg = msg.replace(api_key, 'API_KEY')
except:
pass
return msg
return toUnicode(msg)

View File

@@ -1,44 +1,98 @@
from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
import os
import traceback
from couchpotato import CPLog
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Media
import six
log = CPLog(__name__)
class MediaBase(Plugin):
_type = None
default_dict = {
'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
'library': {'titles': {}, 'files':{}},
'files': {},
'status': {},
'category': {},
}
def initType(self):
addEvent('media.types', self.getType)
def getType(self):
return self._type
def createOnComplete(self, id):
def createOnComplete(self, media_id):
def onComplete():
db = get_session()
media = db.query(Media).filter_by(id = id).first()
fireEventAsync('%s.searcher.single' % media.type, media.to_dict(self.default_dict), on_complete = self.createNotifyFront(id))
db.expire_all()
try:
media = fireEvent('media.get', media_id, single = True)
event_name = '%s.searcher.single' % media.get('type')
fireEventAsync(event_name, media, on_complete = self.createNotifyFront(media_id), manual = True)
except:
log.error('Failed creating onComplete: %s', traceback.format_exc())
return onComplete
def createNotifyFront(self, media_id):
def notifyFront():
db = get_session()
media = db.query(Media).filter_by(id = media_id).first()
fireEvent('notify.frontend', type = '%s.update' % media.type, data = media.to_dict(self.default_dict))
db.expire_all()
try:
media = fireEvent('media.get', media_id, single = True)
event_name = '%s.update' % media.get('type')
fireEvent('notify.frontend', type = event_name, data = media)
except:
log.error('Failed creating onComplete: %s', traceback.format_exc())
return notifyFront
def getDefaultTitle(self, info, ):
# Set default title
default_title = toUnicode(info.get('title'))
titles = info.get('titles', [])
counter = 0
def_title = None
for title in titles:
if (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == six.u('') and toUnicode(titles[0]) == title):
def_title = toUnicode(title)
break
counter += 1
if not def_title:
def_title = toUnicode(titles[0])
return def_title or 'UNKNOWN'
def getPoster(self, image_urls, existing_files):
image_type = 'poster'
# Remove non-existing files
file_type = 'image_%s' % image_type
# Make existing unique
unique_files = list(set(existing_files.get(file_type, [])))
# Remove files that can't be found
for ef in unique_files:
if not os.path.isfile(ef):
unique_files.remove(ef)
# Replace new files list
existing_files[file_type] = unique_files
if len(existing_files) == 0:
del existing_files[file_type]
# Loop over type
for image in image_urls.get(image_type, []):
if not isinstance(image, (str, unicode)):
continue
if file_type not in existing_files or len(existing_files.get(file_type, [])) == 0:
file_path = fireEvent('file.download', url = image, single = True)
if file_path:
existing_files[file_type] = [file_path]
break
else:
break

View File

@@ -1,13 +1,7 @@
from couchpotato.core.event import addEvent
from couchpotato.core.plugins.base import Plugin
from .main import Library
class LibraryBase(Plugin):
def autoload():
return Library()
_type = None
def initType(self):
addEvent('library.types', self.getType)
def getType(self):
return self._type
config = []

View File

@@ -0,0 +1,13 @@
from couchpotato.core.event import addEvent
from couchpotato.core.plugins.base import Plugin
class LibraryBase(Plugin):
_type = None
def initType(self):
addEvent('library.types', self.getType)
def getType(self):
return self._type

View File

@@ -0,0 +1,18 @@
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.media._base.library.base import LibraryBase
class Library(LibraryBase):
def __init__(self):
addEvent('library.title', self.title)
def title(self, library):
return fireEvent(
'library.query',
library,
condense = False,
include_year = False,
include_identifier = False,
single = True
)

View File

@@ -0,0 +1,7 @@
from .main import Matcher
def autoload():
return Matcher()
config = []

View File

@@ -0,0 +1,84 @@
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import simplifyString
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
log = CPLog(__name__)
class MatcherBase(Plugin):
type = None
def __init__(self):
if self.type:
addEvent('%s.matcher.correct' % self.type, self.correct)
def correct(self, chain, release, media, quality):
raise NotImplementedError()
def flattenInfo(self, info):
# Flatten dictionary of matches (chain info)
if isinstance(info, dict):
return dict([(key, self.flattenInfo(value)) for key, value in info.items()])
# Flatten matches
result = None
for match in info:
if isinstance(match, dict):
if result is None:
result = {}
for key, value in match.items():
if key not in result:
result[key] = []
result[key].append(value)
else:
if result is None:
result = []
result.append(match)
return result
def constructFromRaw(self, match):
if not match:
return None
parts = [
''.join([
y for y in x[1:] if y
]) for x in match
]
return ''.join(parts)[:-1].strip()
def simplifyValue(self, value):
if not value:
return value
if isinstance(value, basestring):
return simplifyString(value)
if isinstance(value, list):
return [self.simplifyValue(x) for x in value]
raise ValueError("Unsupported value type")
def chainMatch(self, chain, group, tags):
info = self.flattenInfo(chain.info[group])
found_tags = []
for tag, accepted in tags.items():
values = [self.simplifyValue(x) for x in info.get(tag, [None])]
if any([val in accepted for val in values]):
found_tags.append(tag)
log.debug('tags found: %s, required: %s' % (found_tags, tags.keys()))
if set(tags.keys()) == set(found_tags):
return True
return all([key in found_tags for key, value in tags.items()])

View File

@@ -0,0 +1,89 @@
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import possibleTitles
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.matcher.base import MatcherBase
from caper import Caper
log = CPLog(__name__)
class Matcher(MatcherBase):
def __init__(self):
super(Matcher, self).__init__()
self.caper = Caper()
addEvent('matcher.parse', self.parse)
addEvent('matcher.match', self.match)
addEvent('matcher.flatten_info', self.flattenInfo)
addEvent('matcher.construct_from_raw', self.constructFromRaw)
addEvent('matcher.correct_title', self.correctTitle)
addEvent('matcher.correct_quality', self.correctQuality)
def parse(self, name, parser='scene'):
return self.caper.parse(name, parser)
def match(self, release, media, quality):
match = fireEvent('matcher.parse', release['name'], single = True)
if len(match.chains) < 1:
log.info2('Wrong: %s, unable to parse release name (no chains)', release['name'])
return False
for chain in match.chains:
if fireEvent('%s.matcher.correct' % media['type'], chain, release, media, quality, single = True):
return chain
return False
def correctTitle(self, chain, media):
root_library = media['library']['root_library']
if 'show_name' not in chain.info or not len(chain.info['show_name']):
log.info('Wrong: missing show name in parsed result')
return False
# Get the lower-case parsed show name from the chain
chain_words = [x.lower() for x in chain.info['show_name']]
# Build a list of possible titles of the media we are searching for
titles = root_library['info']['titles']
# Add year suffix titles (will result in ['<name_one>', '<name_one> <suffix_one>', '<name_two>', ...])
suffixes = [None, root_library['info']['year']]
titles = [
title + ((' %s' % suffix) if suffix else '')
for title in titles
for suffix in suffixes
]
# Check show titles match
# TODO check xem names
for title in titles:
for valid_words in [x.split(' ') for x in possibleTitles(title)]:
if valid_words == chain_words:
return True
return False
def correctQuality(self, chain, quality, quality_map):
if quality['identifier'] not in quality_map:
log.info2('Wrong: unknown preferred quality %s', quality['identifier'])
return False
if 'video' not in chain.info:
log.info2('Wrong: no video tags found')
return False
video_tags = quality_map[quality['identifier']]
if not self.chainMatch(chain, 'video', video_tags):
log.info2('Wrong: %s tags not in chain', video_tags)
return False
return True

View File

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

View File

@@ -0,0 +1,199 @@
from string import ascii_letters
from hashlib import md5
from CodernityDB.tree_index import MultiTreeBasedIndex, TreeBasedIndex
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
class MediaIndex(MultiTreeBasedIndex):
_version = 3
custom_header = """from CodernityDB.tree_index import MultiTreeBasedIndex"""
def __init__(self, *args, **kwargs):
kwargs['key_format'] = '32s'
super(MediaIndex, self).__init__(*args, **kwargs)
def make_key(self, key):
return md5(key).hexdigest()
def make_key_value(self, data):
if data.get('_t') == 'media' and (data.get('identifier') or data.get('identifiers')):
identifiers = data.get('identifiers', {})
if data.get('identifier') and 'imdb' not in identifiers:
identifiers['imdb'] = data.get('identifier')
ids = []
for x in identifiers:
ids.append(md5('%s-%s' % (x, identifiers[x])).hexdigest())
return ids, None
class MediaStatusIndex(TreeBasedIndex):
_version = 1
def __init__(self, *args, **kwargs):
kwargs['key_format'] = '32s'
super(MediaStatusIndex, self).__init__(*args, **kwargs)
def make_key(self, key):
return md5(key).hexdigest()
def make_key_value(self, data):
if data.get('_t') == 'media' and data.get('status'):
return md5(data.get('status')).hexdigest(), None
class MediaTypeIndex(TreeBasedIndex):
_version = 1
def __init__(self, *args, **kwargs):
kwargs['key_format'] = '32s'
super(MediaTypeIndex, self).__init__(*args, **kwargs)
def make_key(self, key):
return md5(key).hexdigest()
def make_key_value(self, data):
if data.get('_t') == 'media' and data.get('type'):
return md5(data.get('type')).hexdigest(), None
class TitleSearchIndex(MultiTreeBasedIndex):
_version = 1
custom_header = """from CodernityDB.tree_index import MultiTreeBasedIndex
from itertools import izip
from couchpotato.core.helpers.encoding import simplifyString"""
def __init__(self, *args, **kwargs):
kwargs['key_format'] = '32s'
super(TitleSearchIndex, self).__init__(*args, **kwargs)
self.__l = kwargs.get('w_len', 2)
def make_key_value(self, data):
if data.get('_t') == 'media' and len(data.get('title', '')) > 0:
out = set()
title = str(simplifyString(data.get('title').lower()))
l = self.__l
title_split = title.split()
for x in range(len(title_split)):
combo = ' '.join(title_split[x:])[:32].strip()
out.add(combo.rjust(32, '_'))
combo_range = max(l, min(len(combo), 32))
for cx in range(1, combo_range):
ccombo = combo[:-cx].strip()
if len(ccombo) > l:
out.add(ccombo.rjust(32, '_'))
return out, None
def make_key(self, key):
return key.rjust(32, '_').lower()
class TitleIndex(TreeBasedIndex):
_version = 4
custom_header = """from CodernityDB.tree_index import TreeBasedIndex
from string import ascii_letters
from couchpotato.core.helpers.encoding import toUnicode, simplifyString"""
def __init__(self, *args, **kwargs):
kwargs['key_format'] = '32s'
super(TitleIndex, self).__init__(*args, **kwargs)
def make_key(self, key):
return self.simplify(key)
def make_key_value(self, data):
if data.get('_t') == 'media' and data.get('title') is not None and len(data.get('title')) > 0:
return self.simplify(data['title']), None
def simplify(self, title):
title = toUnicode(title)
nr_prefix = '' if title and len(title) > 0 and title[0] in ascii_letters else '#'
title = simplifyString(title)
for prefix in ['the ', 'an ', 'a ']:
if prefix == title[:len(prefix)]:
title = title[len(prefix):]
break
return str(nr_prefix + title).ljust(32, ' ')[:32]
class StartsWithIndex(TreeBasedIndex):
_version = 3
custom_header = """from CodernityDB.tree_index import TreeBasedIndex
from string import ascii_letters
from couchpotato.core.helpers.encoding import toUnicode, simplifyString"""
def __init__(self, *args, **kwargs):
kwargs['key_format'] = '1s'
super(StartsWithIndex, self).__init__(*args, **kwargs)
def make_key(self, key):
return self.first(key)
def make_key_value(self, data):
if data.get('_t') == 'media' and data.get('title') is not None:
return self.first(data['title']), None
def first(self, title):
title = toUnicode(title)
title = simplifyString(title)
for prefix in ['the ', 'an ', 'a ']:
if prefix == title[:len(prefix)]:
title = title[len(prefix):]
break
return str(title[0] if title and len(title) > 0 and title[0] in ascii_letters else '#').lower()
class MediaChildrenIndex(TreeBasedIndex):
_version = 1
def __init__(self, *args, **kwargs):
kwargs['key_format'] = '32s'
super(MediaChildrenIndex, self).__init__(*args, **kwargs)
def make_key(self, key):
return key
def make_key_value(self, data):
if data.get('_t') == 'media' and data.get('parent_id'):
return data.get('parent_id'), None
class MediaTagIndex(MultiTreeBasedIndex):
_version = 2
custom_header = """from CodernityDB.tree_index import MultiTreeBasedIndex"""
def __init__(self, *args, **kwargs):
kwargs['key_format'] = '32s'
super(MediaTagIndex, self).__init__(*args, **kwargs)
def make_key_value(self, data):
if data.get('_t') == 'media' and data.get('tags') and len(data.get('tags', [])) > 0:
tags = set()
for tag in data.get('tags', []):
tags.add(self.make_key(tag))
return list(tags), None
def make_key(self, key):
return md5(key).hexdigest()

View File

@@ -1,21 +1,36 @@
from couchpotato import get_session
from datetime import timedelta
from operator import itemgetter
import time
import traceback
from string import ascii_lowercase
from CodernityDB.database import RecordNotFound
from couchpotato import tryInt, get_db
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import mergeDicts, splitString, getImdb
from couchpotato.core.helpers.variable import splitString, getImdb, getTitle
from couchpotato.core.logger import CPLog
from couchpotato.core.media import MediaBase
from couchpotato.core.settings.model import Library, LibraryTitle, Release, \
Media
from sqlalchemy.orm import joinedload_all
from sqlalchemy.sql.expression import or_, asc, not_, desc
from string import ascii_lowercase
from .index import MediaIndex, MediaStatusIndex, MediaTypeIndex, TitleSearchIndex, TitleIndex, StartsWithIndex, MediaChildrenIndex, MediaTagIndex
log = CPLog(__name__)
class MediaPlugin(MediaBase):
_database = {
'media': MediaIndex,
'media_search_title': TitleSearchIndex,
'media_status': MediaStatusIndex,
'media_tag': MediaTagIndex,
'media_by_type': MediaTypeIndex,
'media_title': TitleIndex,
'media_startswith': StartsWithIndex,
'media_children': MediaChildrenIndex,
}
def __init__(self):
addApiView('media.refresh', self.refresh, docs = {
@@ -59,37 +74,51 @@ class MediaPlugin(MediaBase):
addApiView('media.available_chars', self.charView)
addEvent('app.load', self.addSingleRefreshView)
addEvent('app.load', self.addSingleListView)
addEvent('app.load', self.addSingleCharView)
addEvent('app.load', self.addSingleDeleteView)
addEvent('app.load', self.addSingleRefreshView, priority = 100)
addEvent('app.load', self.addSingleListView, priority = 100)
addEvent('app.load', self.addSingleCharView, priority = 100)
addEvent('app.load', self.addSingleDeleteView, priority = 100)
addEvent('media.get', self.get)
addEvent('media.with_status', self.withStatus)
addEvent('media.with_identifiers', self.withIdentifiers)
addEvent('media.list', self.list)
addEvent('media.delete', self.delete)
addEvent('media.restatus', self.restatus)
addEvent('media.tag', self.tag)
addEvent('media.untag', self.unTag)
def refresh(self, id = '', **kwargs):
db = get_session()
handlers = []
ids = splitString(id)
for x in splitString(id):
media = db.query(Media).filter_by(id = x).first()
for x in ids:
if media:
# Get current selected title
default_title = ''
for title in media.library.titles:
if title.default: default_title = title.title
refresh_handler = self.createRefreshHandler(x)
if refresh_handler:
handlers.append(refresh_handler)
fireEvent('notify.frontend', type = '%s.busy' % media.type, data = {'id': x})
fireEventAsync('library.update.%s' % media.type, identifier = media.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x))
db.expire_all()
fireEvent('notify.frontend', type = 'media.busy', data = {'_id': ids})
fireEventAsync('schedule.queue', handlers = handlers)
return {
'success': True,
}
def createRefreshHandler(self, media_id):
try:
media = get_db().get('id', media_id)
event = '%s.update_info' % media.get('type')
def handler():
fireEvent(event, media_id = media_id, on_complete = self.createOnComplete(media_id))
return handler
except:
log.error('Refresh handler for non existing media: %s', traceback.format_exc())
def addSingleRefreshView(self):
for media_type in fireEvent('media.types', merge = True):
@@ -97,21 +126,30 @@ class MediaPlugin(MediaBase):
def get(self, media_id):
db = get_session()
try:
db = get_db()
imdb_id = getImdb(str(media_id))
imdb_id = getImdb(str(media_id))
if imdb_id:
m = db.query(Media).filter(Media.library.has(identifier = imdb_id)).first()
else:
m = db.query(Media).filter_by(id = media_id).first()
if imdb_id:
media = db.get('media', 'imdb-%s' % imdb_id, with_doc = True)['doc']
else:
media = db.get('id', media_id)
results = None
if m:
results = m.to_dict(self.default_dict)
if media:
db.expire_all()
return results
# Attach category
try: media['category'] = db.get('id', media.get('category_id'))
except: pass
media['releases'] = fireEvent('release.for_media', media['_id'], single = True)
return media
except RecordNotFound:
log.error('Media with id "%s" not found', media_id)
except:
raise
def getView(self, id = None, **kwargs):
@@ -122,9 +160,39 @@ class MediaPlugin(MediaBase):
'media': media,
}
def list(self, types = None, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None):
def withStatus(self, status, with_doc = True):
db = get_session()
db = get_db()
status = list(status if isinstance(status, (list, tuple)) else [status])
for s in status:
for ms in db.get_many('media_status', s):
if with_doc:
try:
doc = db.get('id', ms['_id'])
yield doc
except RecordNotFound:
log.debug('Record not found, skipping: %s', ms['_id'])
else:
yield ms
def withIdentifiers(self, identifiers, with_doc = False):
db = get_db()
for x in identifiers:
try:
media = db.get('media', '%s-%s' % (x, identifiers[x]), with_doc = with_doc)
return media
except:
pass
log.debug('No media found with identifiers: %s', identifiers)
def list(self, types = None, status = None, release_status = None, status_or = False, limit_offset = None, with_tags = None, starts_with = None, search = None):
db = get_db()
# Make a list from string
if status and not isinstance(status, (list, tuple)):
@@ -133,140 +201,100 @@ class MediaPlugin(MediaBase):
release_status = [release_status]
if types and not isinstance(types, (list, tuple)):
types = [types]
if with_tags and not isinstance(with_tags, (list, tuple)):
with_tags = [with_tags]
# query movie ids
q = db.query(Media) \
.with_entities(Media.id) \
.group_by(Media.id)
# query media ids
if types:
all_media_ids = set()
for media_type in types:
all_media_ids = all_media_ids.union(set([x['_id'] for x in db.get_many('media_by_type', media_type)]))
else:
all_media_ids = set([x['_id'] for x in db.all('media')])
media_ids = list(all_media_ids)
filter_by = {}
# Filter on movie status
if status and len(status) > 0:
statuses = fireEvent('status.get', status, single = len(status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.filter(Media.status_id.in_(statuses))
filter_by['media_status'] = set()
for media_status in fireEvent('media.with_status', status, with_doc = False, single = True):
filter_by['media_status'].add(media_status.get('_id'))
# Filter on release status
if release_status and len(release_status) > 0:
q = q.join(Media.releases)
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.filter(Release.status_id.in_(statuses))
# Filter on type
if types and len(types) > 0:
try: q = q.filter(Media.type.in_(types))
except: pass
# Only join when searching / ordering
if starts_with or search or order != 'release_order':
q = q.join(Media.library, Library.titles) \
.filter(LibraryTitle.default == True)
filter_by['release_status'] = set()
for release_status in fireEvent('release.with_status', release_status, with_doc = False, single = True):
filter_by['release_status'].add(release_status.get('media_id'))
# Add search filters
filter_or = []
if starts_with:
starts_with = toUnicode(starts_with.lower())
if starts_with in ascii_lowercase:
filter_or.append(LibraryTitle.simple_title.startswith(starts_with))
else:
ignore = []
for letter in ascii_lowercase:
ignore.append(LibraryTitle.simple_title.startswith(toUnicode(letter)))
filter_or.append(not_(or_(*ignore)))
starts_with = toUnicode(starts_with.lower())[0]
starts_with = starts_with if starts_with in ascii_lowercase else '#'
filter_by['starts_with'] = [x['_id'] for x in db.get_many('media_startswith', starts_with)]
# Add tag filter
if with_tags:
filter_by['with_tags'] = set()
for tag in with_tags:
for x in db.get_many('media_tag', tag):
filter_by['with_tags'].add(x['_id'])
# Filter with search query
if search:
filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%'))
filter_by['search'] = [x['_id'] for x in db.get_many('media_search_title', search)]
if len(filter_or) > 0:
q = q.filter(or_(*filter_or))
if status_or and 'media_status' in filter_by and 'release_status' in filter_by:
filter_by['status'] = list(filter_by['media_status']) + list(filter_by['release_status'])
del filter_by['media_status']
del filter_by['release_status']
total_count = q.count()
# Filter by combining ids
for x in filter_by:
media_ids = [n for n in media_ids if n in filter_by[x]]
total_count = len(media_ids)
if total_count == 0:
return 0, []
if order == 'release_order':
q = q.order_by(desc(Release.last_edit))
else:
q = q.order_by(asc(LibraryTitle.simple_title))
offset = 0
limit = -1
if limit_offset:
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
limit = splt[0]
offset = 0 if len(splt) is 1 else splt[1]
q = q.limit(limit).offset(offset)
limit = tryInt(splt[0])
offset = tryInt(0 if len(splt) is 1 else splt[1])
# Get all media_ids in sorted order
media_ids = [m.id for m in q.all()]
# List movies based on title order
medias = []
for m in db.all('media_title'):
media_id = m['_id']
if media_id not in media_ids: continue
if offset > 0:
offset -= 1
continue
# List release statuses
releases = db.query(Release) \
.filter(Release.movie_id.in_(media_ids)) \
.all()
release_statuses = dict((m, set()) for m in media_ids)
releases_count = dict((m, 0) for m in media_ids)
for release in releases:
release_statuses[release.movie_id].add('%d,%d' % (release.status_id, release.quality_id))
releases_count[release.movie_id] += 1
# Get main movie data
q2 = db.query(Media) \
.options(joinedload_all('library.titles')) \
.options(joinedload_all('library.files')) \
.options(joinedload_all('status')) \
.options(joinedload_all('files'))
q2 = q2.filter(Media.id.in_(media_ids))
results = q2.all()
# Create dict by movie id
movie_dict = {}
for movie in results:
movie_dict[movie.id] = movie
# List movies based on media_ids order
movies = []
for media_id in media_ids:
releases = []
for r in release_statuses.get(media_id):
x = splitString(r)
releases.append({'status_id': x[0], 'quality_id': x[1]})
media = fireEvent('media.get', media_id, single = True)
# Merge releases with movie dict
movies.append(mergeDicts(movie_dict[media_id].to_dict({
'library': {'titles': {}, 'files':{}},
'files': {},
}), {
'releases': releases,
'releases_count': releases_count.get(media_id),
}))
medias.append(media)
db.expire_all()
return total_count, movies
# remove from media ids
media_ids.remove(media_id)
if len(media_ids) == 0 or len(medias) == limit: break
return total_count, medias
def listView(self, **kwargs):
types = splitString(kwargs.get('types'))
status = splitString(kwargs.get('status'))
release_status = splitString(kwargs.get('release_status'))
limit_offset = kwargs.get('limit_offset')
starts_with = kwargs.get('starts_with')
search = kwargs.get('search')
order = kwargs.get('order')
total_movies, movies = self.list(
types = types,
status = status,
release_status = release_status,
limit_offset = limit_offset,
starts_with = starts_with,
search = search,
order = order
types = splitString(kwargs.get('type')),
status = splitString(kwargs.get('status')),
release_status = splitString(kwargs.get('release_status')),
status_or = kwargs.get('status_or') is not None,
limit_offset = kwargs.get('limit_offset'),
with_tags = splitString(kwargs.get('with_tags')),
starts_with = kwargs.get('starts_with'),
search = kwargs.get('search')
)
return {
@@ -280,68 +308,57 @@ class MediaPlugin(MediaBase):
for media_type in fireEvent('media.types', merge = True):
def tempList(*args, **kwargs):
return self.listView(types = media_type, *args, **kwargs)
return self.listView(types = media_type, **kwargs)
addApiView('%s.list' % media_type, tempList)
def availableChars(self, types = None, status = None, release_status = None):
types = types or []
status = status or []
release_status = release_status or []
db = get_session()
db = get_db()
# 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]
if types and not isinstance(types, (list, tuple)):
types = [types]
q = db.query(Media)
# query media ids
if types:
all_media_ids = set()
for media_type in types:
all_media_ids = all_media_ids.union(set([x['_id'] for x in db.get_many('media_by_type', media_type)]))
else:
all_media_ids = set([x['_id'] for x in db.all('media')])
media_ids = all_media_ids
filter_by = {}
# Filter on movie status
if status and len(status) > 0:
statuses = fireEvent('status.get', status, single = len(release_status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.filter(Media.status_id.in_(statuses))
filter_by['media_status'] = set()
for media_status in fireEvent('media.with_status', status, with_doc = False, single = True):
filter_by['media_status'].add(media_status.get('_id'))
# Filter on release status
if release_status and len(release_status) > 0:
filter_by['release_status'] = set()
for release_status in fireEvent('release.with_status', release_status, with_doc = False, single = True):
filter_by['release_status'].add(release_status.get('media_id'))
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.join(Media.releases) \
.filter(Release.status_id.in_(statuses))
# Filter on type
if types and len(types) > 0:
try: q = q.filter(Media.type.in_(types))
except: pass
q = q.join(Library, LibraryTitle) \
.with_entities(LibraryTitle.simple_title) \
.filter(LibraryTitle.default == True)
titles = q.all()
# Filter by combining ids
for x in filter_by:
media_ids = [n for n in media_ids if n in filter_by[x]]
chars = set()
for title in titles:
try:
char = title[0][0]
char = char if char in ascii_lowercase else '#'
chars.add(str(char))
except:
log.error('Failed getting title for %s', title.libraries_id)
for x in db.all('media_startswith'):
if x['_id'] in media_ids:
chars.add(x['key'])
if len(chars) == 25:
break
db.expire_all()
return ''.join(sorted(chars))
return list(chars)
def charView(self, **kwargs):
@@ -360,55 +377,60 @@ class MediaPlugin(MediaBase):
for media_type in fireEvent('media.types', merge = True):
def tempChar(*args, **kwargs):
return self.charView(types = media_type, *args, **kwargs)
return self.charView(types = media_type, **kwargs)
addApiView('%s.available_chars' % media_type, tempChar)
def delete(self, media_id, delete_from = None):
db = get_session()
try:
db = get_db()
media = db.query(Media).filter_by(id = media_id).first()
if media:
deleted = False
if delete_from == 'all':
db.delete(media)
db.commit()
deleted = True
else:
done_status = fireEvent('status.get', 'done', single = True)
media = db.get('id', media_id)
if media:
deleted = False
total_releases = len(media.releases)
total_deleted = 0
new_movie_status = None
for release in media.releases:
if delete_from in ['wanted', 'snatched', 'late']:
if release.status_id != done_status.get('id'):
db.delete(release)
total_deleted += 1
new_movie_status = 'done'
elif delete_from == 'manage':
if release.status_id == done_status.get('id'):
db.delete(release)
total_deleted += 1
new_movie_status = 'active'
db.commit()
media_releases = fireEvent('release.for_media', media['_id'], single = True)
if delete_from == 'all':
# Delete connected releases
for release in media_releases:
db.delete(release)
if total_releases == total_deleted:
db.delete(media)
db.commit()
deleted = True
elif new_movie_status:
new_status = fireEvent('status.get', new_movie_status, single = True)
media.profile_id = None
media.status_id = new_status.get('id')
db.commit()
else:
fireEvent('media.restatus', media.id, single = True)
if deleted:
fireEvent('notify.frontend', type = 'movie.deleted', data = media.to_dict())
total_releases = len(media_releases)
total_deleted = 0
new_media_status = None
for release in media_releases:
if delete_from in ['wanted', 'snatched', 'late']:
if release.get('status') != 'done':
db.delete(release)
total_deleted += 1
new_media_status = 'done'
elif delete_from == 'manage':
if release.get('status') == 'done' or media.get('status') == 'done':
db.delete(release)
total_deleted += 1
if (total_releases == total_deleted and media['status'] != 'active') or (total_releases == 0 and not new_media_status) or (not new_media_status and delete_from == 'late'):
db.delete(media)
deleted = True
elif new_media_status:
media['status'] = new_media_status
db.update(media)
fireEvent('media.untag', media['_id'], 'recent', single = True)
else:
fireEvent('media.restatus', media.get('_id'), single = True)
if deleted:
fireEvent('notify.frontend', type = 'media.deleted', data = media)
except:
log.error('Failed deleting media: %s', traceback.format_exc())
db.expire_all()
return True
def deleteView(self, id = '', **kwargs):
@@ -430,29 +452,82 @@ class MediaPlugin(MediaBase):
def restatus(self, media_id):
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
try:
db = get_db()
db = get_session()
m = db.get('id', media_id)
previous_status = m['status']
m = db.query(Media).filter_by(id = media_id).first()
if not m or len(m.library.titles) == 0:
log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
return False
log.debug('Changing status for %s', getTitle(m))
if not m['profile_id']:
m['status'] = 'done'
else:
m['status'] = 'active'
log.debug('Changing status for %s', m.library.titles[0].title)
if not m.profile:
m.status_id = done_status.get('id')
else:
move_to_wanted = True
try:
profile = db.get('id', m['profile_id'])
media_releases = fireEvent('release.for_media', m['_id'], single = True)
done_releases = [release for release in media_releases if release.get('status') == 'done']
for t in m.profile.types:
for release in m.releases:
if t.quality.identifier is release.quality.identifier and (release.status_id is done_status.get('id') and t.finish):
move_to_wanted = False
if done_releases:
# Only look at latest added release
release = sorted(done_releases, key = itemgetter('last_edit'), reverse = True)[0]
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
# Check if we are finished with the media
if fireEvent('quality.isfinish', {'identifier': release['quality'], 'is_3d': release.get('is_3d', False)}, profile, timedelta(seconds = time.time() - release['last_edit']).days, single = True):
m['status'] = 'done'
elif previous_status == 'done':
m['status'] = 'done'
db.commit()
except RecordNotFound:
log.debug('Failed restatus, keeping previous: %s', traceback.format_exc())
m['status'] = previous_status
return True
# Only update when status has changed
if previous_status != m['status']:
db.update(m)
# Tag media as recent
self.tag(media_id, 'recent')
return m['status']
except:
log.error('Failed restatus: %s', traceback.format_exc())
def tag(self, media_id, tag):
try:
db = get_db()
m = db.get('id', media_id)
tags = m.get('tags') or []
if tag not in tags:
tags.append(tag)
m['tags'] = tags
db.update(m)
return True
except:
log.error('Failed tagging: %s', traceback.format_exc())
return False
def unTag(self, media_id, tag):
try:
db = get_db()
m = db.get('id', media_id)
tags = m.get('tags') or []
if tag in tags:
new_tags = list(set(tags))
new_tags.remove(tag)
m['tags'] = new_tags
db.update(m)
return True
except:
log.error('Failed untagging: %s', traceback.format_exc())
return False

View File

@@ -0,0 +1,8 @@
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.base import Provider
log = CPLog(__name__)
class AutomationBase(Provider):
pass

View File

@@ -1,9 +1,3 @@
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import tryFloat, mergeDicts, md5, \
possibleTitles, getTitle
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
from urlparse import urlparse
import json
import re
@@ -11,8 +5,18 @@ import time
import traceback
import xml.etree.ElementTree as XMLTree
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.variable import tryFloat, mergeDicts, md5, \
possibleTitles
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
log = CPLog(__name__)
class MultiProvider(Plugin):
def __init__(self):
@@ -35,8 +39,8 @@ class MultiProvider(Plugin):
class Provider(Plugin):
type = None # movie, show, subtitle, trailer, ...
http_time_between_calls = 10 # Default timeout for url requests
type = None # movie, show, subtitle, trailer, ...
http_time_between_calls = 10 # Default timeout for url requests
last_available_check = {}
is_available = {}
@@ -62,7 +66,7 @@ class Provider(Plugin):
def getJsonData(self, url, decode_from = None, **kwargs):
cache_key = '%s%s' % (md5(url), md5('%s' % kwargs.get('params', {})))
cache_key = md5(url)
data = self.getCache(cache_key, url, **kwargs)
if data:
@@ -79,7 +83,7 @@ class Provider(Plugin):
def getRSSData(self, url, item_path = 'channel/item', **kwargs):
cache_key = '%s%s' % (md5(url), md5('%s' % kwargs.get('params', {})))
cache_key = md5(url)
data = self.getCache(cache_key, url, **kwargs)
if data and len(data) > 0:
@@ -87,27 +91,30 @@ class Provider(Plugin):
data = XMLTree.fromstring(data)
return self.getElements(data, item_path)
except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
try:
data = XMLTree.fromstring(ss(data))
return self.getElements(data, item_path)
except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
return []
def getHTMLData(self, url, **kwargs):
cache_key = '%s%s' % (md5(url), md5('%s' % kwargs.get('data', {})))
cache_key = md5(url)
return self.getCache(cache_key, url, **kwargs)
class YarrProvider(Provider):
protocol = None # nzb, torrent, torrent_magnet
type = 'movie'
protocol = None # nzb, torrent, torrent_magnet
cat_ids = {}
cat_backup_id = None
sizeGb = ['gb', 'gib']
sizeMb = ['mb', 'mib']
sizeKb = ['kb', 'kib']
size_gb = ['gb', 'gib']
size_mb = ['mb', 'mib']
size_kb = ['kb', 'kib']
last_login_check = None
@@ -122,6 +129,9 @@ class YarrProvider(Provider):
else:
return []
def buildUrl(self, *args, **kwargs):
pass
def login(self):
# Check if we are still logged in every hour
@@ -174,11 +184,11 @@ class YarrProvider(Provider):
try:
return self.urlopen(url, headers = {'User-Agent': Env.getIdentifier()}, show_error = False)
except:
log.error('Failed getting nzb from %s: %s', (self.getName(), traceback.format_exc()))
log.error('Failed getting release from %s: %s', (self.getName(), traceback.format_exc()))
return 'try_next'
def search(self, movie, quality):
def search(self, media, quality):
if self.isDisabled():
return []
@@ -190,15 +200,17 @@ class YarrProvider(Provider):
# Create result container
imdb_results = hasattr(self, '_search')
results = ResultList(self, movie, quality, imdb_results = imdb_results)
results = ResultList(self, media, quality, imdb_results = imdb_results)
# Do search based on imdb id
if imdb_results:
self._search(movie, quality, results)
self._search(media, quality, results)
# Search possible titles
else:
for title in possibleTitles(getTitle(movie['library'])):
self._searchOnTitle(title, movie, quality, results)
media_title = fireEvent('library.query', media, include_year = False, single = True)
for title in possibleTitles(media_title):
self._searchOnTitle(title, media, quality, results)
return results
@@ -222,28 +234,33 @@ class YarrProvider(Provider):
def parseSize(self, size):
sizeRaw = size.lower()
size_raw = size.lower()
size = tryFloat(re.sub(r'[^0-9.]', '', size).strip())
for s in self.sizeGb:
if s in sizeRaw:
for s in self.size_gb:
if s in size_raw:
return size * 1024
for s in self.sizeMb:
if s in sizeRaw:
for s in self.size_mb:
if s in size_raw:
return size
for s in self.sizeKb:
if s in sizeRaw:
for s in self.size_kb:
if s in size_raw:
return size / 1024
return 0
def getCatId(self, identifier):
def getCatId(self, quality = None):
if not quality: quality = {}
identifier = quality.get('identifier')
for cats in self.cat_ids:
ids, qualities = cats
if identifier in qualities:
want_3d = False
if quality.get('custom'):
want_3d = quality['custom'].get('3d')
for ids, qualities in self.cat_ids:
if identifier in qualities or (want_3d and '3d' in qualities):
return ids
if self.cat_backup_id:
@@ -278,7 +295,7 @@ class ResultList(list):
new_result = self.fillResult(result)
is_correct = fireEvent('searcher.correct_release', new_result, self.media, self.quality,
imdb_results = self.kwargs.get('imdb_results', False), single = True)
imdb_results = self.kwargs.get('imdb_results', False), single = True)
if is_correct and new_result['id'] not in self.result_ids:
is_correct_weight = float(is_correct)
@@ -288,7 +305,7 @@ class ResultList(list):
old_score = new_result['score']
new_result['score'] = int(old_score * is_correct_weight)
log.info('Found correct release with weight %.02f, old_score(%d) now scaled to score(%d)', (
log.info2('Found correct release with weight %.02f, old_score(%d) now scaled to score(%d)', (
is_correct_weight,
old_score,
new_result['score']

View File

@@ -0,0 +1,5 @@
from couchpotato.core.media._base.providers.base import Provider
class BaseInfoProvider(Provider):
type = 'unknown'

View File

@@ -0,0 +1,8 @@
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
log = CPLog(__name__)
class MetaDataBase(Plugin):
pass

View File

@@ -1,6 +1,7 @@
from couchpotato.core.providers.base import YarrProvider
import time
from couchpotato.core.media._base.providers.base import YarrProvider
class NZBProvider(YarrProvider):

View File

@@ -1,16 +1,16 @@
from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider
from couchpotato.environment import Env
import re
import traceback
from bs4 import BeautifulSoup
from couchpotato.core.helpers.variable import tryInt, simplifyString
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.nzb.base import NZBProvider
log = CPLog(__name__)
class BinSearch(NZBProvider):
class Base(NZBProvider):
urls = {
'download': 'https://www.binsearch.info/fcgi/nzb.fcgi?q=%s',
@@ -18,29 +18,17 @@ class BinSearch(NZBProvider):
'search': 'https://www.binsearch.info/index.php?%s',
}
http_time_between_calls = 4 # Seconds
http_time_between_calls = 4 # Seconds
def _search(self, movie, quality, results):
def _search(self, media, quality, results):
arguments = tryUrlencode({
'q': movie['library']['identifier'],
'm': 'n',
'max': 400,
'adv_age': Env.setting('retention', 'nzb'),
'adv_sort': 'date',
'adv_col': 'on',
'adv_nfo': 'on',
'minsize': quality.get('size_min'),
'maxsize': quality.get('size_max'),
})
data = self.getHTMLData(self.urls['search'] % arguments)
data = self.getHTMLData(self.urls['search'] % self.buildUrl(media, quality))
if data:
try:
html = BeautifulSoup(data)
main_table = html.find('table', attrs = {'id':'r2'})
main_table = html.find('table', attrs = {'id': 'r2'})
if not main_table:
return
@@ -48,11 +36,11 @@ class BinSearch(NZBProvider):
items = main_table.find_all('tr')
for row in items:
title = row.find('span', attrs = {'class':'s'})
title = row.find('span', attrs = {'class': 's'})
if not title: continue
nzb_id = row.find('input', attrs = {'type':'checkbox'})['name']
nzb_id = row.find('input', attrs = {'type': 'checkbox'})['name']
info = row.find('span', attrs = {'class':'d'})
size_match = re.search('size:.(?P<size>[0-9\.]+.[GMB]+)', info.text)
@@ -62,10 +50,10 @@ class BinSearch(NZBProvider):
def extra_check(item):
parts = re.search('available:.(?P<parts>\d+)./.(?P<total>\d+)', info.text)
total = tryInt(parts.group('total'))
parts = tryInt(parts.group('parts'))
total = float(tryInt(parts.group('total')))
parts = float(tryInt(parts.group('parts')))
if (total / parts) < 0.95 or ((total / parts) >= 0.95 and not ('par2' in info.text.lower() or 'pa3' in info.text.lower())):
if (total / parts) < 1 and ((total / parts) < 0.95 or ((total / parts) >= 0.95 and not ('par2' in info.text.lower() or 'pa3' in info.text.lower()))):
log.info2('Wrong: \'%s\', not complete: %s out of %s', (item['name'], parts, total))
return False
@@ -77,7 +65,7 @@ class BinSearch(NZBProvider):
results.append({
'id': nzb_id,
'name': title.text,
'name': simplifyString(title.text),
'age': tryInt(age),
'size': self.parseSize(size_match.group('size')),
'url': self.urls['download'] % nzb_id,
@@ -102,3 +90,31 @@ class BinSearch(NZBProvider):
return 'try_next'
config = [{
'name': 'binsearch',
'groups': [
{
'tab': 'searcher',
'list': 'nzb_providers',
'name': 'binsearch',
'description': 'Free provider, less accurate. See <a href="https://www.binsearch.info/">BinSearch</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAATklEQVQY02NwQAMMWAXOnz+PKvD//3/CAvM//z+fgiwAAs+RBab4PP//vwbFjPlAffgEChzOo2r5fBuIfRAC5w8D+QUofkkp8MHjOWQAAM3Sbogztg2wAAAAAElFTkSuQmCC',
'options': [
{
'name': 'enabled',
'type': 'enabler',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
},
],
}]

View File

@@ -0,0 +1,266 @@
from urlparse import urlparse
import time
import traceback
import re
from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import cleanHost, splitString, tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.base import ResultList
from couchpotato.core.media._base.providers.nzb.base import NZBProvider
from couchpotato.environment import Env
from dateutil.parser import parse
from requests import HTTPError
log = CPLog(__name__)
class Base(NZBProvider, RSS):
urls = {
'detail': 'details/%s',
'download': 't=get&id=%s'
}
passwords_regex = 'password|wachtwoord'
limits_reached = {}
http_time_between_calls = 1 # Seconds
def search(self, media, quality):
hosts = self.getHosts()
results = ResultList(self, media, quality, imdb_results = True)
for host in hosts:
if self.isDisabled(host):
continue
self._searchOnHost(host, media, quality, results)
return results
def _searchOnHost(self, host, media, quality, results):
query = self.buildUrl(media, host)
url = '%s&%s' % (self.getUrl(host['host']), query)
nzbs = self.getRSSData(url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()})
for nzb in nzbs:
date = None
spotter = None
for item in nzb:
if date and spotter:
break
if item.attrib.get('name') == 'usenetdate':
date = item.attrib.get('value')
break
# Get the name of the person who posts the spot
if item.attrib.get('name') == 'poster':
if "@spot.net" in item.attrib.get('value'):
spotter = item.attrib.get('value').split("@")[0]
continue
if not date:
date = self.getTextElement(nzb, 'pubDate')
nzb_id = self.getTextElement(nzb, 'guid').split('/')[-1:].pop()
name = self.getTextElement(nzb, 'title')
if not name:
continue
name_extra = ''
if spotter:
name_extra = spotter
description = ''
if "@spot.net" in nzb_id:
try:
# Get details for extended description to retrieve passwords
query = self.buildDetailsUrl(nzb_id, host['api_key'])
url = '%s&%s' % (self.getUrl(host['host']), query)
nzb_details = self.getRSSData(url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()})[0]
description = self.getTextElement(nzb_details, 'description')
# Extract a password from the description
password = re.search('(?:' + self.passwords_regex + ')(?: *)(?:\:|\=)(?: *)(.*?)\<br\>|\n|$', description, flags = re.I).group(1)
if password:
name += ' {{%s}}' % password.strip()
except:
log.debug('Error getting details of "%s": %s', (name, traceback.format_exc()))
results.append({
'id': nzb_id,
'provider_extra': urlparse(host['host']).hostname or host['host'],
'name': toUnicode(name),
'name_extra': name_extra,
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024,
'url': ((self.getUrl(host['host']) + self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host),
'detail_url': (cleanHost(host['host']) + self.urls['detail']) % tryUrlencode(nzb_id),
'content': self.getTextElement(nzb, 'description'),
'description': description,
'score': host['extra_score'],
})
def getHosts(self):
uses = splitString(str(self.conf('use')), clean = False)
hosts = splitString(self.conf('host'), clean = False)
api_keys = splitString(self.conf('api_key'), clean = False)
extra_score = splitString(self.conf('extra_score'), clean = False)
custom_tags = splitString(self.conf('custom_tag'), clean = False)
list = []
for nr in range(len(hosts)):
try: key = api_keys[nr]
except: key = ''
try: host = hosts[nr]
except: host = ''
try: score = tryInt(extra_score[nr])
except: score = 0
try: custom_tag = custom_tags[nr]
except: custom_tag = ''
list.append({
'use': uses[nr],
'host': host,
'api_key': key,
'extra_score': score,
'custom_tag': custom_tag
})
return list
def belongsTo(self, url, provider = None, host = None):
hosts = self.getHosts()
for host in hosts:
result = super(Base, self).belongsTo(url, host = host['host'], provider = provider)
if result:
return result
def getUrl(self, host):
if '?page=newznabapi' in host:
return cleanHost(host)[:-1] + '&'
return cleanHost(host) + 'api?'
def isDisabled(self, host = None):
return not self.isEnabled(host)
def isEnabled(self, host = None):
# Return true if at least one is enabled and no host is given
if host is None:
for host in self.getHosts():
if self.isEnabled(host):
return True
return False
return NZBProvider.isEnabled(self) and host['host'] and host['api_key'] and int(host['use'])
def getApiExt(self, host):
return '&apikey=%s' % host['api_key']
def download(self, url = '', nzb_id = ''):
host = urlparse(url).hostname
if self.limits_reached.get(host):
# Try again in 3 hours
if self.limits_reached[host] > time.time() - 10800:
return 'try_next'
try:
data = self.urlopen(url, show_error = False)
self.limits_reached[host] = False
return data
except HTTPError as e:
if e.code == 503:
response = e.read().lower()
if 'maximum api' in response or 'download limit' in response:
if not self.limits_reached.get(host):
log.error('Limit reached for newznab provider: %s', host)
self.limits_reached[host] = time.time()
return 'try_next'
log.error('Failed download from %s: %s', (host, traceback.format_exc()))
return 'try_next'
def buildDetailsUrl(self, nzb_id, api_key):
query = tryUrlencode({
't': 'details',
'id': nzb_id,
'apikey': api_key,
})
return query
config = [{
'name': 'newznab',
'groups': [
{
'tab': 'searcher',
'list': 'nzb_providers',
'name': 'newznab',
'order': 10,
'description': 'Enable <a href="http://newznab.com/" target="_blank">NewzNab</a> such as <a href="https://nzb.su" target="_blank">NZB.su</a>, \
<a href="https://nzbs.org" target="_blank">NZBs.org</a>, <a href="http://dognzb.cr/" target="_blank">DOGnzb.cr</a>, \
<a href="https://github.com/spotweb/spotweb" target="_blank">Spotweb</a>, <a href="https://nzbgeek.info/" target="_blank">NZBGeek</a>, \
<a href="https://smackdownonyou.com" target="_blank">SmackDown</a>, <a href="https://www.nzbfinder.ws" target="_blank">NZBFinder</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAACVBMVEVjhwD///86aRovd/sBAAAAMklEQVQI12NgAIPQUCCRmQkjssDEShiRuRIqwZqZGcDAGBrqANUhGgIkWAOABKMDxCAA24UK50b26SAAAAAASUVORK5CYII=',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': True,
},
{
'name': 'use',
'default': '0,0,0,0,0,0'
},
{
'name': 'host',
'default': 'api.nzb.su,api.dognzb.cr,nzbs.org,https://index.nzbgeek.info, https://smackdownonyou.com, https://www.nzbfinder.ws',
'description': 'The hostname of your newznab provider',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'default': '0,0,0,0,0,0',
'description': 'Starting score for each release found via this provider.',
},
{
'name': 'custom_tag',
'advanced': True,
'label': 'Custom tag',
'default': ',,,,,',
'description': 'Add custom tags, for example add rls=1 to get only scene releases from nzbs.org',
},
{
'name': 'api_key',
'default': ',,,,,',
'label': 'Api Key',
'description': 'Can be found on your profile page',
'type': 'combined',
'combine': ['use', 'host', 'api_key', 'extra_score', 'custom_tag'],
},
],
},
],
}]

View File

@@ -1,37 +1,28 @@
import time
from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider
from couchpotato.core.media._base.providers.nzb.base import NZBProvider
from dateutil.parser import parse
import time
log = CPLog(__name__)
class NZBClub(NZBProvider, RSS):
class Base(NZBProvider, RSS):
urls = {
'search': 'http://www.nzbclub.com/nzbfeed.aspx?%s',
'search': 'https://www.nzbclub.com/nzbfeeds.aspx?%s',
}
http_time_between_calls = 4 #seconds
http_time_between_calls = 4 # seconds
def _searchOnTitle(self, title, movie, quality, results):
def _search(self, media, quality, results):
q = '"%s %s"' % (title, movie['library']['year'])
params = tryUrlencode({
'q': q,
'ig': 1,
'rpp': 200,
'st': 5,
'sp': 1,
'ns': 1,
})
nzbs = self.getRSSData(self.urls['search'] % params)
nzbs = self.getRSSData(self.urls['search'] % self.buildUrl(media))
for nzb in nzbs:
@@ -64,7 +55,7 @@ class NZBClub(NZBProvider, RSS):
def getMoreInfo(self, item):
full_description = self.getCache('nzbclub.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
html = BeautifulSoup(full_description)
nfo_pre = html.find('pre', attrs = {'class':'nfo'})
nfo_pre = html.find('pre', attrs = {'class': 'nfo'})
description = toUnicode(nfo_pre.text) if nfo_pre else ''
item['description'] = description
@@ -78,3 +69,32 @@ class NZBClub(NZBProvider, RSS):
return False
return True
config = [{
'name': 'nzbclub',
'groups': [
{
'tab': 'searcher',
'list': 'nzb_providers',
'name': 'NZBClub',
'description': 'Free provider, less accurate. See <a href="https://www.nzbclub.com/">NZBClub</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACEUlEQVQ4y3VSMWgUQRR9/8/s7OzeJSdnTsVGghLEYBNQjBpQiRBFhIB2EcHG1kbs0murhZAmVocExEZQ0c7CxkLINYcJJpoYj9wZcnu72fF21uJSXMzuhyne58/j/fcf4b+KokgBIOSU53lxP5b9oNVqDT36dH+5UjoiKvIwPFEEgWBshGZ3E7/NOupL9fMjx0e+ZhKsrq+c/FPZKJi0w4FsQXMBDEJsd7BNW9h2tuyP9vfTALIJkMIu1hYRtINM+dpzcWc0sbkreK4fUEogyraAmKGF3+7vcT/wtR9QwkCabSAzQQuvk0uglAo5YaQ5DASGYjfMXcHVOqKu6NmR7iehlKAdHWUqWPv1c3i+9uwVdRlEBGaGEAJCCrDo9ShhvF6qPq8tL57bp+DbRn2sHtUuCY9YphLMu5921VhrwYJ5tbt0tt6sjQP4vEfB2Ikz7/ytwbeR6ljHkXCUA6UcOLtPOg4MYhtH8ZcLw5er+xQMDAwEURRNl96X596Y6oxFwsw9fmtTOAr2Ik19nL365FZpsLSdnQPPM8aYewc+lDcX4rkHqbQMAGTJXulOLzycmr1bKBTi3DOGYagajcahiaOT89fbM0/dxEsUu3aidfPljWO3HzebzYNBELi5Z5RSJlrrHd/3w8lT114MrVTWOn875fHRiYVisRhorWMpZXdvNnLKGCOstb0AMlulVJI19w/+nceU4D0aCwAAAABJRU5ErkJggg==',
'options': [
{
'name': 'enabled',
'type': 'enabler',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
},
],
}]

View File

@@ -0,0 +1,126 @@
import re
import time
from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.media._base.providers.nzb.base import NZBProvider
from dateutil.parser import parse
log = CPLog(__name__)
class Base(NZBProvider, RSS):
urls = {
'download': 'https://www.nzbindex.com/download/',
'search': 'https://www.nzbindex.com/rss/?%s',
}
http_time_between_calls = 1 # Seconds
def _search(self, media, quality, results):
nzbs = self.getRSSData(self.urls['search'] % self.buildUrl(media, quality))
for nzb in nzbs:
enclosure = self.getElement(nzb, 'enclosure').attrib
nzbindex_id = int(self.getTextElement(nzb, "link").split('/')[4])
title = self.getTextElement(nzb, "title")
match = fireEvent('matcher.parse', title, parser='usenet', single = True)
if not match.chains:
log.info('Unable to parse release with title "%s"', title)
continue
# TODO should we consider other lower-weight chains here?
info = fireEvent('matcher.flatten_info', match.chains[0].info, single = True)
release_name = fireEvent('matcher.construct_from_raw', info.get('release_name'), single = True)
file_name = info.get('detail', {}).get('file_name')
file_name = file_name[0] if file_name else None
title = release_name or file_name
# Strip extension from parsed title (if one exists)
ext_pos = title.rfind('.')
# Assume extension if smaller than 4 characters
# TODO this should probably be done a better way
if len(title[ext_pos + 1:]) <= 4:
title = title[:ext_pos]
if not title:
log.info('Unable to find release name from match')
continue
try:
description = self.getTextElement(nzb, "description")
except:
description = ''
def extra_check(item):
if '#c20000' in item['description'].lower():
log.info('Wrong: Seems to be passworded: %s', item['name'])
return False
return True
results.append({
'id': nzbindex_id,
'name': title,
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
'size': tryInt(enclosure['length']) / 1024 / 1024,
'url': enclosure['url'],
'detail_url': enclosure['url'].replace('/download/', '/release/'),
'description': description,
'get_more_info': self.getMoreInfo,
'extra_check': extra_check,
})
def getMoreInfo(self, item):
try:
if '/nfo/' in item['description'].lower():
nfo_url = re.search('href=\"(?P<nfo>.+)\" ', item['description']).group('nfo')
full_description = self.getCache('nzbindex.%s' % item['id'], url = nfo_url, cache_timeout = 25920000)
html = BeautifulSoup(full_description)
item['description'] = toUnicode(html.find('pre', attrs = {'id': 'nfo0'}).text)
except:
pass
config = [{
'name': 'nzbindex',
'groups': [
{
'tab': 'searcher',
'list': 'nzb_providers',
'name': 'nzbindex',
'description': 'Free provider, less accurate. See <a href="https://www.nzbindex.com/">NZBIndex</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAo0lEQVR42t2SQQ2AMBAEcUCwUAv94QMLfHliAQtYqIVawEItYAG6yZFMLkUANNlk79Kbbtp2P1j9uKxVV9VWFeStl+Wh3fWK9hNwEoADZkJtMD49AqS5AUjWGx6A+m+ARICGrM5W+wSTB0gETKzdHZwCEZAJ8PGZQN4AiQAmkR9s06EBAugJiBoAAPFfAQcBgZcIHzwA6TYP4JsXeSg3P9L31w3eksbH3zMb/wAAAABJRU5ErkJggg==',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': True,
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
},
],
}]

View File

@@ -0,0 +1,103 @@
from urlparse import urlparse, parse_qs
import time
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.nzb.base import NZBProvider
from dateutil.parser import parse
log = CPLog(__name__)
class Base(NZBProvider, RSS):
urls = {
'search': 'https://rss.omgwtfnzbs.org/rss-search.php?%s',
'detail_url': 'https://omgwtfnzbs.org/details.php?id=%s',
}
http_time_between_calls = 1 # Seconds
cat_ids = [
([15], ['dvdrip']),
([15, 16], ['brrip']),
([16], ['720p', '1080p', 'bd50']),
([17], ['dvdr']),
]
cat_backup_id = 'movie'
def search(self, movie, quality):
if quality['identifier'] in fireEvent('quality.pre_releases', single = True):
return []
return super(Base, self).search(movie, quality)
def _searchOnTitle(self, title, movie, quality, results):
q = '%s %s' % (title, movie['info']['year'])
params = tryUrlencode({
'search': q,
'catid': ','.join([str(x) for x in self.getCatId(quality)]),
'user': self.conf('username', default = ''),
'api': self.conf('api_key', default = ''),
})
nzbs = self.getRSSData(self.urls['search'] % params)
for nzb in nzbs:
enclosure = self.getElement(nzb, 'enclosure').attrib
nzb_id = parse_qs(urlparse(self.getTextElement(nzb, 'link')).query).get('id')[0]
results.append({
'id': nzb_id,
'name': toUnicode(self.getTextElement(nzb, 'title')),
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, 'pubDate')).timetuple()))),
'size': tryInt(enclosure['length']) / 1024 / 1024,
'url': enclosure['url'],
'detail_url': self.urls['detail_url'] % nzb_id,
'description': self.getTextElement(nzb, 'description')
})
config = [{
'name': 'omgwtfnzbs',
'groups': [
{
'tab': 'searcher',
'list': 'nzb_providers',
'name': 'OMGWTFNZBs',
'description': 'See <a href="http://omgwtfnzbs.org/">OMGWTFNZBs</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAIAAADAAbR1AAADbElEQVR4AZ2UW0ybZRiAy/OvdHaLYvB0YTRIFi7GkM44zRLmIfNixkWdiRMyYoxRE8/TC7MYvXCGEBmr3mxLwVMwY0wYA7e6Wso4lB6h/U9taSlMGIfBXLYlJMyo0S///2dJI5lxN8/F2/f9nu9737e/jYmXr6KTbN9BGG9HE/NotQ76UWziNzrXFiETk/5ARUNH+7+0kW7fSgTl0VKGOLZzidOkmuuIo7q2oTArNLPIzhdIkqXkerFOm2CaD/5bcKrjIL2c3fkhPxOq93Kcb91v46fV9TQKF4TgV/TbUsQtzfCaK6jMOd5DJrguSIIhexmqqVxN0FXbRR8/ND/LYTTj6J7nl2gnL47OkDW4KJhnQHCa6JpKVNJGA3OC58nwBJoZ//ebbIyKpBxjrr0o1q1FMRkrKXZnHWF85VvxMrJxibwhGyd0f5bLnKzqJs1k0Sfo+EU8hdAUvkbcwKEgs2D0OiV4jmmD1zb+Tp6er0JMMvDxPo5xev9zTBF683NS+N56n1YiB95B5crr93KRuKhKI0tb0Kw2mgLLqTjLEWO8424i9IvURaYeOckwf3+/yCC9e3bQQ/MuD+Monk0k+XFXMUfx7z5EEP+XlXi5tLlMxH8zLppw7idJrugcus30kC86gc7UrQqjLIukM8zWHOACeU+TiMxXN6ExVOkgz4lvPEzice1GIVhxhG4CrZvpl6TH55giKWqXGLy9hZh5aUtgDSew/msSyCKpl+DDNfxJc8NBIsxUxUnz14O/oONu+IIIvso9TLBQ1SY5rUhuSzUhAqJ2mRXBLDOCeUtgUZXsaObT8BffhUJPqWgiV+3zKKzYH0ClvTRLhD77HIqVkyh5jThnivehoG+qJctIRSPn6bxvO4FCgTl9c1DmbpjLajbQFE8aW5SU3rg+zOPGUjTUF9NFpLEbH2c/KmGYlY69/GQJVtGMSUcEp9eCbB1nctbxHTLRdTUkGDf+B02uGWRG3OvpJ/zSMwzif+oxVBID3cQKBavLCiPmB2PM2UuSCUPgrX4VDb97AwEG67bh4+KTOlncvu3M31BwA5rLHbCfEjwkNDky9e/SSbSxnD46Pg0RJtpXRvhmBSZHpRjWtKwFybjuQeXaKxto4WjLZZZvVmC17pZLJFkwxm5++PS2Mrwc7nyIMYZe/IzoP5d6QgEybqTXAAAAAElFTkSuQmCC',
'options': [
{
'name': 'enabled',
'type': 'enabler',
},
{
'name': 'username',
'default': '',
},
{
'name': 'api_key',
'label': 'Api Key',
'default': '',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'default': 20,
'type': 'int',
'description': 'Starting score for each release found via this provider.',
}
],
},
],
}]

View File

@@ -0,0 +1,141 @@
import re
import traceback
from bs4 import BeautifulSoup
from couchpotato.core.helpers.variable import tryInt, getIdentifier
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
log = CPLog(__name__)
class Base(TorrentProvider):
urls = {
'test': 'https://awesome-hd.net/',
'detail': 'https://awesome-hd.net/torrents.php?torrentid=%s',
'search': 'https://awesome-hd.net/searchapi.php?action=imdbsearch&passkey=%s&imdb=%s&internal=%s',
'download': 'https://awesome-hd.net/torrents.php?action=download&id=%s&authkey=%s&torrent_pass=%s',
}
http_time_between_calls = 1
def _search(self, movie, quality, results):
data = self.getHTMLData(self.urls['search'] % (self.conf('passkey'), getIdentifier(movie), self.conf('only_internal')))
if data:
try:
soup = BeautifulSoup(data)
if soup.find('error'):
log.error(soup.find('error').get_text())
return
authkey = soup.find('authkey').get_text()
entries = soup.find_all('torrent')
for entry in entries:
torrentscore = 0
torrent_id = entry.find('id').get_text()
name = entry.find('name').get_text()
year = entry.find('year').get_text()
releasegroup = entry.find('releasegroup').get_text()
resolution = entry.find('resolution').get_text()
encoding = entry.find('encoding').get_text()
freeleech = entry.find('freeleech').get_text()
torrent_desc = '/ %s / %s / %s ' % (releasegroup, resolution, encoding)
if freeleech == '0.25' and self.conf('prefer_internal'):
torrent_desc += '/ Internal'
torrentscore += 200
if encoding == 'x264' and self.conf('favor') in ['encode', 'both']:
torrentscore += 300
if re.search('Remux', encoding) and self.conf('favor') in ['remux', 'both']:
torrentscore += 200
results.append({
'id': torrent_id,
'name': re.sub('[^A-Za-z0-9\-_ \(\).]+', '', '%s (%s) %s' % (name, year, torrent_desc)),
'url': self.urls['download'] % (torrent_id, authkey, self.conf('passkey')),
'detail_url': self.urls['detail'] % torrent_id,
'size': self.parseSize(entry.find('size').get_text()),
'seeders': tryInt(entry.find('seeders').get_text()),
'leechers': tryInt(entry.find('leechers').get_text()),
'score': torrentscore
})
except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
config = [{
'name': 'awesomehd',
'groups': [
{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'Awesome-HD',
'description': '<a href="https://awesome-hd.net">AHD</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC+UlEQVR4AV1SO0y6dxQ9H4g8CoIoohZ5NA0aR2UgkYpNB5uocTSaLlrDblMH09Gt8d90r3YpJkanxjA4GGkbO7RNxSABq8jDGnkpD+UD5NV7Bxvbk9wvv+/3uPece66A/yEWi42FQqHVfD7/cbPZtIEglUpjOp3uZHR0dBvAn3gDIRqNgjE4OKj0+Xzf3NzcfD4wMCCjf5TLZbTbbajVatzf3+Pu7q5uNpt35ufnvwBQAScQRREEldfr9RWLxan+/n5YrVa+jFarhVfQQyQSCU4EhULhX15engEgSrjC0dHRVqlUmjQYDBgaGgKtuTqz4mTgIoVCASaTCX19fajVapOHh4dbFJBks9mxcDi8qtFoJEajkfVyJWi1WkxMTMDhcIAT8x6D7/Dd6+vr1fHx8TGp2+3+iqo5+YCzBwIBToK5ubl/mQwPDyMSibAs2Gw2UHNRrValz8/PDUk8Hv9EqVRCr9fj4uICTNflcqFer+Pg4AB7e3uoVCq8x9Rxfn6O7u5uqFQq8FspZXxHTekggByA3W4Hr9PpNDeRL3I1cMhkMrBrnZ2dyGQyvNYIs7OzVbJNPjIyAraLwYdcjR8wXl5eIJfLwRIFQQDLYkm3t7c1CdGPPT4+cpOImp4PODMeaK+n10As2jBbrHifHOjS6qAguVFimkqlwAMmIQnHV1dX4NDQhVwuhyZTV6pgIktzDzkkk0lEwhEEzs7ASQr5Ai4vL1nuccfCwsLO/v6+p9FoyJhF6ekJro/cPCzIZLNQa7rQoK77/SdgWWpKkCaJ5EB9aWnpe6nH40nRMBnJV4f5gw+FX3/5GX/8/htXRZdOzzqhJWn6nl6YbTZqqhrhULD16fT0d8FgcFtYW1vD5uamfGVl5cd4IjldKhZACdkJvKfWUANrxEaJV4hiGVaL1b+7653hXzwRZQr2X76xsfG1xWIRaZzbNPv/CdrjEL9cX/+WXFBSgEPgzxuwG3Yans9OT0+naBZMIJDNfzudzp8WFxd/APAX3uAf9WOTxOPLdosAAAAASUVORK5CYII=',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
{
'name': 'passkey',
'default': '',
},
{
'name': 'seed_ratio',
'label': 'Seed ratio',
'type': 'float',
'default': 1,
'description': 'Will not be (re)moved until this seed ratio is met.',
},
{
'name': 'seed_time',
'label': 'Seed time',
'type': 'int',
'default': 40,
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
},
{
'name': 'only_internal',
'advanced': True,
'type': 'bool',
'default': 1,
'description': 'Only search for internal releases.'
},
{
'name': 'prefer_internal',
'advanced': True,
'type': 'bool',
'default': 1,
'description': 'Favors internal releases over non-internal releases.'
},
{
'name': 'favor',
'advanced': True,
'default': 'both',
'type': 'dropdown',
'values': [('Encodes & Remuxes', 'both'), ('Encodes', 'encode'), ('Remuxes', 'remux'), ('None', 'none')],
'description': 'Give extra scoring to encodes or remuxes.'
},
{
'name': 'extra_score',
'advanced': True,
'type': 'int',
'default': 20,
'description': 'Starting score for each release found via this provider.',
},
],
},
],
}]

View File

@@ -1,8 +1,11 @@
import time
import traceback
from couchpotato.core.helpers.variable import getImdb, md5, cleanHost
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import YarrProvider
from couchpotato.core.media._base.providers.base import YarrProvider
from couchpotato.environment import Env
import time
log = CPLog(__name__)
@@ -41,14 +44,15 @@ class TorrentProvider(YarrProvider):
prop_name = 'proxy.%s' % proxy
last_check = float(Env.prop(prop_name, default = 0))
if last_check > time.time() - 1209600:
if last_check > time.time() - 86400:
continue
data = ''
try:
data = self.urlopen(proxy, timeout = 3, show_error = False)
except:
log.debug('Failed %s proxy %s', (self.getName(), proxy))
log.debug('Failed %s proxy %s: %s', (self.getName(), proxy, traceback.format_exc()))
if self.correctProxy(data):
log.debug('Using proxy for %s: %s', (self.getName(), proxy))
@@ -63,9 +67,10 @@ class TorrentProvider(YarrProvider):
return cleanHost(self.proxy_domain).rstrip('/') + url
def correctProxy(self):
def correctProxy(self, data):
return True
class TorrentMagnetProvider(TorrentProvider):
protocol = 'torrent_magnet'

View File

@@ -0,0 +1,139 @@
import traceback
from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
log = CPLog(__name__)
class Base(TorrentProvider):
urls = {
'test': 'http://www.bit-hdtv.com/',
'login': 'http://www.bit-hdtv.com/takelogin.php',
'login_check': 'http://www.bit-hdtv.com/messages.php',
'detail': 'http://www.bit-hdtv.com/details.php?id=%s',
'search': 'http://www.bit-hdtv.com/torrents.php?',
}
# Searches for movies only - BiT-HDTV's subcategory and resolution search filters appear to be broken
http_time_between_calls = 1 # Seconds
def _search(self, media, quality, results):
query = self.buildUrl(media, quality)
url = "%s&%s" % (self.urls['search'], query)
data = self.getHTMLData(url)
if data:
# Remove BiT-HDTV's output garbage so outdated BS4 versions successfully parse the HTML
split_data = data.partition('-->')
if '## SELECT COUNT(' in split_data[0]:
data = split_data[2]
html = BeautifulSoup(data)
try:
result_table = html.find('table', attrs = {'width': '750', 'class': ''})
if result_table is None:
return
entries = result_table.find_all('tr')
for result in entries[1:]:
cells = result.find_all('td')
link = cells[2].find('a')
torrent_id = link['href'].replace('/details.php?id=', '')
results.append({
'id': torrent_id,
'name': link.contents[0].get_text(),
'url': cells[0].find('a')['href'],
'detail_url': self.urls['detail'] % torrent_id,
'size': self.parseSize(cells[6].get_text()),
'seeders': tryInt(cells[8].string),
'leechers': tryInt(cells[9].string),
'get_more_info': self.getMoreInfo,
})
except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
def getLoginParams(self):
return {
'username': self.conf('username'),
'password': self.conf('password'),
}
def getMoreInfo(self, item):
full_description = self.getCache('bithdtv.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
html = BeautifulSoup(full_description)
nfo_pre = html.find('table', attrs = {'class': 'detail'})
description = toUnicode(nfo_pre.text) if nfo_pre else ''
item['description'] = description
return item
def loginSuccess(self, output):
return 'logout.php' in output.lower()
loginCheckSuccess = loginSuccess
config = [{
'name': 'bithdtv',
'groups': [
{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'BiT-HDTV',
'description': '<a href="http://bit-hdtv.com">BiT-HDTV</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAABMklEQVR4AZ3Qu0ojcQCF8W9MJcQbJNgEEQUbQVIqWgnaWfkIvoCgggixEAmIhRtY2GV3w7KwU61B0EYIxmiw0YCik84ipaCuc0nmP5dcjIUgOjqDvxf4OAdf9mnMLcUJyPyGSCP+YRdC+Kp8iagJKhuS+InYRhTGgDbeV2uEMand4ZRxizjXHQEimxhraAnUr73BNqQxMiNeV2SwcjTLEVtb4Zl10mXutvOWm2otw5Sxz6TGTbdd6ncuYvVLXAXrvM+ruyBpy1S3JLGDfUQ1O6jn5vTsrJXvqSt4UNfj6vxTRPxBHER5QeSirhLGk/5rWN+ffB1XZuxjnDy1q87m7TS+xOGA+Iv4gfkbaw+nOMXHDHnITGEk0VfRFnn4Po4vNYm6RGukmggR0L08+l+e4HMeASo/i6AJUjLgAAAAAElFTkSuQmCC',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
{
'name': 'username',
'default': '',
},
{
'name': 'password',
'default': '',
'type': 'password',
},
{
'name': 'seed_ratio',
'label': 'Seed ratio',
'type': 'float',
'default': 1,
'description': 'Will not be (re)moved until this seed ratio is met.',
},
{
'name': 'seed_time',
'label': 'Seed time',
'type': 'int',
'default': 40,
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 20,
'description': 'Starting score for each release found via this provider.',
}
],
},
],
}]

View File

@@ -0,0 +1,134 @@
import traceback
from bs4 import BeautifulSoup, SoupStrainer
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
log = CPLog(__name__)
class Base(TorrentProvider):
urls = {
'test': 'https://www.bitsoup.me/',
'login': 'https://www.bitsoup.me/takelogin.php',
'login_check': 'https://www.bitsoup.me/my.php',
'search': 'https://www.bitsoup.me/browse.php?%s',
'baseurl': 'https://www.bitsoup.me/%s',
}
http_time_between_calls = 1 # Seconds
only_tables_tags = SoupStrainer('table')
def _searchOnTitle(self, title, movie, quality, results):
url = self.urls['search'] % self.buildUrl(title, movie, quality)
data = self.getHTMLData(url)
if data:
html = BeautifulSoup(data, 'html.parser', parse_only = self.only_tables_tags)
try:
result_table = html.find('table', attrs = {'class': 'koptekst'})
if not result_table or 'nothing found!' in data.lower():
return
entries = result_table.find_all('tr')
for result in entries[1:]:
all_cells = result.find_all('td')
torrent = all_cells[1].find('a')
download = all_cells[3].find('a')
torrent_id = torrent['href']
torrent_id = torrent_id.replace('details.php?id=', '')
torrent_id = torrent_id.replace('&hit=1', '')
torrent_name = torrent.getText()
torrent_size = self.parseSize(all_cells[7].getText())
torrent_seeders = tryInt(all_cells[9].getText())
torrent_leechers = tryInt(all_cells[10].getText())
torrent_url = self.urls['baseurl'] % download['href']
torrent_detail_url = self.urls['baseurl'] % torrent['href']
results.append({
'id': torrent_id,
'name': torrent_name,
'size': torrent_size,
'seeders': torrent_seeders,
'leechers': torrent_leechers,
'url': torrent_url,
'detail_url': torrent_detail_url,
})
except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
def getLoginParams(self):
return {
'username': self.conf('username'),
'password': self.conf('password'),
'ssl': 'yes',
}
def loginSuccess(self, output):
return 'logout.php' in output.lower()
loginCheckSuccess = loginSuccess
config = [{
'name': 'bitsoup',
'groups': [
{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'Bitsoup',
'description': '<a href="https://bitsoup.me">Bitsoup</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAB8ElEQVR4AbWSS2sTURiGz3euk0mswaE37HhNhIrajQheFgF3rgR/lAt/gOBCXNZlo6AbqfUWRVCxi04wqUnTRibpJLaJzdzOOZ6WUumyC5/VHOb9eN/FA91uFx0FjI4IPfgiGLTWH73tn348GKmN7ijD0d2b41fO5qJEaX24AWNIUrVQCTTJ3Llx6vbV6Vtzk7Gi9+ebi996guFDDYAQAVj4FExP5qdOZB49W62t/zH3hECcwsPnbWeMXz6Xi2K1f0ApeK3hMCHHbP5gvvoriBgFAAQJEAxhjJ4u+YWTNsVI6b1JgtPWZkoIefKy4fcii2OTw2BABs7wj3bYDlLL4rvjGWOdTser1j5Xf7c3Q/MbHQYApxItvnm31mhQQ71eX2vUB76/vsWB2hg0QuogrMwLIG8P3InM2/eVGXeDViqVwWB79vRU2lgJYmdHcgXCTAXQFJTN5HguvDCR2Hxsxe8EvT54nlcul5vNpqDIEgwRQanAhAAABgRIyiQcjpIkkTOuWyqVoN/vSylX67XXH74uV1vHRUyxxFqbLBCSmBpiXSq6xcL5QrGYzWZ3XQIAwdlOJB+/aL764ucdmncYs0WsCI7kvTnn+qyDMEnTVCn1Tz5KsBFg6fvWcmsUAcnYNC/g2hnromvvqbHvxv+39S+MX+bWkFXwAgAAAABJRU5ErkJggg==',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
{
'name': 'username',
'default': '',
},
{
'name': 'password',
'default': '',
'type': 'password',
},
{
'name': 'seed_ratio',
'label': 'Seed ratio',
'type': 'float',
'default': 1,
'description': 'Will not be (re)moved until this seed ratio is met.',
},
{
'name': 'seed_time',
'label': 'Seed time',
'type': 'int',
'default': 40,
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 20,
'description': 'Starting score for each release found via this provider.',
}
],
},
],
}]

View File

@@ -0,0 +1,116 @@
import re
import json
import traceback
from couchpotato.core.helpers.variable import tryInt, getIdentifier
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
log = CPLog(__name__)
class Base(TorrentProvider):
urls = {
'test': 'https://hdbits.org/',
'detail': 'https://hdbits.org/details.php?id=%s',
'download': 'https://hdbits.org/download.php?id=%s&passkey=%s',
'api': 'https://hdbits.org/api/torrents'
}
http_time_between_calls = 1 # Seconds
def _post_query(self, **params):
post_data = {
'username': self.conf('username'),
'passkey': self.conf('passkey')
}
post_data.update(params)
try:
result = self.getJsonData(self.urls['api'], data = json.dumps(post_data))
if result:
if result['status'] != 0:
log.error('Error searching hdbits: %s' % result['message'])
else:
return result['data']
except:
pass
return None
def _search(self, movie, quality, results):
match = re.match(r'tt(\d{7})', getIdentifier(movie))
data = self._post_query(imdb = {'id': match.group(1)})
if data:
try:
for result in data:
results.append({
'id': result['id'],
'name': result['name'],
'url': self.urls['download'] % (result['id'], self.conf('passkey')),
'detail_url': self.urls['detail'] % result['id'],
'size': tryInt(result['size']) / 1024 / 1024,
'seeders': tryInt(result['seeders']),
'leechers': tryInt(result['leechers'])
})
except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
config = [{
'name': 'hdbits',
'groups': [
{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'HDBits',
'wizard': True,
'description': '<a href="http://hdbits.org">HDBits</a>',
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABi0lEQVR4AZWSzUsbQRjGdyabTcvSNPTSHlpQQeMHJApC8CJRvHgQQU969+LJP8G7f4N3DwpeFRQvRr0EKaUl0ATSpkigUNFsMl/r9NmZLCEHA/nNO5PfvMPDm0DI6fV3ZxiolEICe1oZCBVCCmBPKwOh2ErKBHGE4KYEXBpSLkUlqO4LcM7f+6nVhRnOhSkOz/hexk+tL+YL0yPF2YmN4tynD++4gTLGkNNac9YFLoREBR1+cnF3dFY6v/m6PD+FaXiNJtgA4xYbABxiGrz6+6HWaI5/+Qh37YS0/3Znc8UxwNGBIIBX22z+/ZdJ+4wzyjpR4PEpODg8tgUXBv2iWUzSpa12B0IR6n6lvt8Aek2lZHb084+fdRNgrwY8z81PjhVy2d2ttUrtV/lbBa+JXGEpDMPnoF2tN1QYRqVUtf6nFbThb7wk7le395elcqhASLb39okDiHY00VCtCTEHwSiH4AI0lkOiT1dwMeSfT3SRxiQWNO7Zwj1egkoVIQFMKvSiC3bcjXq9Jf8DcDIRT3hh10kAAAAASUVORK5CYII=',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
{
'name': 'username',
'default': '',
},
{
'name': 'passkey',
'default': '',
},
{
'name': 'seed_ratio',
'label': 'Seed ratio',
'type': 'float',
'default': 1,
'description': 'Will not be (re)moved until this seed ratio is met.',
},
{
'name': 'seed_time',
'label': 'Seed time',
'type': 'int',
'default': 40,
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
},
],
},
],
}]

View File

@@ -0,0 +1,195 @@
import re
import traceback
from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.variable import tryInt, splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
log = CPLog(__name__)
class Base(TorrentProvider):
urls = {
'download': 'https://www.ilovetorrents.me/%s',
'detail': 'https://www.ilovetorrents.me/%s',
'search': 'https://www.ilovetorrents.me/browse.php?search=%s&page=%s&cat=%s',
'test': 'https://www.ilovetorrents.me/',
'login': 'https://www.ilovetorrents.me/takelogin.php',
'login_check': 'https://www.ilovetorrents.me'
}
cat_ids = [
(['41'], ['720p', '1080p', 'brrip']),
(['19'], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']),
(['20'], ['dvdr'])
]
cat_backup_id = 200
disable_provider = False
http_time_between_calls = 1
def _searchOnTitle(self, title, movie, quality, results):
page = 0
total_pages = 1
cats = self.getCatId(quality)
while page < total_pages:
movieTitle = tryUrlencode('"%s" %s' % (title, movie['info']['year']))
search_url = self.urls['search'] % (movieTitle, page, cats[0])
page += 1
data = self.getHTMLData(search_url)
if data:
try:
results_table = None
data_split = splitString(data, '<table')
soup = None
for x in data_split:
soup = BeautifulSoup(x)
results_table = soup.find('table', attrs = {'class': 'koptekst'})
if results_table:
break
if not results_table:
return
try:
pagelinks = soup.findAll(href = re.compile('page'))
page_numbers = [int(re.search('page=(?P<page_number>.+'')', i['href']).group('page_number')) for i in pagelinks]
total_pages = max(page_numbers)
except:
pass
entries = results_table.find_all('tr')
for result in entries[1:]:
prelink = result.find(href = re.compile('details.php'))
link = prelink['href']
download = result.find('a', href = re.compile('download.php'))['href']
if link and download:
def extra_score(item):
trusted = (0, 10)[result.find('img', alt = re.compile('Trusted')) is not None]
vip = (0, 20)[result.find('img', alt = re.compile('VIP')) is not None]
confirmed = (0, 30)[result.find('img', alt = re.compile('Helpers')) is not None]
moderated = (0, 50)[result.find('img', alt = re.compile('Moderator')) is not None]
return confirmed + trusted + vip + moderated
id = re.search('id=(?P<id>\d+)&', link).group('id')
url = self.urls['download'] % download
fileSize = self.parseSize(result.select('td.rowhead')[5].text)
results.append({
'id': id,
'name': toUnicode(prelink.find('b').text),
'url': url,
'detail_url': self.urls['detail'] % link,
'size': fileSize,
'seeders': tryInt(result.find_all('td')[2].string),
'leechers': tryInt(result.find_all('td')[3].string),
'extra_score': extra_score,
'get_more_info': self.getMoreInfo
})
except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
def getLoginParams(self):
return {
'username': self.conf('username'),
'password': self.conf('password'),
'submit': 'Welcome to ILT',
}
def getMoreInfo(self, item):
cache_key = 'ilt.%s' % item['id']
description = self.getCache(cache_key)
if not description:
try:
full_description = self.getHTMLData(item['detail_url'])
html = BeautifulSoup(full_description)
nfo_pre = html.find('td', attrs = {'class': 'main'}).findAll('table')[1]
description = toUnicode(nfo_pre.text) if nfo_pre else ''
except:
log.error('Failed getting more info for %s', item['name'])
description = ''
self.setCache(cache_key, description, timeout = 25920000)
item['description'] = description
return item
def loginSuccess(self, output):
return 'logout.php' in output.lower()
loginCheckSuccess = loginSuccess
config = [{
'name': 'ilovetorrents',
'groups': [
{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'ILoveTorrents',
'description': 'Where the Love of Torrents is Born',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACPUlEQVR4AYWM0U9SbxjH3+v266I/oNvWZTfd2J1d0ZqbZEFwWrUImOKs4YwtumFKZvvlJJADR2TCQQlMPKg5NmpREgaekAPnBATKgmK1LqQlx6awHnZWF1Tr2Xfvvs+7z+dB0mlO7StpAh+M4S/2jbo3w8+xvJvlnSneEt+10zwer5ujNUOoChjALWFw5XOwdCAk/P57cGvPl+Oht0W7VJHN5NC1uW1BON4hGjXbwpVWMZhsy9v7sEIXAsDNYBXgdkEoIKyWD2CF8ut/aOXTZc/fBSgLWw1BgA4BDHOV0GkT90cBQpXahU5TFomsb38XhJC5/Tbh1P8c6rJlBeGfAeyMhUFwNVcs9lxV9Ot0dwmyd+mrNvRtbJ2fSPC6Z3Vsvub2z3sDFACAAYzk0+kUyxEkyfN7PopqNBro55A+P6yPKIrL5zF1HwjdeBJJCObIsZO79bo3sHhWhglo5WMV3mazuVPb4fLvSL8/FAkB1hK6rXQPwYhMyROK8VK5LAiH/jsMt0HQjxiN4/ePdoilllcqDyt3Mkg8mRBNbIhMb8RERkowQA/p76g0/UDDdCoNmDminM0qSK5vlpE5kugCHhNPxntwWmJPYTMZtYcFR6ABHQsVRlYLukVORaaULvqKI46keFSCv77kSPS6kxrPptLNDHgz16fWBtyxe6v5h08LUy+KI8ushqTPWWIX8Sg6b45IrGtyW6zXFb/hpQf9m3oqfWuB0fpSw0uZ4WB69En69uOk2rmO2V52PXj+A/mI4ESKpb2HAAAAAElFTkSuQmCC',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False
},
{
'name': 'username',
'label': 'Username',
'type': 'string',
'default': '',
'description': 'The user name for your ILT account',
},
{
'name': 'password',
'label': 'Password',
'type': 'password',
'default': '',
'description': 'The password for your ILT account.',
},
{
'name': 'seed_ratio',
'label': 'Seed ratio',
'type': 'float',
'default': 1,
'description': 'Will not be (re)moved until this seed ratio is met.',
},
{
'name': 'seed_time',
'label': 'Seed time',
'type': 'int',
'default': 40,
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
}
]
}]

View File

@@ -0,0 +1,172 @@
import traceback
from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
import six
log = CPLog(__name__)
class Base(TorrentProvider):
urls = {
'test': 'https://www.iptorrents.com/',
'base_url': 'https://www.iptorrents.com',
'login': 'https://www.iptorrents.com/torrents/',
'login_check': 'https://www.iptorrents.com/inbox.php',
'search': 'https://www.iptorrents.com/torrents/?%s%%s&q=%s&qf=ti&p=%%d',
}
http_time_between_calls = 1 # Seconds
cat_backup_id = None
def buildUrl(self, title, media, quality):
return self._buildUrl(title.replace(':', ''), quality)
def _buildUrl(self, query, quality):
cat_ids = self.getCatId(quality)
if not cat_ids:
log.warning('Unable to find category ids for identifier "%s"', quality.get('identifier'))
return None
return self.urls['search'] % ("&".join(("l%d=" % x) for x in cat_ids), tryUrlencode(query).replace('%', '%%'))
def _searchOnTitle(self, title, media, quality, results):
freeleech = '' if not self.conf('freeleech') else '&free=on'
base_url = self.buildUrl(title, media, quality)
if not base_url: return
pages = 1
current_page = 1
while current_page <= pages and not self.shuttingDown():
data = self.getHTMLData(base_url % (freeleech, current_page))
if data:
html = BeautifulSoup(data)
try:
page_nav = html.find('span', attrs = {'class': 'page_nav'})
if page_nav:
next_link = page_nav.find("a", text = "Next")
if next_link:
final_page_link = next_link.previous_sibling.previous_sibling
pages = int(final_page_link.string)
result_table = html.find('table', attrs = {'class': 'torrents'})
if not result_table or 'nothing found!' in data.lower():
return
entries = result_table.find_all('tr')
for result in entries[1:]:
torrent = result.find_all('td')
if len(torrent) <= 1:
break
torrent = torrent[1].find('a')
torrent_id = torrent['href'].replace('/details.php?id=', '')
torrent_name = six.text_type(torrent.string)
torrent_download_url = self.urls['base_url'] + (result.find_all('td')[3].find('a'))['href'].replace(' ', '.')
torrent_details_url = self.urls['base_url'] + torrent['href']
torrent_size = self.parseSize(result.find_all('td')[5].string)
torrent_seeders = tryInt(result.find('td', attrs = {'class': 'ac t_seeders'}).string)
torrent_leechers = tryInt(result.find('td', attrs = {'class': 'ac t_leechers'}).string)
results.append({
'id': torrent_id,
'name': torrent_name,
'url': torrent_download_url,
'detail_url': torrent_details_url,
'size': torrent_size,
'seeders': torrent_seeders,
'leechers': torrent_leechers,
})
except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
break
current_page += 1
def getLoginParams(self):
return {
'username': self.conf('username'),
'password': self.conf('password'),
'login': 'submit',
}
def loginSuccess(self, output):
return 'don\'t have an account' not in output.lower()
def loginCheckSuccess(self, output):
return '/logout.php' in output.lower()
config = [{
'name': 'iptorrents',
'groups': [
{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'IPTorrents',
'description': '<a href="http://www.iptorrents.com">IPTorrents</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABRklEQVR42qWQO0vDUBiG8zeKY3EqQUtNO7g0J6ZJ1+ifKIIFQXAqDYKCyaaYxM3udrZLHdRFhXrZ6liCW6mubfk874EESgqaeOCF7/Y8hEh41aq6yZi2nyZgBGya9XKtZs4No05pAkZV2YbEmyMMsoSxLQeC46wCTdPPY4HruPQyGIhF97qLWsS78Miydn4XdK46NJ9OsQAYBzMIMf8MQ9wtCnTdWCaIDx/u7uljOIQEe0hiIWPamSTLay3+RxOCSPI9+RJAo7Er9r2bnqjBFAqyK+VyK4f5/Cr5ni8OFKVCz49PFI5GdNvvU7ttE1M1zMU+8AMqFksEhrMnQsBDzqmDAwzx2ehRLwT7yyCI+vSC99c3mozH1NxrJgWWtR1BOECfEJSVCm6WCzJGCA7+IWhBsM4zywDPwEp4vCjx2DzBH2ODAfsDb33Ps6dQwJgAAAAASUVORK5CYII=',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
{
'name': 'username',
'default': '',
},
{
'name': 'password',
'default': '',
'type': 'password',
},
{
'name': 'freeleech',
'default': 0,
'type': 'bool',
'description': 'Only search for [FreeLeech] torrents.',
},
{
'name': 'seed_ratio',
'label': 'Seed ratio',
'type': 'float',
'default': 1,
'description': 'Will not be (re)moved until this seed ratio is met.',
},
{
'name': 'seed_time',
'label': 'Seed time',
'type': 'int',
'default': 40,
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
},
],
}]

View File

@@ -0,0 +1,182 @@
import re
import traceback
from bs4 import BeautifulSoup
from couchpotato.core.helpers.variable import tryInt, getIdentifier
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider
log = CPLog(__name__)
class Base(TorrentMagnetProvider):
urls = {
'detail': '%s/%s',
'search': '%s/%s-i%s/',
}
cat_ids = [
(['cam'], ['cam']),
(['telesync'], ['ts', 'tc']),
(['screener', 'tvrip'], ['screener']),
(['x264', '720p', '1080p', 'blu-ray', 'hdrip'], ['bd50', '1080p', '720p', 'brrip']),
(['dvdrip'], ['dvdrip']),
(['dvd'], ['dvdr']),
]
http_time_between_calls = 1 # Seconds
cat_backup_id = None
proxy_list = [
'https://kickass.to',
'http://kickass.pw',
'http://kickassto.come.in',
'http://katproxy.ws',
'http://www.kickassunblock.info',
'http://www.kickassproxy.info',
'http://katph.eu',
'http://kickassto.come.in',
]
def _search(self, media, quality, results):
data = self.getHTMLData(self.urls['search'] % (self.getDomain(), 'm', getIdentifier(media).replace('tt', '')))
if data:
cat_ids = self.getCatId(quality)
table_order = ['name', 'size', None, 'age', 'seeds', 'leechers']
try:
html = BeautifulSoup(data)
resultdiv = html.find('div', attrs = {'class': 'tabs'})
for result in resultdiv.find_all('div', recursive = False):
if result.get('id').lower().strip('tab-') not in cat_ids:
continue
try:
for temp in result.find_all('tr'):
if temp['class'] is 'firstr' or not temp.get('id'):
continue
new = {}
nr = 0
for td in temp.find_all('td'):
column_name = table_order[nr]
if column_name:
if column_name == 'name':
link = td.find('div', {'class': 'torrentname'}).find_all('a')[2]
new['id'] = temp.get('id')[-7:]
new['name'] = link.text
new['url'] = td.find('a', 'imagnet')['href']
new['detail_url'] = self.urls['detail'] % (self.getDomain(), link['href'][1:])
new['verified'] = True if td.find('a', 'iverify') else False
new['score'] = 100 if new['verified'] else 0
elif column_name is 'size':
new['size'] = self.parseSize(td.text)
elif column_name is 'age':
new['age'] = self.ageToDays(td.text)
elif column_name is 'seeds':
new['seeders'] = tryInt(td.text)
elif column_name is 'leechers':
new['leechers'] = tryInt(td.text)
nr += 1
# Only store verified torrents
if self.conf('only_verified') and not new['verified']:
continue
results.append(new)
except:
log.error('Failed parsing KickAssTorrents: %s', traceback.format_exc())
except AttributeError:
log.debug('No search results found.')
def ageToDays(self, age_str):
age = 0
age_str = age_str.replace('&nbsp;', ' ')
regex = '(\d*.?\d+).(sec|hour|day|week|month|year)+'
matches = re.findall(regex, age_str)
for match in matches:
nr, size = match
mult = 1
if size == 'week':
mult = 7
elif size == 'month':
mult = 30.5
elif size == 'year':
mult = 365
age += tryInt(nr) * mult
return tryInt(age)
def isEnabled(self):
return super(Base, self).isEnabled() and self.getDomain()
def correctProxy(self, data):
return 'search query' in data.lower()
config = [{
'name': 'kickasstorrents',
'groups': [
{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'KickAssTorrents',
'description': '<a href="https://kat.ph/">KickAssTorrents</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACD0lEQVR42pXK20uTcRjA8d/fsJsuap0orBuFlm3hir3JJvQOVmuwllN20Lb2isI2nVHKjBqrCWYaNnNuBrkSWxglhDVJOkBdSWUOq5FgoiOrMdRJ2xPPxW+8OUf1ge/FcyCUSVe2qedK5U/OxNTTXRNXEQ52Glb4O6dNEfK1auJkvRY7+/zxnQbA/D596laXcY3OWOiaIX2393SGznUmxkUo/YkDgqHemuzobQ7+NV+reo5Q1mqp68GABdY3+/EloO+JeN4tEqiFU8f3CwhyWo9E7wfMgI0ELTDx0AvjIxcgvZoC9P7NMN7yMmrFeoKa68rfDfmrARsNN0Ihr55cx59ctZWSiwS5bLKpwW4dYJH+M/B6/CYszE0BFZ+egG+Ln+HRoBN/cpl1pV6COIMkOnBVA/w+fXgGKJVM4LxhumMleoL06hJ3wKcCfl+/TAKKx17gnFePRwkqxR4BQSpFkbCrrQJueI7mWpyfATQ9OQY43+uv/+PutBycJ3y2qn2x7jY50GJvnwLKZjOwspyE5I8F4N+1yr1uwqcs3ym63Hwo29EiAyzUWQVr6WVAS4lZCPutQG/2GtES2YiW3d3XflYKtL72kzAcdEDHeSa3czeIMyyz/TApRKvcFfE0isHbJMnrHCf6xTLb1ORvWNlWo91cvHrJUQo0o6ZoRi7dIiT/g2WEDi27Iyov21xMCvgNfXvtwIACfHwAAAAASUVORK5CYII=',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': True,
},
{
'name': 'domain',
'advanced': True,
'label': 'Proxy server',
'description': 'Domain for requests, keep empty to let CouchPotato pick.',
},
{
'name': 'seed_ratio',
'label': 'Seed ratio',
'type': 'float',
'default': 1,
'description': 'Will not be (re)moved until this seed ratio is met.',
},
{
'name': 'seed_time',
'label': 'Seed time',
'type': 'int',
'default': 40,
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
},
{
'name': 'only_verified',
'advanced': True,
'type': 'bool',
'default': False,
'description': 'Only search for verified releases.'
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
},
],
}]

View File

@@ -1,67 +1,42 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import getTitle, tryInt, mergeDicts
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider
from dateutil.parser import parse
import htmlentitydefs
import json
import re
import time
import traceback
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import getTitle, tryInt, mergeDicts, getIdentifier
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
from dateutil.parser import parse
import six
log = CPLog(__name__)
class PassThePopcorn(TorrentProvider):
class Base(TorrentProvider):
urls = {
'domain': 'https://tls.passthepopcorn.me',
'detail': 'https://tls.passthepopcorn.me/torrents.php?torrentid=%s',
'torrent': 'https://tls.passthepopcorn.me/torrents.php',
'login': 'https://tls.passthepopcorn.me/ajax.php?action=login',
'login_check': 'https://tls.passthepopcorn.me/ajax.php?action=login',
'search': 'https://tls.passthepopcorn.me/search/%s/0/7/%d'
'domain': 'https://tls.passthepopcorn.me',
'detail': 'https://tls.passthepopcorn.me/torrents.php?torrentid=%s',
'torrent': 'https://tls.passthepopcorn.me/torrents.php',
'login': 'https://tls.passthepopcorn.me/ajax.php?action=login',
'login_check': 'https://tls.passthepopcorn.me/ajax.php?action=login',
'search': 'https://tls.passthepopcorn.me/search/%s/0/7/%d'
}
http_time_between_calls = 2
quality_search_params = {
'bd50': {'media': 'Blu-ray', 'format': 'BD50'},
'1080p': {'resolution': '1080p'},
'720p': {'resolution': '720p'},
'brrip': {'media': 'Blu-ray'},
'dvdr': {'resolution': 'anysd'},
'dvdrip': {'media': 'DVD'},
'scr': {'media': 'DVD-Screener'},
'r5': {'media': 'R5'},
'tc': {'media': 'TC'},
'ts': {'media': 'TS'},
'cam': {'media': 'CAM'}
}
def _search(self, media, quality, results):
post_search_filters = {
'bd50': {'Codec': ['BD50']},
'1080p': {'Resolution': ['1080p']},
'720p': {'Resolution': ['720p']},
'brrip': {'Source': ['Blu-ray'], 'Quality': ['High Definition'], 'Container': ['!ISO']},
'dvdr': {'Codec': ['DVD5', 'DVD9']},
'dvdrip': {'Source': ['DVD'], 'Codec': ['!DVD5', '!DVD9']},
'scr': {'Source': ['DVD-Screener']},
'r5': {'Source': ['R5']},
'tc': {'Source': ['TC']},
'ts': {'Source': ['TS']},
'cam': {'Source': ['CAM']}
}
def _search(self, movie, quality, results):
movie_title = getTitle(movie['library'])
movie_title = getTitle(media)
quality_id = quality['identifier']
params = mergeDicts(self.quality_search_params[quality_id].copy(), {
'order_by': 'relevance',
'order_way': 'descending',
'searchstr': movie['library']['identifier']
'searchstr': getIdentifier(media)
})
url = '%s?json=noredirect&%s' % (self.urls['torrent'], tryUrlencode(params))
@@ -88,11 +63,11 @@ class PassThePopcorn(TorrentProvider):
if 'GoldenPopcorn' in torrent and torrent['GoldenPopcorn']:
torrentdesc += ' HQ'
if self.conf('prefer_golden'):
torrentscore += 200
torrentscore += 5000
if 'Scene' in torrent and torrent['Scene']:
torrentdesc += ' Scene'
if self.conf('prefer_scene'):
torrentscore += 50
torrentscore += 2000
if 'RemasterTitle' in torrent and torrent['RemasterTitle']:
torrentdesc += self.htmlToASCII(' %s' % torrent['RemasterTitle'])
@@ -161,24 +136,24 @@ class PassThePopcorn(TorrentProvider):
def htmlToUnicode(self, text):
def fixup(m):
text = m.group(0)
if text[:2] == "&#":
txt = m.group(0)
if txt[:2] == "&#":
# character reference
try:
if text[:3] == "&#x":
return unichr(int(text[3:-1], 16))
if txt[:3] == "&#x":
return unichr(int(txt[3:-1], 16))
else:
return unichr(int(text[2:-1]))
return unichr(int(txt[2:-1]))
except ValueError:
pass
else:
# named entity
try:
text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
txt = unichr(htmlentitydefs.name2codepoint[txt[1:-1]])
except KeyError:
pass
return text # leave as is
return re.sub("&#?\w+;", fixup, u'%s' % text)
return txt # leave as is
return re.sub("&#?\w+;", fixup, six.u('%s') % text)
def unicodeToASCII(self, text):
import unicodedata
@@ -189,11 +164,11 @@ class PassThePopcorn(TorrentProvider):
def getLoginParams(self):
return {
'username': self.conf('username'),
'password': self.conf('password'),
'passkey': self.conf('passkey'),
'keeplogged': '1',
'login': 'Login'
'username': self.conf('username'),
'password': self.conf('password'),
'passkey': self.conf('passkey'),
'keeplogged': '1',
'login': 'Login'
}
def loginSuccess(self, output):
@@ -203,3 +178,90 @@ class PassThePopcorn(TorrentProvider):
return False
loginCheckSuccess = loginSuccess
config = [{
'name': 'passthepopcorn',
'groups': [
{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'PassThePopcorn',
'description': '<a href="https://passthepopcorn.me">PassThePopcorn.me</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAARklEQVQoz2NgIAP8BwMiGWRpIN1JNWn/t6T9f532+W8GkNt7vzz9UkfarZVpb68BuWlbnqW1nU7L2DMx7eCoBlpqGOppCQB83zIgIg+wWQAAAABJRU5ErkJggg==',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False
},
{
'name': 'domain',
'advanced': True,
'label': 'Proxy server',
'description': 'Domain for requests (HTTPS only!), keep empty to use default (tls.passthepopcorn.me).',
},
{
'name': 'username',
'default': '',
},
{
'name': 'password',
'default': '',
'type': 'password',
},
{
'name': 'passkey',
'default': '',
},
{
'name': 'prefer_golden',
'advanced': True,
'type': 'bool',
'label': 'Prefer golden',
'default': 1,
'description': 'Favors Golden Popcorn-releases over all other releases.'
},
{
'name': 'prefer_scene',
'advanced': True,
'type': 'bool',
'label': 'Prefer scene',
'default': 0,
'description': 'Favors scene-releases over non-scene releases.'
},
{
'name': 'require_approval',
'advanced': True,
'type': 'bool',
'label': 'Require approval',
'default': 0,
'description': 'Require staff-approval for releases to be accepted.'
},
{
'name': 'seed_ratio',
'label': 'Seed ratio',
'type': 'float',
'default': 1,
'description': 'Will not be (re)moved until this seed ratio is met.',
},
{
'name': 'seed_time',
'label': 'Seed time',
'type': 'int',
'default': 40,
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 20,
'description': 'Starting score for each release found via this provider.',
}
],
}
]
}]

View File

@@ -0,0 +1,135 @@
import traceback
from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
log = CPLog(__name__)
class Base(TorrentProvider):
urls = {
'test': 'https://www.sceneaccess.eu/',
'login': 'https://www.sceneaccess.eu/login',
'login_check': 'https://www.sceneaccess.eu/inbox',
'detail': 'https://www.sceneaccess.eu/details?id=%s',
'search': 'https://www.sceneaccess.eu/browse?c%d=%d',
'archive': 'https://www.sceneaccess.eu/archive?&c%d=%d',
'download': 'https://www.sceneaccess.eu/%s',
}
http_time_between_calls = 1 # Seconds
def _searchOnTitle(self, title, media, quality, results):
url = self.buildUrl(title, media, quality)
data = self.getHTMLData(url)
if data:
html = BeautifulSoup(data)
try:
resultsTable = html.find('table', attrs = {'id': 'torrents-table'})
if resultsTable is None:
return
entries = resultsTable.find_all('tr', attrs = {'class': 'tt_row'})
for result in entries:
link = result.find('td', attrs = {'class': 'ttr_name'}).find('a')
url = result.find('td', attrs = {'class': 'td_dl'}).find('a')
leechers = result.find('td', attrs = {'class': 'ttr_leechers'}).find('a')
torrent_id = link['href'].replace('details?id=', '')
results.append({
'id': torrent_id,
'name': link['title'],
'url': self.urls['download'] % url['href'],
'detail_url': self.urls['detail'] % torrent_id,
'size': self.parseSize(result.find('td', attrs = {'class': 'ttr_size'}).contents[0]),
'seeders': tryInt(result.find('td', attrs = {'class': 'ttr_seeders'}).find('a').string),
'leechers': tryInt(leechers.string) if leechers else 0,
'get_more_info': self.getMoreInfo,
})
except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
def getMoreInfo(self, item):
full_description = self.getCache('sceneaccess.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
html = BeautifulSoup(full_description)
nfo_pre = html.find('div', attrs = {'id': 'details_table'})
description = toUnicode(nfo_pre.text) if nfo_pre else ''
item['description'] = description
return item
# Login
def getLoginParams(self):
return {
'username': self.conf('username'),
'password': self.conf('password'),
'submit': 'come on in',
}
def loginSuccess(self, output):
return '/inbox' in output.lower()
loginCheckSuccess = loginSuccess
config = [{
'name': 'sceneaccess',
'groups': [
{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'SceneAccess',
'description': '<a href="https://sceneaccess.eu/">SceneAccess</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAACT0lEQVR4AYVQS0sbURidO3OTmajJ5FElTTOkPmZ01GhHrIq0aoWAj1Vc+A/cuRMXbl24V9SlCGqrLhVFCrooEhCp2BAx0mobTY2kaR7qmOm87EXL1EWxh29xL+c7nPMdgGHYO5bF/gdbefnr6WlbWRnxluMwAB4Z0uEgXa7nwaDL7+/RNPzxbYvb/XJ0FBYVfd/ayh0fQ4qCGEHcm0KLRZUk7Pb2YRJPRwcsKMidnKD3t9VVT3s7BDh+z5FOZ3Vfn3h+Hltfx00mRRSRWFcUmmVNhYVqPn8dj3va2oh+txvcQRVF9ebm1fi4k+dRFbosY5rm4Hk7xxULQnJnx93S4g0EIEEQRoDLo6PrWEw8Pc0eHLwYGopMTDirqlJ7eyhYYGHhfgfHCcKYksZGVB/NcXI2mw6HhZERqrjYTNPHi4tFPh8aJIYIhgPlcCRDoZLW1s75+Z/7+59nZ/OJhLWigqAoKZX6Mjf3dXkZ3pydGYLc4aEoCCkInzQ1fRobS2xuvllaonkedfArnY5OTdGVldBkOADgqq2Nr6z8CIWaJietDHOhKB+HhwFKC6Gnq4ukKJvP9zcSbjYDXbeVlkKzuZBhnnV3e3t6UOmaJO0ODibW1hB1GYkg8R/gup7Z3TVZLJ5AILW9LcZiVpYtYBhw16O3t7cauckyeF9Tgz0ATpL2+nopmWycmbnY2LiKRjFk6/d7+/vRJfl4HGzV1T0UIM43MGBvaIBWK/YvwM5w+IMgGH8tkyEgvIpE7M3Nt6qqZrNyOq1kMmouh455Ggz+BhKY4GEc2CfwAAAAAElFTkSuQmCC',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
{
'name': 'username',
'default': '',
},
{
'name': 'password',
'default': '',
'type': 'password',
},
{
'name': 'seed_ratio',
'label': 'Seed ratio',
'type': 'float',
'default': 1,
'description': 'Will not be (re)moved until this seed ratio is met.',
},
{
'name': 'seed_time',
'label': 'Seed time',
'type': 'int',
'default': 40,
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 20,
'description': 'Starting score for each release found via this provider.',
}
],
},
],
}]

View File

@@ -0,0 +1,172 @@
import re
import traceback
from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider
import six
log = CPLog(__name__)
class Base(TorrentMagnetProvider):
urls = {
'detail': '%s/torrent/%s',
'search': '%s/search/%%s/%%s/7/%%s'
}
cat_backup_id = 200
disable_provider = False
http_time_between_calls = 0
proxy_list = [
'https://nobay.net',
'https://thebay.al',
'https://thepiratebay.se',
'http://thepiratebay.cd',
'http://thebootlegbay.com',
'http://www.tpb.gr',
'http://tpbproxy.co.uk',
'http://pirateproxy.in',
'http://www.getpirate.com',
'http://piratebay.io',
'http://bayproxy.li',
'http://proxybay.pw',
]
def _search(self, media, quality, results):
page = 0
total_pages = 1
cats = self.getCatId(quality)
base_search_url = self.urls['search'] % self.getDomain()
while page < total_pages:
search_url = base_search_url % self.buildUrl(media, page, cats)
page += 1
data = self.getHTMLData(search_url)
if data:
try:
soup = BeautifulSoup(data)
results_table = soup.find('table', attrs = {'id': 'searchResult'})
if not results_table:
return
try:
total_pages = len(soup.find('div', attrs = {'align': 'center'}).find_all('a'))
except:
pass
entries = results_table.find_all('tr')
for result in entries[1:]:
link = result.find(href = re.compile('torrent\/\d+\/'))
download = result.find(href = re.compile('magnet:'))
try:
size = re.search('Size (?P<size>.+),', six.text_type(result.select('font.detDesc')[0])).group('size')
except:
continue
if link and download:
def extra_score(item):
trusted = (0, 10)[result.find('img', alt = re.compile('Trusted')) is not None]
vip = (0, 20)[result.find('img', alt = re.compile('VIP')) is not None]
confirmed = (0, 30)[result.find('img', alt = re.compile('Helpers')) is not None]
moderated = (0, 50)[result.find('img', alt = re.compile('Moderator')) is not None]
return confirmed + trusted + vip + moderated
results.append({
'id': re.search('/(?P<id>\d+)/', link['href']).group('id'),
'name': six.text_type(link.string),
'url': download['href'],
'detail_url': self.getDomain(link['href']),
'size': self.parseSize(size),
'seeders': tryInt(result.find_all('td')[2].string),
'leechers': tryInt(result.find_all('td')[3].string),
'extra_score': extra_score,
'get_more_info': self.getMoreInfo
})
except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
def isEnabled(self):
return super(Base, self).isEnabled() and self.getDomain()
def correctProxy(self, data):
return 'title="Pirate Search"' in data
def getMoreInfo(self, item):
full_description = self.getCache('tpb.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
html = BeautifulSoup(full_description)
nfo_pre = html.find('div', attrs = {'class': 'nfo'})
description = ''
try:
description = toUnicode(nfo_pre.text)
except:
pass
item['description'] = description
return item
config = [{
'name': 'thepiratebay',
'groups': [
{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'ThePirateBay',
'description': 'The world\'s largest bittorrent tracker. <a href="http://fucktimkuik.org/">ThePirateBay</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAA3UlEQVQY02P4DwT/YADIZvj//7qnozMYODmtAAusZoCDELDAegYGViZhAWZmRoYoqIDupfhNN1M3dTBEggXWMZg9jZRXV77YxhAOFpjDwMAPMoCXmcHsF1SAQZ6bQY2VgUEbKHClcAYzg3mINEO8jSCD478/DPsZmvqWblu1bOmStes3Pp0ezVDF4Gif0Hfx9///74/ObRZ2YNiZ47C8XIRBxFJR0jbSSUud4f9zAQWn8NTuziAt2zy5xIMM/z8LFX0E+fD/x0MRDCeA1v7Z++Y/FDzyvAtyBxIA+h8A8ZKLeT+lJroAAAAASUVORK5CYII=',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False
},
{
'name': 'domain',
'advanced': True,
'label': 'Proxy server',
'description': 'Domain for requests, keep empty to let CouchPotato pick.',
},
{
'name': 'seed_ratio',
'label': 'Seed ratio',
'type': 'float',
'default': 1,
'description': 'Will not be (re)moved until this seed ratio is met.',
},
{
'name': 'seed_time',
'label': 'Seed time',
'type': 'int',
'default': 40,
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
}
]
}]

View File

@@ -0,0 +1,136 @@
import traceback
from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
log = CPLog(__name__)
class Base(TorrentProvider):
urls = {
'test': 'https://www.torrentbytes.net/',
'login': 'https://www.torrentbytes.net/takelogin.php',
'login_check': 'https://www.torrentbytes.net/inbox.php',
'detail': 'https://www.torrentbytes.net/details.php?id=%s',
'search': 'https://www.torrentbytes.net/browse.php?search=%s&cat=%d',
'download': 'https://www.torrentbytes.net/download.php?id=%s&name=%s',
}
cat_ids = [
([5], ['720p', '1080p', 'bd50']),
([19], ['cam']),
([19], ['ts', 'tc']),
([19], ['r5', 'scr']),
([19], ['dvdrip']),
([19], ['brrip']),
([20], ['dvdr']),
]
http_time_between_calls = 1 # Seconds
cat_backup_id = None
def _searchOnTitle(self, title, movie, quality, results):
url = self.urls['search'] % (tryUrlencode('%s %s' % (title.replace(':', ''), movie['info']['year'])), self.getCatId(quality)[0])
data = self.getHTMLData(url)
if data:
html = BeautifulSoup(data)
try:
result_table = html.find('table', attrs = {'border': '1'})
if not result_table:
return
entries = result_table.find_all('tr')
for result in entries[1:]:
cells = result.find_all('td')
link = cells[1].find('a', attrs = {'class': 'index'})
full_id = link['href'].replace('details.php?id=', '')
torrent_id = full_id[:6]
results.append({
'id': torrent_id,
'name': link.contents[0],
'url': self.urls['download'] % (torrent_id, link.contents[0]),
'detail_url': self.urls['detail'] % torrent_id,
'size': self.parseSize(cells[6].contents[0] + cells[6].contents[2]),
'seeders': tryInt(cells[8].find('span').contents[0]),
'leechers': tryInt(cells[9].find('span').contents[0]),
})
except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
def getLoginParams(self):
return {
'username': self.conf('username'),
'password': self.conf('password'),
'login': 'submit',
}
def loginSuccess(self, output):
return 'logout.php' in output.lower() or 'Welcome' in output.lower()
loginCheckSuccess = loginSuccess
config = [{
'name': 'torrentbytes',
'groups': [
{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'TorrentBytes',
'description': '<a href="http://torrentbytes.net">TorrentBytes</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAeFBMVEUAAAAAAEQAA1QAEmEAKnQALHYAMoEAOokAQpIASYsASZgAS5UATZwATosATpgAVJ0AWZwAYZ4AZKAAaZ8Ab7IAcbMAfccAgcQAgcsAhM4AiscAjMkAmt0AoOIApecAp/EAqvQAs+kAt+wA3P8A4f8A//8VAAAfDbiaAl08AAAAjUlEQVQYGQXBO04DQRAFwHqz7Z8sECIl5f73ISRD5GBs7UxTlWfg9vYXnvJRQJqOL88D6BAwJtMMumHUVCl60aa6H93IrIv0b+157f1lpk+fm87lMWrZH0vncKbXdRUQrRmrh9C6Iwkq6rg4PXZcyXmbizzeV/g+rDra0rGve8jPKLSOJNi2AQAwAGjwD7ApPkEHdtPQAAAAAElFTkSuQmCC',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
{
'name': 'username',
'default': '',
},
{
'name': 'password',
'default': '',
'type': 'password',
},
{
'name': 'seed_ratio',
'label': 'Seed ratio',
'type': 'float',
'default': 1,
'description': 'Will not be (re)moved until this seed ratio is met.',
},
{
'name': 'seed_time',
'label': 'Seed time',
'type': 'int',
'default': 40,
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 20,
'description': 'Starting score for each release found via this provider.',
}
],
},
],
}]

View File

@@ -0,0 +1,114 @@
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
log = CPLog(__name__)
class Base(TorrentProvider):
urls = {
'test': 'http://www.td.af/',
'login': 'http://www.td.af/torrents/',
'login_check': 'http://www.torrentday.com/userdetails.php',
'detail': 'http://www.td.af/details.php?id=%s',
'search': 'http://www.td.af/V3/API/API.php',
'download': 'http://www.td.af/download.php/%s/%s',
}
http_time_between_calls = 1 # Seconds
def _searchOnTitle(self, title, media, quality, results):
query = '"%s" %s' % (title, media['info']['year'])
data = {
'/browse.php?': None,
'cata': 'yes',
'jxt': 8,
'jxw': 'b',
'search': query,
}
data = self.getJsonData(self.urls['search'], data = data)
try: torrents = data.get('Fs', [])[0].get('Cn', {}).get('torrents', [])
except: return
for torrent in torrents:
results.append({
'id': torrent['id'],
'name': torrent['name'],
'url': self.urls['download'] % (torrent['id'], torrent['fname']),
'detail_url': self.urls['detail'] % torrent['id'],
'size': self.parseSize(torrent.get('size')),
'seeders': tryInt(torrent.get('seed')),
'leechers': tryInt(torrent.get('leech')),
})
def getLoginParams(self):
return {
'username': self.conf('username'),
'password': self.conf('password'),
'submit.x': 18,
'submit.y': 11,
'submit': 'submit',
}
def loginSuccess(self, output):
return 'Password not correct' not in output
def loginCheckSuccess(self, output):
return 'logout.php' in output.lower()
config = [{
'name': 'torrentday',
'groups': [
{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'TorrentDay',
'description': '<a href="http://www.td.af/">TorrentDay</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC5ElEQVQ4y12TXUgUURTH//fO7Di7foeQJH6gEEEIZZllVohfSG/6UA+RSFAQQj74VA8+Bj30lmAlRVSEvZRfhNhaka5ZUG1paKaW39tq5O6Ou+PM3M4o6m6X+XPPzD3zm/+dcy574r515WfIW8CZBM4YAA5Gc/aQC3yd7oXYEONcsISE5dTDh91HS0t7FEWhBUAeN9ynV/d9qJAgE4AECURAcVsGlCCnly26LMA0IQwTa52dje3d3e3hcPi8qqrrMjcVYI3EHCQZlkFOHBwR2QHh2ASAAIJxWGAQEDxjePhs3527XjJwnb37OHBq0T+Tyyjh+9KnEzNJ7nouc1Q/3A3HGsOvnJy+PSUlj81w2Lny9WuJ6+3AmTjD4HOcrdR2dWXLRQePvyaSLfQOPMPC8mC9iHCsOxSyzJCelzdSXlNzD5ujpb25Wbfc/XXJemTXF4+nnCNq+AMLe50uFfEJTiw4GXSFtiHL0SnIq66+p0kSArqO+eH3RdsAv9+f5vW7L7GICq6rmM8XBCAXlBw90rOyxibn5yzfkg/L09M52/jxqdESaIrBXHYZZbB1GX8cEpySxKIB8S5XcOnvqpli1zuwmrTtoLjw5LOK/eeuWsE4JH5IRPaPZKiKigmPp+5pa+u1aEjIMhEgrRkmi9mgxGUhM7LNJSzOzsE3+cOeExovXOjdytE0LV4zqNZUtV0uZzAGoGkhDH/2YHZiErmv4uyWQnZZWc+hoqL3WzlTExN5hhA8IEwkZWZOxwB++30YG/9GkYCPvqAaHAW5uWPROW86OmqCprUR7z1yZDAGQNuCvkoB/baIKUBWMTYymv+gra3eJNvjXu+B562tFyXqTJ6YuHK8rKwvBmC3vR7cOCPQLWFz8LnfXWUrJo9U19BwMyUlJRjTSMJ2ENxUiGxq9KXQfwqYlnWstvbR5aamG9g0uzM8Q4OFt++3NNixQ2NgYmeN03FOTUv7XVpV9aKisvLl1vN/WVhNc/Fi1NEAAAAASUVORK5CYII=',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
{
'name': 'username',
'default': '',
},
{
'name': 'password',
'default': '',
'type': 'password',
},
{
'name': 'seed_ratio',
'label': 'Seed ratio',
'type': 'float',
'default': 1,
'description': 'Will not be (re)moved until this seed ratio is met.',
},
{
'name': 'seed_time',
'label': 'Seed time',
'type': 'int',
'default': 40,
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
},
],
}]

View File

@@ -0,0 +1,126 @@
import traceback
from bs4 import BeautifulSoup
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
import six
log = CPLog(__name__)
class Base(TorrentProvider):
urls = {
'test': 'http://www.torrentleech.org/',
'login': 'http://www.torrentleech.org/user/account/login/',
'login_check': 'http://torrentleech.org/user/messages',
'detail': 'http://www.torrentleech.org/torrent/%s',
'search': 'http://www.torrentleech.org/torrents/browse/index/query/%s/categories/%d',
'download': 'http://www.torrentleech.org%s',
}
http_time_between_calls = 1 # Seconds
cat_backup_id = None
def _searchOnTitle(self, title, media, quality, results):
url = self.urls['search'] % self.buildUrl(title, media, quality)
data = self.getHTMLData(url)
if data:
html = BeautifulSoup(data)
try:
result_table = html.find('table', attrs = {'id': 'torrenttable'})
if not result_table:
return
entries = result_table.find_all('tr')
for result in entries[1:]:
link = result.find('td', attrs = {'class': 'name'}).find('a')
url = result.find('td', attrs = {'class': 'quickdownload'}).find('a')
details = result.find('td', attrs = {'class': 'name'}).find('a')
results.append({
'id': link['href'].replace('/torrent/', ''),
'name': six.text_type(link.string),
'url': self.urls['download'] % url['href'],
'detail_url': self.urls['download'] % details['href'],
'size': self.parseSize(result.find_all('td')[4].string),
'seeders': tryInt(result.find('td', attrs = {'class': 'seeders'}).string),
'leechers': tryInt(result.find('td', attrs = {'class': 'leechers'}).string),
})
except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
def getLoginParams(self):
return {
'username': self.conf('username'),
'password': self.conf('password'),
'remember_me': 'on',
'login': 'submit',
}
def loginSuccess(self, output):
return '/user/account/logout' in output.lower() or 'welcome back' in output.lower()
loginCheckSuccess = loginSuccess
config = [{
'name': 'torrentleech',
'groups': [
{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'TorrentLeech',
'description': '<a href="http://torrentleech.org">TorrentLeech</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACHUlEQVR4AZVSO48SYRSdGTCBEMKzILLAWiybkKAGMZRUUJEoDZX7B9zsbuQPYEEjNLTQkYgJDwsoSaxspEBsCITXjjNAIKi8AkzceXgmbHQ1NJ5iMufmO9/9zrmXlCSJ+B8o75J8Pp/NZj0eTzweBy0Wi4PBYD6f12o1r9ebTCZx+22HcrnMsuxms7m6urTZ7LPZDMVYLBZ8ZV3yo8aq9Pq0wzCMTqe77dDv9y8uLyAWBH6xWOyL0K/56fcb+rrPgPZ6PZfLRe1fsl6vCUmGKIqoqNXqdDr9Dbjps9znUV0uTqdTjuPkDoVCIfcuJ4gizjMMm8u9vW+1nr04czqdK56c37CbKY9j2+1WEARZ0Gq1RFHAz2q1qlQqXxoN69HRcDjUarW8ZD6QUigUOnY8uKYH8N1sNkul9yiGw+F6vS4Rxn8EsodEIqHRaOSnq9T7ajQazWQycEIR1AEBYDabSZJyHDucJyegwWBQr9ebTCaKvHd4cCQANUU9evwQ1Ofz4YvUKUI43GE8HouSiFiNRhOowWBIpVLyHITJkuW3PwgAEf3pgIwxF5r+OplMEsk3CPT5szCMnY7EwUdhwUh/CXiej0Qi3idPz89fdrpdbsfBzH7S3Q9K5pP4c0sAKpVKoVAQGO1ut+t0OoFAQHkH2Da/3/+but3uarWK0ZMQoNdyucRutdttmqZxMTzY7XaYxsrgtUjEZrNhkSwWyy/0NCatZumrNQAAAABJRU5ErkJggg==',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
{
'name': 'username',
'default': '',
},
{
'name': 'password',
'default': '',
'type': 'password',
},
{
'name': 'seed_ratio',
'label': 'Seed ratio',
'type': 'float',
'default': 1,
'description': 'Will not be (re)moved until this seed ratio is met.',
},
{
'name': 'seed_time',
'label': 'Seed time',
'type': 'int',
'default': 40,
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 20,
'description': 'Starting score for each release found via this provider.',
}
],
},
],
}]

View File

@@ -0,0 +1,188 @@
from urlparse import urlparse
import re
import traceback
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import splitString, tryInt, tryFloat
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.base import ResultList
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
log = CPLog(__name__)
class Base(TorrentProvider):
urls = {}
limits_reached = {}
http_time_between_calls = 1 # Seconds
def search(self, media, quality):
hosts = self.getHosts()
results = ResultList(self, media, quality, imdb_results = True)
for host in hosts:
if self.isDisabled(host):
continue
self._searchOnHost(host, media, quality, results)
return results
def _searchOnHost(self, host, media, quality, results):
torrents = self.getJsonData(self.buildUrl(media, host), cache_timeout = 1800)
if torrents:
try:
if torrents.get('error'):
log.error('%s: %s', (torrents.get('error'), host['host']))
elif torrents.get('results'):
for torrent in torrents.get('results', []):
results.append({
'id': torrent.get('torrent_id'),
'protocol': 'torrent' if re.match('^(http|https|ftp)://.*$', torrent.get('download_url')) else 'torrent_magnet',
'provider_extra': urlparse(host['host']).hostname or host['host'],
'name': toUnicode(torrent.get('release_name')),
'url': torrent.get('download_url'),
'detail_url': torrent.get('details_url'),
'size': torrent.get('size'),
'score': host['extra_score'],
'seeders': torrent.get('seeders'),
'leechers': torrent.get('leechers'),
'seed_ratio': host['seed_ratio'],
'seed_time': host['seed_time'],
})
except:
log.error('Failed getting results from %s: %s', (host['host'], traceback.format_exc()))
def getHosts(self):
uses = splitString(str(self.conf('use')), clean = False)
hosts = splitString(self.conf('host'), clean = False)
names = splitString(self.conf('name'), clean = False)
seed_times = splitString(self.conf('seed_time'), clean = False)
seed_ratios = splitString(self.conf('seed_ratio'), clean = False)
pass_keys = splitString(self.conf('pass_key'), clean = False)
extra_score = splitString(self.conf('extra_score'), clean = False)
host_list = []
for nr in range(len(hosts)):
try: key = pass_keys[nr]
except: key = ''
try: host = hosts[nr]
except: host = ''
try: name = names[nr]
except: name = ''
try: ratio = seed_ratios[nr]
except: ratio = ''
try: seed_time = seed_times[nr]
except: seed_time = ''
host_list.append({
'use': uses[nr],
'host': host,
'name': name,
'seed_ratio': tryFloat(ratio),
'seed_time': tryInt(seed_time),
'pass_key': key,
'extra_score': tryInt(extra_score[nr]) if len(extra_score) > nr else 0
})
return host_list
def belongsTo(self, url, provider = None, host = None):
hosts = self.getHosts()
for host in hosts:
result = super(Base, self).belongsTo(url, host = host['host'], provider = provider)
if result:
return result
def isDisabled(self, host = None):
return not self.isEnabled(host)
def isEnabled(self, host = None):
# Return true if at least one is enabled and no host is given
if host is None:
for host in self.getHosts():
if self.isEnabled(host):
return True
return False
return TorrentProvider.isEnabled(self) and host['host'] and host['pass_key'] and int(host['use'])
config = [{
'name': 'torrentpotato',
'groups': [
{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'TorrentPotato',
'order': 10,
'description': 'CouchPotato torrent provider. Checkout <a href="https://github.com/RuudBurger/CouchPotatoServer/wiki/CouchPotato-Torrent-Provider">the wiki page about this provider</a> for more info.',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAABSElEQVR4AZ2Nz0oCURTGv8t1YMpqUxt9ARFxoQ/gQtppgvUKcu/sxB5iBJkogspaBC6iVUplEC6kv+oiiKDNhAtt16roP0HQgdsMLgaxfvy4nHP4Pi48qE2g4v91JOqT1CH/UnA7w7icUlLawyEdj+ZI/7h6YluWbRiddHonHh9M70aj7VTKzuXuikUMci/EO/ACnAI15599oAk8AR/AgxBQNCzreD7bmpl+FOIVuAHqQDUcJo+AK+CZFKLt95/MpSmMt0TiW9POxse6UvYZ6zB2wFgjFiNpOGesR0rZ0PVPXf8KhUCl22CwClz4eN8weoZBb9c0bdPsOWvHx/cYu9Y0CoNoZTJrwAbn5DrnZc6XOV+igVbnsgo0IxEomlJuA1vUIYGyq3PZBChwmExCUSmVZgMBDIUCK4UCFIv5vHIhm/XUDeAf/ADbcpd5+aXSWQAAAABJRU5ErkJggg==',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
{
'name': 'use',
'default': ''
},
{
'name': 'host',
'default': '',
'description': 'The url path of your TorrentPotato provider.',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'default': '0',
'description': 'Starting score for each release found via this provider.',
},
{
'name': 'name',
'label': 'Username',
'default': '',
},
{
'name': 'seed_ratio',
'label': 'Seed ratio',
'default': '1',
'description': 'Will not be (re)moved until this seed ratio is met.',
},
{
'name': 'seed_time',
'label': 'Seed time',
'default': '40',
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
},
{
'name': 'pass_key',
'default': ',',
'label': 'Pass Key',
'description': 'Can be found on your profile page',
'type': 'combined',
'combine': ['use', 'host', 'pass_key', 'name', 'seed_ratio', 'seed_time', 'extra_score'],
},
],
},
],
}]

View File

@@ -0,0 +1,132 @@
import traceback
from bs4 import BeautifulSoup
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
import six
log = CPLog(__name__)
class Base(TorrentProvider):
urls = {
'test': 'https://torrentshack.net/',
'login': 'https://torrentshack.net/login.php',
'login_check': 'https://torrentshack.net/inbox.php',
'detail': 'https://torrentshack.net/torrent/%s',
'search': 'https://torrentshack.net/torrents.php?action=advanced&searchstr=%s&scene=%s&filter_cat[%d]=1',
'download': 'https://torrentshack.net/%s',
}
http_time_between_calls = 1 # Seconds
def _search(self, media, quality, results):
url = self.urls['search'] % self.buildUrl(media, quality)
data = self.getHTMLData(url)
if data:
html = BeautifulSoup(data)
try:
result_table = html.find('table', attrs = {'id': 'torrent_table'})
if not result_table:
return
entries = result_table.find_all('tr', attrs = {'class': 'torrent'})
for result in entries:
link = result.find('span', attrs = {'class': 'torrent_name_link'}).parent
url = result.find('td', attrs = {'class': 'torrent_td'}).find('a')
results.append({
'id': link['href'].replace('torrents.php?torrentid=', ''),
'name': six.text_type(link.span.string).translate({ord(six.u('\xad')): None}),
'url': self.urls['download'] % url['href'],
'detail_url': self.urls['download'] % link['href'],
'size': self.parseSize(result.find_all('td')[5].string),
'seeders': tryInt(result.find_all('td')[7].string),
'leechers': tryInt(result.find_all('td')[8].string),
})
except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
def getLoginParams(self):
return {
'username': self.conf('username'),
'password': self.conf('password'),
'keeplogged': '1',
'login': 'Login',
}
def loginSuccess(self, output):
return 'logout.php' in output.lower()
loginCheckSuccess = loginSuccess
def getSceneOnly(self):
return '1' if self.conf('scene_only') else ''
config = [{
'name': 'torrentshack',
'groups': [
{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'TorrentShack',
'description': '<a href="https://www.torrentshack.net/">TorrentShack</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABmElEQVQoFQXBzY2cVRiE0afqvd84CQiAnxWWtyxsS6ThINBYg2Dc7mZBMEjE4mzs6e9WcY5+ePNuVFJJodQAoLo+SaWCy9rcV8cmjah3CI6iYu7oRU30kE5xxELRfamklY3k1NL19sSm7vPzP/ZdNZzKVDaY2sPZJBh9fv5ITrmG2+Vp4e1sPchVqTCQZJnVXi+/L4uuAJGly1+Pw8CprLbi8Om7tbT19/XRqJUk11JP9uHj9ulxhXbvJbI9qJvr5YkGXFG2IBT8tXczt+sfzDZCp3765f3t9tHEHGEDACma77+8o4oATKk+/PfW9YmHruRFjWoVSFsVsGu1YSKq6Oc37+n98unPZSRlY7vsKDqN+92X3yR9+PdXee3iJNKMStqdcZqoTJbUSi5JOkpfRlhSI0mSpEmCFKoU7FqSNOLAk54uGwCStMUCgLrVic62g7oDoFmmdI+P3S0pDe1xvDqb6XrZqbtzShWNoh9fv/XQHaDdM9OqrZi2M7M3UrB2vlkPS1IbdEBk7UiSoD6VlZ6aKWer4aH4f/AvKoHUTjuyAAAAAElFTkSuQmCC',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
{
'name': 'username',
'default': '',
},
{
'name': 'password',
'default': '',
'type': 'password',
},
{
'name': 'seed_ratio',
'label': 'Seed ratio',
'type': 'float',
'default': 1,
'description': 'Will not be (re)moved until this seed ratio is met.',
},
{
'name': 'seed_time',
'label': 'Seed time',
'type': 'int',
'default': 40,
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
},
{
'name': 'scene_only',
'type': 'bool',
'default': False,
'description': 'Only allow scene releases.'
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
},
],
}]

View File

@@ -0,0 +1,129 @@
import re
import traceback
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import tryInt, splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider
import six
log = CPLog(__name__)
class Base(TorrentMagnetProvider, RSS):
urls = {
'detail': 'https://torrentz.eu/%s',
'search': 'https://torrentz.eu/feed?q=%s',
'verified_search': 'https://torrentz.eu/feed_verified?q=%s'
}
http_time_between_calls = 0
def _search(self, media, quality, results):
search_url = self.urls['verified_search'] if self.conf('verified_only') else self.urls['search']
# Create search parameters
search_params = self.buildUrl(media)
smin = quality.get('size_min')
smax = quality.get('size_max')
if smin and smax:
search_params += ' size %sm - %sm' % (smin, smax)
min_seeds = tryInt(self.conf('minimal_seeds'))
if min_seeds:
search_params += ' seed > %s' % (min_seeds - 1)
rss_data = self.getRSSData(search_url % search_params)
if rss_data:
try:
for result in rss_data:
name = self.getTextElement(result, 'title')
detail_url = self.getTextElement(result, 'link')
description = self.getTextElement(result, 'description')
magnet = splitString(detail_url, '/')[-1]
magnet_url = 'magnet:?xt=urn:btih:%s&dn=%s&tr=%s' % (magnet.upper(), tryUrlencode(name), tryUrlencode('udp://tracker.openbittorrent.com/announce'))
reg = re.search('Size: (?P<size>\d+) MB Seeds: (?P<seeds>[\d,]+) Peers: (?P<peers>[\d,]+)', six.text_type(description))
size = reg.group('size')
seeds = reg.group('seeds').replace(',', '')
peers = reg.group('peers').replace(',', '')
results.append({
'id': magnet,
'name': six.text_type(name),
'url': magnet_url,
'detail_url': detail_url,
'size': tryInt(size),
'seeders': tryInt(seeds),
'leechers': tryInt(peers),
})
except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
config = [{
'name': 'torrentz',
'groups': [
{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'Torrentz',
'description': 'Torrentz is a free, fast and powerful meta-search engine. <a href="https://torrentz.eu/">Torrentz</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAQklEQVQ4y2NgAALjtJn/ycEMlGiGG0IVAxiwAKzOxaKGARcgxgC8YNSAwWoAzuRMjgsIugqfAUR5CZcBRIcHsWEAADSA96Ig020yAAAAAElFTkSuQmCC',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': True
},
{
'name': 'verified_only',
'type': 'bool',
'default': True,
'advanced': True,
'description': 'Only search verified releases',
},
{
'name': 'minimal_seeds',
'type': 'int',
'default': 1,
'advanced': True,
'description': 'Only return releases with minimal X seeds',
},
{
'name': 'seed_ratio',
'label': 'Seed ratio',
'type': 'float',
'default': 1,
'description': 'Will not be (re)moved until this seed ratio is met.',
},
{
'name': 'seed_time',
'label': 'Seed time',
'type': 'int',
'default': 40,
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
}
]
}]

View File

@@ -0,0 +1,120 @@
import traceback
from couchpotato.core.helpers.variable import tryInt, getIdentifier
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider
log = CPLog(__name__)
class Base(TorrentMagnetProvider):
urls = {
'test': '%s/api',
'search': '%s/api/list.json?keywords=%s&quality=%s',
'detail': '%s/api/movie.json?id=%s'
}
http_time_between_calls = 1 # seconds
proxy_list = [
'http://yify.unlocktorrent.com',
'http://yify-torrents.com.come.in',
'http://yts.re',
'http://yts.im'
'http://yify-torrents.im',
]
def search(self, movie, quality):
if not quality.get('hd', False):
return []
return super(Base, self).search(movie, quality)
def _search(self, movie, quality, results):
domain = self.getDomain()
if not domain:
return
search_url = self.urls['search'] % (domain, getIdentifier(movie), quality['identifier'])
data = self.getJsonData(search_url)
if data and data.get('MovieList'):
try:
for result in data.get('MovieList'):
if result['Quality'] and result['Quality'] not in result['MovieTitle']:
title = result['MovieTitle'] + ' BrRip ' + result['Quality']
else:
title = result['MovieTitle'] + ' BrRip'
results.append({
'id': result['MovieID'],
'name': title,
'url': result['TorrentMagnetUrl'],
'detail_url': self.urls['detail'] % (domain, result['MovieID']),
'size': self.parseSize(result['Size']),
'seeders': tryInt(result['TorrentSeeds']),
'leechers': tryInt(result['TorrentPeers']),
})
except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
def correctProxy(self, data):
data = data.lower()
return 'yify' in data and 'yts' in data
config = [{
'name': 'yify',
'groups': [
{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'Yify',
'description': 'Free provider, less accurate. Small HD movies, encoded by <a href="https://yify-torrents.com/">Yify</a>.',
'wizard': False,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACL0lEQVR4AS1SPW/UQBAd23fxne/Ld2dvzvHuzPocEBAKokCBqGiQ6IgACYmvUKRBFEQgKKGg4BAlUoggggYUEQpSHOI7CIEoQs/fYcbLaU/efTvvvZlnA1qydoxU5kcxX0CkgmQZtPy0hCUjvK+WgEByOZ5dns1O5bzna8fRVkgsxH8B0YouIvBhdD5T11NiVOoKrsttyUcpRW0InUrFnwe9HzuP2uaQZYhF2LQ76TTXw2RVMTK8mYYbjfh+zNquMVCrqn93aArLSixPxnafdGDLaz1tjY5rmNa8z5BczEQOxQfCl1GyoqoWxYRN1bkh7ELw3q/vhP6HIL4TG9KumpjgvwuyM7OsjSj98E/vszMfZ7xvPtMaWxGO5crwIumKCR5HxDtJ0AWKGG204RfUd/3smJYqwem/Q7BTS1ZGfM4LNpVwuKAz6cMeROst0S2EwNE7GjTehO2H3dxqIpdkydat15G3F8SXBi4GlpBNlSz012L/k2+W0CLLk/jbcf13rf41yJeMQ8QWUZiHCfCA9ad+81nEKPtoS9mJOf9v0NmMJHgUT6xayheK9EIK7JJeU/AF4scDF7Y5SPlJrRcxJ+um4ibNEdObxLiIwJim+eT2AL5D9CIcnZ5zvSJi9eIlNHVVtZ831dk5svPgvjPWTq+ktWkd/kD0qtm71x+sDQe3kt6DXnM7Ct+GajmTxKlkAokWljyAKSm5oWa2w+BH4P2UuVub7eTyiGOQYapY/wEztHduSDYz5gAAAABJRU5ErkJggg==',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False
},
{
'name': 'domain',
'advanced': True,
'label': 'Proxy server',
'description': 'Domain for requests, keep empty to let CouchPotato pick.',
},
{
'name': 'seed_ratio',
'label': 'Seed ratio',
'type': 'float',
'default': 1,
'description': 'Will not be (re)moved until this seed ratio is met.',
},
{
'name': 'seed_time',
'label': 'Seed time',
'type': 'int',
'default': 40,
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
}
]
}]

View File

@@ -1,9 +1,11 @@
from urlparse import urlparse
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import simplifyString
from couchpotato.core.helpers.variable import getImdb, md5
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from urlparse import urlparse
log = CPLog(__name__)

View File

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

View File

@@ -1,6 +1,6 @@
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.helpers.variable import mergeDicts, getImdb
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
@@ -35,12 +35,21 @@ class Search(Plugin):
elif isinstance(types, (list, tuple, set)):
types = list(types)
imdb_identifier = getImdb(q)
if not types:
result = fireEvent('info.search', q = q, merge = True)
if imdb_identifier:
result = fireEvent('movie.info', identifier = imdb_identifier, merge = True)
result = {result['type']: [result]}
else:
result = fireEvent('info.search', q = q, merge = True)
else:
result = {}
for media_type in types:
result[media_type] = fireEvent('%s.search' % media_type)
if imdb_identifier:
result[media_type] = fireEvent('%s.info' % media_type, identifier = imdb_identifier)
else:
result[media_type] = fireEvent('%s.search' % media_type, q = q)
return mergeDicts({
'success': True,

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