Compare commits

..

329 Commits

Author SHA1 Message Date
Ruud Burger
147e565249 Merge pull request #4157 from seedzero/tv-develop-merge
[TV] Merge 'develop' into 'tv'
2014-11-09 14:39:06 +01:00
seedzero
4e568ff515 Merge branch 'develop' into tv 2014-10-28 17:37:56 +11:00
Ruud
02b6659235 Don't show ignored in ETA message 2014-10-20 20:46:07 +02:00
Ruud
dacc3d8f47 Remove torrentleech 2014-10-19 22:47:29 +02:00
Ruud
4f140bb1ac Remove print 2014-10-19 22:41:50 +02:00
Ruud
3dffaa7075 Update Tornado 2014-10-19 16:13:03 +02:00
Ruud
51c8de0fc3 Force filename renamer setting 2014-10-16 22:39:39 +02:00
Ruud
4f23ccc284 Also rename on old version database
fix #4083
2014-10-15 21:28:52 +02:00
Ruud
a6ff34a47f Only check exists on file 2014-10-14 22:49:19 +02:00
Ruud
f1a2d960bc Make tab font smaller 2014-10-13 13:24:57 +02:00
Ruud
4e7069e0c6 Deluge, allow "no port"
fix #4055
2014-10-13 13:20:21 +02:00
Ruud
477a47e45e Don't show fanart error on future movies 2014-10-13 13:12:44 +02:00
Ruud
a3264240ab Don't error out on future movies 2014-10-13 13:10:38 +02:00
Ruud
f9d9fffedb Don't ss int or float 2014-10-12 16:41:48 +02:00
Ruud
6b4e9a3fac Remove chardet from requests library 2014-10-11 22:57:57 +02:00
Ruud Burger
6787289846 Merge pull request #4054 from joshka/patch-1
Fix default permissions on files to remove execute bits on files #4053
2014-10-11 18:04:08 +02:00
Joshua McKinney
d31a2e2768 Fix default permissions on files to remove execute bits on files #4053 2014-10-12 03:01:20 +11:00
Ruud
c992680209 Meta and Middle click not triggering new tab 2014-10-11 15:35:56 +02:00
Ruud
65f0dc25d2 Allow 1080p in shitty quality releases 2014-10-11 14:32:12 +02:00
Ruud
b616af3a83 Make minimum scoring editable
fix #4042
2014-10-10 23:16:58 +02:00
Ruud
ca13107330 Ignore exceptions on removing db_backup stuff 2014-10-10 22:46:35 +02:00
Ruud
c7ce18f8c2 Better error message for missing cd number 2014-10-10 15:13:58 +02:00
Ruud
b6f288a522 Close request connection 2014-10-09 23:10:28 +02:00
Ruud
cb48ca03df Remove profile when marking movies done 2014-10-08 23:05:04 +02:00
Ruud
7b6641d709 Never restatus "down" when adding release 2014-10-08 23:00:11 +02:00
Ruud
3c12a2c4bf Don't restatus movies to active when scanning manage section 2014-10-08 22:35:56 +02:00
Ruud
259e2bc61c Don't skip unpacking on manage scan 2014-10-08 21:58:34 +02:00
Ruud
9f6e4cc2fa Remove NotifyMyWP 2014-10-08 20:46:11 +02:00
Ruud
a763957334 Log minimum 1 second wait 2014-10-07 22:53:57 +02:00
Ruud
06293dc0a2 Simplify tmdb provider 2014-10-07 22:50:22 +02:00
Ruud
38a5d967dd Remove tmdb3 lib 2014-10-07 21:40:41 +02:00
Ruud
4cdb9bc81d Remove tmdb3 dependency 2014-10-07 21:40:32 +02:00
Ruud
2104cb2839 Always try to return version string 2014-10-07 20:30:51 +02:00
Ruud
d4a4bd40a8 Always return version info 2014-10-07 20:14:17 +02:00
Ruud
ba47d7eea7 Use torrent-duplicate if returned from Transmission
fix #4014
2014-10-07 11:46:42 +02:00
Ruud
c9638ec3fa Encode templ download destination 2014-10-05 18:47:30 +02:00
Ruud
14d636d098 Return image filepath in unicode 2014-10-05 18:46:44 +02:00
Ruud
e1d4df7937 Give api encoding error log 2014-10-05 18:46:16 +02:00
Ruud
e08d06ba31 Update chardet 2014-10-05 14:40:27 +02:00
Ruud
984ee7580d Force unicode on file loading for guessit 2014-10-05 13:53:57 +02:00
Ruud
ab118ea580 Windows special chars not returning any folders 2014-10-05 13:38:22 +02:00
Ruud
f897eebb41 Allow regex in required and ignored words
close #3376
2014-10-05 12:37:00 +02:00
Ruud
755873c5e7 Don't ss int and float 2014-10-05 12:33:13 +02:00
Ruud
dbc254efbe Also encode log with names tuples 2014-10-05 11:15:55 +02:00
Ruud
9de8ed2dee Don't migrate old databases 2014-10-05 11:05:56 +02:00
Ruud
230b7f47cc Always save encode logger stuff 2014-10-05 00:41:20 +02:00
Ruud
58878d8a0f Replace non existing chars 2014-10-05 00:18:22 +02:00
Ruud
d9bb1bfbfb - Debug code 2014-10-04 22:57:18 +02:00
Ruud
f8674f9baa Grammar 2014-10-04 22:17:58 +02:00
Ruud
20f1076037 Don't load suggestions when chartview active 2014-10-04 21:48:46 +02:00
Ruud
e84f2aa04c Don't load charts if suggestion tab is enabled 2014-10-04 17:47:21 +02:00
Ruud
01f70051f8 For people who can't read good 2014-10-04 17:37:25 +02:00
Ruud
492f69b149 Actually use the smtp port from settings
fix #4003
2014-10-04 16:53:40 +02:00
Ruud
2270b2a28b Don't force parser for trailer searching 2014-10-04 16:50:01 +02:00
Ruud
b5a0418a36 Make available space check optional
fix #3973
2014-10-04 16:26:33 +02:00
Ruud
e595722139 Safari hanging on password input creation
Fix #3997
2014-10-04 15:37:18 +02:00
Ruud
78ba855c68 Add CP tag only when renamer or unique tag is enabled. 2014-10-04 14:33:51 +02:00
Ruud
158f638fb9 No need for double replace 2014-10-04 14:07:38 +02:00
Ruud
5bea9dd04f Always return safestring on renamer replace 2014-09-29 16:24:57 +02:00
Ruud
910393d00e Allow original without cd name 2014-09-26 15:33:38 +02:00
Ruud
4b66b0ea07 Add NZBVortex group support
fix #1279
2014-09-23 22:36:01 +02:00
Ruud
543226450c NZBVortex status checking 2014-09-23 22:22:27 +02:00
Ruud
b9dbadda0b Add randomstring support to cptag 2014-09-23 22:00:27 +02:00
Ruud
7cb214d8a2 Don't force send host with every request 2014-09-23 20:33:26 +02:00
Ruud
f6d4ddbe80 NZBVortex, create unique ID 2014-09-23 17:40:21 +02:00
Ruud
faefd7a5b5 Traks notifier always enabled 2014-09-23 16:54:24 +02:00
Ruud
8f02b0eea0 Api documention updates
close #3955
2014-09-23 12:36:46 +02:00
Ruud
39d0f91de2 Add permission calculator link
#3953
2014-09-23 12:27:09 +02:00
Ruud
b3d75cb485 Check if file got moved successful on move/copy
close #3893
2014-09-23 10:02:36 +02:00
Ruud
17b940a271 Allow 5 redirects 2014-09-23 00:38:44 +02:00
Ruud
3338b72d1f Stop endless redirect loop
fix #3931
2014-09-23 00:38:36 +02:00
Ruud
70ca31a265 Only allow single redirect for now
fix #3931
2014-09-23 00:29:48 +02:00
Ruud
d7f43c2cf8 Make minimum seeders configurable
fix #3202
2014-09-22 22:15:13 +02:00
Ruud
b1f88c1c48 Allow https for Transmission
close #3880
2014-09-22 21:53:27 +02:00
Ruud
6fa6d530ec Remove older backup folders 2014-09-22 21:45:43 +02:00
Ruud
11e7fb23ca Stream larger file download
fix #2488
2014-09-22 21:38:53 +02:00
Ruud
da9d2b5ed8 Check free diskspace before starting moving files
fix #3893
2014-09-22 21:03:13 +02:00
Ruud Burger
2599bac1a4 Merge pull request #3944 from softcat/develop
Fixed filmstarts.de provider
2014-09-21 23:12:25 +02:00
Ruud
0bae509311 Use github img url for Growl notification
fix #1363
2014-09-21 22:29:52 +02:00
Ruud
2fa7834e6e Allow typing in directory setting
closes #479
2014-09-21 22:17:09 +02:00
Ruud
2deb6ee6a7 Trakt not moving movie to collection
fix #3018
2014-09-21 15:57:29 +02:00
Ruud
0d166025d0 Safestring before base encode 2014-09-21 15:13:50 +02:00
Ruud
7861416dc5 Don't write over files already renamed 2014-09-21 10:43:53 +02:00
Ruud
2639c5e9ad Force add <cd> if not set
fix #1811
2014-09-21 10:40:29 +02:00
Ruud
c4db4ace13 Log move, copy, link 2014-09-21 00:09:56 +02:00
Ruud
db367a80d1 Do proper cleanup after rename 2014-09-20 23:58:54 +02:00
softcat
3093b21555 Fixed filmstarts.de provider 2014-09-20 17:57:51 +02:00
Ruud
9b62e32da8 Symlink failing on encode
fix #3371
2014-09-19 10:42:48 +02:00
Ruud
a0b3ee8186 Safe encode path names in renamer
fix #3425
2014-09-19 10:39:54 +02:00
Ruud
d70da1edce TorrentShack use correct columns
fix #3940
2014-09-19 10:20:02 +02:00
Ruud
7c674b3aab Re-add movie giving rev conflict
fix #3939
2014-09-19 10:03:02 +02:00
Ruud
98540f2fcd Make sure original_folder isn't empty
fix #3747
2014-09-19 00:15:40 +02:00
Ruud
2f0e197320 Mark faulty movies done 2014-09-19 00:15:18 +02:00
Ruud
db49585818 Renamer doesn't loop over all movies properly 2014-09-18 21:32:31 +02:00
Ruud
160bc1a5c4 Always release lock 2014-09-18 20:57:46 +02:00
Ruud
8e23b02653 Stop on 429 code 2014-09-18 20:19:49 +02:00
Ruud
41e69aeac3 Delete nzbindex provider 2014-09-18 19:52:10 +02:00
Ruud
be30200a18 Use correct arg 2014-09-18 17:57:10 +02:00
Ruud
387650d040 Don't tag recent for fixed ignored movies 2014-09-18 17:51:21 +02:00
Ruud
052d64eb39 Force restatus on ignored movies 2014-09-18 17:45:55 +02:00
Ruud
a3a8a820fe release.update_status not triggered on frontend 2014-09-18 16:49:44 +02:00
Ruud
1b724b5606 Media got tagged with ignored, instead of release 2014-09-18 16:04:21 +02:00
Ruud
5fc9d7182c Hide urllib3 error
closes #3887
2014-09-18 14:26:34 +02:00
Ruud
c948216e33 I need to watch more Sesame Street..
26 letters in the alphabet + # for numbers is 27..
2014-09-18 14:17:00 +02:00
Ruud
035b99bc8a Don't use event when not needed 2014-09-17 23:06:59 +02:00
Ruud
f74b837faa Ignore RecordDeleted in release for media call 2014-09-17 23:05:01 +02:00
Ruud
4c198f7116 Ignore RecordDeleted on notification getter
fix #3888
2014-09-17 22:51:27 +02:00
Ruud
76322c0145 Don't save data in notification 2014-09-17 21:50:41 +02:00
Ruud
12150c5efc Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-09-17 12:59:29 +02:00
Ruud
4a9452672a Use status_code to stop requesting url 2014-09-17 12:59:24 +02:00
Ruud
f7eeaf3eda Locking mechanism 2014-09-16 22:44:33 +02:00
Ruud
002ce4d4e1 Ignore deleted media documents 2014-09-16 22:05:54 +02:00
Ruud
80df57f2b6 Delete corrupted documents 2014-09-16 22:00:54 +02:00
Ruud
0358378cae Fix marshal data corrupted documents 2014-09-16 15:16:32 +02:00
Ruud
fa054b6b34 Migration: Don't fail on missing release file 2014-09-16 13:55:44 +02:00
Ruud Burger
4b9e226cc6 Merge pull request #3884 from botez/develop
Update iptorrents.py
2014-09-11 13:58:18 +02:00
Ruud Burger
6c586f8b19 Merge pull request #3909 from seedzero/tv-list-api
[TV] 'list' API's return correct type
2014-09-11 13:57:57 +02:00
seedzero
bb609e073b [TV] Fix for new 'list' API output 2014-09-11 21:36:05 +10:00
seedzero
02571d0f5d 'list' API's, return as media type 2014-09-11 21:34:19 +10:00
Ruud
ca24bf031c Change quality test 2014-09-08 19:21:31 +02:00
Ruud
af8806e292 Move webrip to scr alternative 2014-09-07 21:50:20 +02:00
Ruud
4f646094b5 Add quality test 2014-09-07 21:50:08 +02:00
Ruud
6e8503cfc5 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-09-07 21:43:16 +02:00
Ruud
4879bc6251 Move hdtv & hdrip to alternative for brrips 2014-09-07 21:42:45 +02:00
Ruud
ab253f9030 Add quality test 2014-09-07 21:41:59 +02:00
Ruud
bc6d197004 Don't score identifier quality.guess twice 2014-09-07 21:40:54 +02:00
Troy Olson
1de457fa8d Update iptorrents.py
Adds another URL parameter when searching on iptorrents for brrip.  This allows it to find brrip that are also classified as 720p/1080p.

See this thread for info:
https://couchpota.to/forum/viewtopic.php?f=5&t=4032&sid=7892abbaaca9dad8bd3cc27cafb7fd33

And this prior pull request for more info:
https://github.com/RuudBurger/CouchPotatoServer/pull/3696
2014-09-03 12:33:04 -07:00
Ruud
9e564c49b3 Update library: requests 2014-09-03 16:47:03 +02:00
Ruud
50a150f570 Update library: httplib2 2014-09-03 16:47:03 +02:00
Ruud
8d55b0c92a Assume non-3d if quality guess fails 2014-09-02 21:11:54 +02:00
Ruud
5a2df62462 Add quality test 2014-09-01 22:51:55 +02:00
Ruud
9d21dd9196 Newznab download status code not caught properly 2014-09-01 22:13:49 +02:00
Ruud
3b34196901 Also untag active movies 2014-09-01 14:57:59 +02:00
Ruud
bad26026ae Clean done after week, not 3 days 2014-09-01 14:50:42 +02:00
Ruud
6e455e62d5 Remove debug print 2014-09-01 14:49:46 +02:00
Ruud
c97bd38c83 Import cleanup 2014-09-01 14:23:26 +02:00
Ruud
356322c5b1 Use named argument for with_status calls 2014-09-01 13:40:14 +02:00
Ruud
9dbb477dd8 Use latest found release if files are the same 2014-08-31 23:11:28 +02:00
Ruud
089609d5d2 Give higher penalty for allowed identifiers 2014-08-31 22:19:41 +02:00
Ruud
487ddf1c25 Don't check message in dev mode 2014-08-31 22:19:09 +02:00
Ruud
83b4c17969 Better quality guessing based on size 2014-08-31 16:26:25 +02:00
Ruud
08c381cf0d Use video metadata titles for scoring 2014-08-31 15:03:51 +02:00
Ruud
286f14a6d2 Return titles included in video headers 2014-08-31 15:03:10 +02:00
Ruud
0b14fe5454 Look at all releases for restatus "done" 2014-08-31 15:02:35 +02:00
Ruud
c5a0d521d1 Simplify coming soon / late listing 2014-08-31 01:11:40 +02:00
Ruud
4a1f70da09 ConnectionError not caught properly 2014-08-31 00:23:06 +02:00
Ruud
87e97cd8a5 Only show poster when available
fix #3866
2014-08-30 17:06:51 +02:00
Ruud
e0dffe20a4 Update lib: Tornado 2014-08-30 14:01:12 +02:00
Ruud
73d37584ad Cleanup import 2014-08-30 14:00:57 +02:00
Ruud Burger
5fd3e86624 Merge pull request #3857 from MLWALK3R/develop
TPB & Kickass proxy update
Use https for torrentleech
2014-08-29 12:39:44 +02:00
Ruud
b0ff526c95 Improved quality matching
fixes #3829
2014-08-28 16:33:59 +02:00
Ruud
3cfe90d581 break backlog on error 2014-08-28 15:25:25 +02:00
Ruud
1d60d9caf1 Blu-ray backlog not working
fixes #3826
2014-08-28 15:23:01 +02:00
Michael Walker
8e0d1520e8 Removed URL
URL had a captcha
2014-08-28 12:44:24 +01:00
Ruud
b07f91d6a5 Wrap exceptions around score calculations
Fix #3859
2014-08-28 13:41:41 +02:00
Michael Walker
43af091b02 SSL'd Links
SSL'd Links
2014-08-28 02:13:22 +01:00
Michael Walker
5f0543ba42 Updated URL's
Removed dead links
2014-08-28 02:10:30 +01:00
Michael Walker
ef8cd1aa40 URL Fix
Domain is missing WWW recond casing an ISSUE.
2014-08-28 01:59:37 +01:00
Michael Walker
e01fe51b9e Merge pull request #1 from RuudBurger/develop
Develop
2014-08-28 01:58:20 +01:00
Ruud
afa782194d Remove ending seperator 2014-08-27 22:05:16 +02:00
Ruud
77e602f359 Use proper bitsoup variable 2014-08-27 21:45:53 +02:00
Ruud
a6063b0665 Merge branch 'fuzeman-develop_tv_sync' into develop 2014-08-27 21:44:43 +02:00
Ruud
9a7e4ea500 Merge branch 'develop_tv_sync' of git://github.com/fuzeman/CouchPotatoServer into fuzeman-develop_tv_sync 2014-08-27 21:44:09 +02:00
Ruud
1daedb7259 Update bitsoup tables
closes #3807
2014-08-27 20:07:27 +02:00
Ruud Burger
8e82e976f1 Merge pull request #3809 from dkboy/develop
Update bitsoup.py table order
2014-08-27 19:48:40 +02:00
Ruud
8b445ac9f9 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-08-27 19:46:53 +02:00
Ruud
91c24105cc Dockers readme install 2014-08-27 19:45:46 +02:00
Ruud Burger
13df26851e Merge pull request #3836 from clinton-hall/patch-1
Ignore separator at end of string. Fixes #3823
2014-08-27 19:41:37 +02:00
Ruud
ca58d25785 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-08-27 19:40:47 +02:00
Ruud
42d728f71e Update ubuntu start script
closes #3846
2014-08-27 19:40:28 +02:00
Ruud Burger
659960899e Merge pull request #3848 from xombiemp/develop
Fix brrip search parameters for passthepopcorn
2014-08-27 19:38:43 +02:00
Ruud Burger
d40b052cbc Merge pull request #3852 from MLWALK3R/patch-2
Updated TPB URL's
2014-08-27 19:38:11 +02:00
Ruud
282f6fb73a Only get fanart with extended info 2014-08-27 19:35:52 +02:00
Ruud
416c9eabde Update charts every 3 days 2014-08-27 19:35:32 +02:00
Michael Walker
b4a15f344d Updated URL's
Replaced TPB links with working URL's
2014-08-26 15:53:10 +01:00
Andrew Parker
c545c9aab1 Couchpotato will identify a release with 'hdtv' in the name as brrip. The passthepopcorn provider searched for brrip with the parameters media=Blu-ray. This created a condition where a 720p hdtv movie would not be snatched because the 720p quality would skip it due to being marked as 'brrip', and br-rip quality would not find it because it was not from a Blu-ray source.
This changes the search parameters for brrip to resolution=anyhd to remove the requirement that it's source media is Blu-ray.
2014-08-25 09:43:06 -06:00
Clinton Hall
c0492a41d9 Ignore separator at end of string. Fixes #3823 2014-08-22 11:58:29 +09:30
Dean Gardiner
60e8c3ad9b Merge branch 'develop_tv_sync' into tv 2014-08-20 15:03:12 +12:00
seedzero
cfd92b8268 Documentation added for media type .list & .delete APIs 2014-08-20 15:00:09 +12:00
Dean Gardiner
436883a96d Fixed media.types & addSingleListView
addSingleCharView, addSingleDeleteView

Conflicts:

	couchpotato/core/media/show/_base/main.py
2014-08-20 14:59:57 +12:00
seedzero
c381b719b1 Stop movie searcher searching for TV shows and
hosing episodes
2014-08-20 14:59:11 +12:00
Dean Gardiner
81d4d9a4e2 Changed "media.with_identifiers" to remove "No media found with..." messages 2014-08-20 14:58:55 +12:00
Dean Gardiner
e2df3a4dfd Added children to "library.related" 2014-08-20 14:58:27 +12:00
Dean Gardiner
7df92f2882 Fix possible dashboard error, add "types" parameter to "media.with_status", limit suggestions to movies (for now) 2014-08-20 14:58:04 +12:00
Dean Gardiner
072b6d09fa Renamed "[media].update_info" event to "[media].update" 2014-08-20 14:57:35 +12:00
Dean Gardiner
3869e350bf Added "media_id" parameter to "library.tree" event 2014-08-20 14:57:16 +12:00
Dean Gardiner
058846f54f Added "find" helper function 2014-08-20 14:57:02 +12:00
Dean Gardiner
cd836f3660 Include releases in "library.tree" 2014-08-20 14:56:36 +12:00
Dean Gardiner
d75f58f5ec Fixed "library.related" and "libary.tree" to work with "show.episode", 'show.season" media types 2014-08-20 14:56:13 +12:00
Dean Gardiner
f2b0d3f80b Switched "library.tree" to use "media_children" index 2014-08-20 14:55:56 +12:00
Dean Gardiner
a366d57278 Added "library.tree" event/api call 2014-08-20 14:55:34 +12:00
Dean Gardiner
a821d85bf2 Fixed MediaBase.getPoster(), switched MovieBase to use this generic method 2014-08-20 14:55:07 +12:00
Dean Gardiner
a1ce3e0d6b Added "library.root" event, fixes to "matcher", "release" and "score" to use "library.root" + handle missing "year" 2014-08-20 14:54:24 +12:00
Dean Gardiner
e7be5c7809 Added "library.related" event and "library.query", "library.related" API calls 2014-08-20 14:52:08 +12:00
Dean Gardiner
894f46a741 Merge branch 'develop' into tv 2014-08-20 14:26:19 +12:00
Dean Gardiner
7d5efad20c Merge pull request #3817 from seedzero/tv 2014-08-20 13:54:45 +12:00
seedzero
ba14c95e82 Documentation added for media type .list & .delete APIs 2014-08-19 01:36:12 +10:00
seedzero
2ad249b195 Fixed media.types & addSingleListView
addSingleCharView, addSingleDeleteView
2014-08-18 23:34:23 +10:00
dkboy
89f3b6624e Update bitsoup.py
They've rearranged the table
2014-08-17 14:52:43 +12:00
Ruud Burger
3546f29caf Merge pull request #3771 from fenduru/ptp-freeleech
Give higher weight to freeleech torrents
2014-08-16 13:32:45 +02:00
Ruud
e3414fe91f Remove movie if no releases are left after delete 2014-08-16 13:29:34 +02:00
Ruud
bdadd00d93 Don't add & on url creation 2014-08-16 12:44:52 +02:00
Ruud
dd7de31e9f Update TorrentShack url
fix #3797
2014-08-14 21:28:17 +02:00
Dean Gardiner
deb7943203 Fixed broken quality profile identifiers 2014-08-10 13:12:52 +12:00
Dean Gardiner
4e78b0cac1 Merge pull request #3750 from seedzero/tv 2014-08-10 13:06:09 +12:00
seedzero
c8f0cdc90f Newznab search fixes 2014-08-10 12:47:25 +12:00
seedzero
ce80ac5a33 Fix show search not including quality profile 2014-08-10 12:46:29 +12:00
seedzero
5e438e5343 Stop movie searcher searching for TV shows and
hosing episodes
2014-08-10 12:39:59 +12:00
fenduru
6897dab647 Give higher weight to freeleech torrents 2014-08-09 11:20:23 -04:00
Dean Gardiner
12dd9c6b14 [TV] Updated ShowBase.create() to use "media.with_identifiers" 2014-08-07 14:06:31 +12:00
Dean Gardiner
478dc0f242 Changed "media.with_identifiers" to remove "No media found with..." messages 2014-08-07 14:05:05 +12:00
Dean Gardiner
5d886ccf1f [TV] Moved "episode" and "season" modules into "show/_base/", fixed episode update bug 2014-08-07 12:46:58 +12:00
Dean Gardiner
7f466f9c08 [TV] Split matcher into separate modules 2014-08-07 12:45:58 +12:00
Dean Gardiner
7fbd89a317 [TV] Use trakt.tv for show searches (better text searching, posters) 2014-08-05 16:14:18 +12:00
Dean Gardiner
6f620f451b Merge branch 'tv_season_searcher' into tv 2014-08-03 23:01:00 +12:00
Dean Gardiner
dea5bbbf1c Update score plugin to use the "root" media (show, movie) title 2014-08-03 22:58:25 +12:00
Dean Gardiner
68bde6086d [TV] Fixed incorrect 'release.delete' call in searcher and issue adding shows 2014-08-03 22:56:59 +12:00
Dean Gardiner
34bb8c7993 [TV] Fixed issue retrieving episodes in season searcher 2014-08-03 22:05:56 +12:00
Dean Gardiner
74c7cf4381 Added children to "library.related" 2014-08-03 22:04:49 +12:00
Dean Gardiner
efe0a4af53 [TV] Minor adjustments to season item UI 2014-08-03 22:03:54 +12:00
Dean Gardiner
b9c6d983e1 [TV] Added season actions/releases 2014-08-03 18:37:05 +12:00
Dean Gardiner
3d6ce1c2e2 [TV} Working show and season searcher, fixed season correctRelease/matcher 2014-08-03 17:26:16 +12:00
Ruud
accf19bb26 Different log level 2014-08-01 13:33:59 +02:00
Ruud
4126007cac Don't download 0 seed torrents
fix #3728
2014-08-01 13:29:39 +02:00
Ruud
9f12fe2636 Add edge meta for IE
fix #3727
2014-08-01 13:23:00 +02:00
Ruud
9fb348f3a4 Don't try to ignore None release 2014-08-01 13:21:15 +02:00
Ruud
e749d132cd Better message 2014-07-28 16:12:54 +02:00
Ruud Burger
bed9458604 Update contributing.md 2014-07-28 16:06:38 +02:00
Ruud
7984ee9fcf Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2014-07-28 16:02:49 +02:00
Ruud
69e3e36fae Update contribute 2014-07-28 16:01:27 +02:00
Ruud
456563eab0 Update log post text 2014-07-28 16:01:16 +02:00
Dean Gardiner
a06bfcb3bf Merge branch 'tv_xem' into tv 2014-07-28 20:39:10 +12:00
Dean Gardiner
fe2e508e4c Fix possible dashboard error, add "types" parameter to "media.with_status", limit suggestions to movies (for now) 2014-07-28 20:33:50 +12:00
Dean Gardiner
72cb53bcc0 [TV] Fixed xem episode updates and finished adding "update_extras" events 2014-07-28 20:19:51 +12:00
Dean Gardiner
90be6ec38b [TV] Renamed "[media].update_info" events, renamed "updateInfo" functions 2014-07-28 19:55:09 +12:00
Dean Gardiner
212d5c5432 Renamed "[media].update_info" event to "[media].update" 2014-07-28 19:54:06 +12:00
Dean Gardiner
b10e25ab8c [TV] Disabled excessive logging from tvdb_api 2014-07-28 19:45:45 +12:00
Dean Gardiner
5c4f8186df [TV] Restructured and cleaned "show.add" and "show.update_info" 2014-07-28 19:45:22 +12:00
Dean Gardiner
02d4a7625b [TV] Fixes to xem info provider, updated data structure 2014-07-28 16:14:05 +12:00
Dean Gardiner
8018ef979f [TV] Fixes to TheTVDb.getSeasonInfo 2014-07-28 16:13:13 +12:00
Dean Gardiner
482f5f82e6 [TV] Disable tvdb query simplifying (API doesn't support "fuzzy" matching) 2014-07-28 16:12:31 +12:00
Dean Gardiner
88f8cd708b [TV] Implemented fast show updates, working on "update_info" restructure 2014-07-28 16:11:20 +12:00
Dean Gardiner
aa92d76eb4 Added "media_id" parameter to "library.tree" event 2014-07-28 16:06:51 +12:00
Dean Gardiner
3e05bc8d78 Added "find" helper function 2014-07-28 16:06:05 +12:00
Ruud
7b6fa4f0e5 Transmission failed to download 2014-07-27 21:28:06 +02:00
Ruud
cd1dc39ef2 Add bd50 torrentleech 2014-07-27 21:14:31 +02:00
Dean Gardiner
4de9879927 [TV] Fixed dashboard issues with shows 2014-07-28 00:42:10 +12:00
Dean Gardiner
479e20d8f3 [TV] Added "eta" display placeholder (data not there yet) 2014-07-27 23:51:44 +12:00
Dean Gardiner
f7ed5d4b2f Merge remote-tracking branch 'RuudBurger/develop' into tv
Conflicts:
	couchpotato/core/plugins/quality/main.py
2014-07-27 23:49:49 +12:00
Dean Gardiner
bda44848a1 [TV] Added "full_search" placeholder methods to avoid errors on startup 2014-07-27 15:08:28 +12:00
Dean Gardiner
f3ae8a05cc [TV] Added "status" to episode and season media 2014-07-27 15:07:52 +12:00
Dean Gardiner
43275297e9 [TV] Improved episode actions drop-down (releases) 2014-07-27 15:05:51 +12:00
Dean Gardiner
d79556f36f [TV] Moved imdb and refresh components to new "episode.actions.js", implemented episode "release" action 2014-07-27 13:52:54 +12:00
Dean Gardiner
8fe3d6f58f [TV] Adjust episode table column size, added quality indicators 2014-07-27 01:05:41 +12:00
Dean Gardiner
a1ca367037 Include releases in "library.tree" 2014-07-27 01:03:58 +12:00
Dean Gardiner
bfdf565a0d [TV] Changed show list to call "media.available_chars" correctly 2014-07-27 00:18:48 +12:00
Dean Gardiner
c77eaabbff [TV] Update messages containing "movie", fixed alignment and search box 2014-07-27 00:15:14 +12:00
Dean Gardiner
44063dfcc5 [TV] Only expand/extend height when showing the episodes view 2014-07-26 23:00:41 +12:00
Dean Gardiner
c2c98f644b [TV] Fixed matcher and provider events 2014-07-26 22:56:26 +12:00
Dean Gardiner
74caecbe89 Merge branch 'tv_interface' into tv 2014-07-26 22:35:09 +12:00
Dean Gardiner
a721a40d5e Merge branch 'tv_metadata' into tv 2014-07-26 22:34:58 +12:00
Dean Gardiner
338e645579 Merge branch 'tv_searcher' into tv 2014-07-26 22:34:46 +12:00
Dean Gardiner
5f2dd0aac3 [TV] Fixed episode info updates 2014-07-26 22:31:58 +12:00
Dean Gardiner
0f434afd33 [TV] Prefix child media types with "show." 2014-07-26 22:30:48 +12:00
Dean Gardiner
364527b0b2 Fixed "library.related" and "libary.tree" to work with "show.episode", 'show.season" media types 2014-07-26 22:29:16 +12:00
Dean Gardiner
ac857301ac [TV] Create "Episode" class, "media.refresh" is now fired 2014-07-26 14:23:20 +12:00
Dean Gardiner
c038c66dc9 Switched "library.tree" to use "media_children" index 2014-07-26 13:54:55 +12:00
Dean Gardiner
c81891683c [TV] Cleaner season/episode titles in list, move specials to bottom 2014-07-25 17:12:42 +12:00
Dean Gardiner
d787cb0cdb [TV] Build out basic show interface with episode list 2014-07-25 16:59:42 +12:00
Dean Gardiner
2d5a3e7564 Added "library.tree" event/api call 2014-07-25 16:56:18 +12:00
Dean Gardiner
7ae178e2a6 Fixed MediaBase.getPoster(), switched MovieBase to use this generic method 2014-07-25 13:44:18 +12:00
Dean Gardiner
e885ade131 [TV] Fixed show posters 2014-07-25 13:44:07 +12:00
Dean Gardiner
0925dd08bc [TV] Split searcher into separate modules, searching/snatching mostly working again 2014-07-25 11:59:27 +12:00
Dean Gardiner
050d8ccfda Added "library.root" event, fixes to "matcher", "release" and "score" to use "library.root" + handle missing "year" 2014-07-25 11:58:27 +12:00
Dean Gardiner
4efdca91d5 [TV] Added temporary TV qualities 2014-07-25 11:34:32 +12:00
Dean Gardiner
0d128a3525 [TV} Fixed query/identifier event handlers and moved them to [media.show.library] 2014-07-25 10:54:00 +12:00
Dean Gardiner
0f97e57307 Added "library.related" event and "library.query", "library.related" API calls 2014-07-25 10:52:53 +12:00
Ruud
0771aeac3b Update nzbgeek api url 2014-07-20 16:20:12 +02:00
Ruud
cd0afd20e5 Always reconnect Transmission. fix #3631 2014-07-17 23:58:51 +02:00
Ruud
324920cd8c Don't migrate when db is closed 2014-07-17 23:25:35 +02:00
Ruud
12cda35494 Try fix migration failure from 2.5.1 2014-07-17 22:24:48 +02:00
Ruud Burger
1c1af9f90c Merge pull request #3620 from peerster/develop
Include instructions for Linux with systemd
2014-07-12 20:21:23 +02:00
Ruud
687221f035 Update last_edit when tagging with recent 2014-07-12 19:38:49 +02:00
peerster
a99d52392f Another layout fix 2014-07-12 16:40:24 +02:00
peerster
bd6690b159 layout fix 2014-07-12 16:34:41 +02:00
peerster
b13df16b53 Added systemd specific instructions 2014-07-12 16:29:42 +02:00
Ruud
06f49be090 Don't error out on media.get. fix #3611 2014-07-11 16:47:15 +02:00
Ruud
0b48ad5084 Change fanart api url 2014-07-11 16:24:45 +02:00
Ruud
32ce93d2e9 Encode video path 2014-07-11 10:30:32 +02:00
Ruud
e0479e79bd AwesomeHD not returning proper size. fix #3587 2014-07-08 20:24:18 +02:00
Ruud
04e22b3966 XBMC error > info2 2014-07-08 20:09:24 +02:00
Ruud
40a5ce087b Better label for Pushbullet settings 2014-07-06 22:19:45 +02:00
Ruud
330e15bbcb Snatched not giving enough data to show notification. fix #3564 2014-07-04 20:36:48 +02:00
Ruud
d201d9fff9 Allow change of file move action 2014-07-02 19:02:55 +02:00
Ruud
f765794c99 Don't add managed to no-process renamer list. fix #3538 2014-07-01 21:00:05 +02:00
Ruud
34320e617d Unrar extract time options. closes #2733 2014-06-30 22:47:41 +02:00
Ruud
169ddeef5d Allow custom unrar path. fix #3460 2014-06-30 22:38:18 +02:00
Ruud
33ad4c22c7 Try make unrar executable for user 2014-06-30 21:58:19 +02:00
Ruud
265f90fe69 Unrar cleanup 2014-06-30 21:36:32 +02:00
Ruud
099b72ed27 Allow 720p in lower qualities. fix #3539 2014-06-30 16:28:45 +02:00
Ruud
d20c0ee37e Remove Smackdown from defaults 2014-06-29 23:34:48 +02:00
Ruud
f6030a333a Don't remove pyc files when using desktop updater 2014-06-29 23:33:23 +02:00
Ruud
6833e78546 Set correct branch 2014-06-11 23:54:11 +02:00
Ruud
30c56f29d0 Merge branch 'refs/heads/develop' into tv 2014-06-11 23:53:11 +02:00
Dean Gardiner
7ed0c6f099 Fixed missing identifiers for 'thetvdb' 2014-04-06 16:54:10 +12:00
Ruud
af64961502 Episode searching 2014-04-05 21:16:20 +02:00
Ruud
342e61da48 Show searcher init 2014-04-05 20:37:44 +02:00
Ruud
8ce30f0aad Nested media index 2014-04-05 20:37:24 +02:00
Ruud
63b8e3ff1a Remove downloaders.js from clientscript 2014-04-05 16:39:13 +02:00
Ruud
91c3df7c46 Use correct super class 2014-04-05 12:48:10 +02:00
Ruud
ae3d9c0a0a Add wanted shows 2014-04-05 11:51:55 +02:00
Ruud
090eb6f14d Allow type option in listing 2014-04-05 11:50:12 +02:00
Ruud
44de06f518 Merge branch 'refs/heads/develop' into tv 2014-04-05 11:34:23 +02:00
Ruud
b23db7541d Use correct key to check success 2014-04-05 10:39:20 +02:00
Ruud
7410288781 Merge branch 'refs/heads/develop' into tv 2014-04-05 10:22:38 +02:00
Ruud
bb4252363d Merge branch 'refs/heads/develop' into tv 2014-03-22 09:52:32 +01:00
Ruud
0a0a1704be Add show updated 2014-03-22 09:52:07 +01:00
Ruud
b13b32952f Only add rating if available 2014-03-21 21:51:00 +01:00
Ruud
0978ac33bc Merge branch 'refs/heads/develop' into tv 2014-03-21 21:40:55 +01:00
Ruud
6e8b7d25e5 Don't try to parse episodes if they aren't in the data 2014-03-21 21:40:10 +01:00
Ruud
0f555dbb85 Merge branch 'refs/heads/develop' into tv 2014-03-21 19:50:32 +01:00
Ruud
43e4ed6e2d Merge branch 'refs/heads/develop' into tv 2014-03-19 18:15:41 +01:00
Ruud
2e50eb487c Cleanup 2014-03-19 08:18:37 +01:00
Ruud
70e5f1a6d8 Merge branch 'refs/heads/develop' into tv 2014-03-19 08:09:48 +01:00
Ruud
9cfa7fa2a3 Cleanup 2014-03-18 22:28:22 +01:00
Ruud
cfc9f524a7 Cleanup 2014-03-17 22:11:55 +01:00
Ruud
8281fdc08b Show cleanup 2014-03-16 20:32:42 +01:00
Ruud
949f76cd50 Merge branch 'refs/heads/nosql' into tv 2014-03-16 18:59:50 +01:00
Ruud
9631be1ee4 Move tv branch to nosql 2014-03-15 11:47:49 +01:00
319 changed files with 17905 additions and 15879 deletions

View File

@@ -10,7 +10,6 @@ import socket
import subprocess
import sys
import traceback
import time
# Root path
base_path = dirname(os.path.abspath(__file__))

View File

@@ -1,235 +0,0 @@
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

@@ -29,19 +29,25 @@ OS X:
* Then do `python CouchPotatoServer/CouchPotato.py`
* Your browser should open up, but if it doesn't go to `http://localhost:5050/`
Linux (Ubuntu / Debian):
Linux:
* Install [GIT](http://git-scm.com/) with `apt-get install git-core`
* (Ubuntu / Debian) Install [GIT](http://git-scm.com/) with `apt-get install git-core`
* (Fedora / CentOS) Install [GIT](http://git-scm.com/) with `yum install git`
* '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`
* 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`
* (Ubuntu / Debian) To run on boot copy the init script `sudo cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato`
* (Ubuntu / Debian) Copy the default paths file `sudo cp CouchPotatoServer/init/ubuntu.default /etc/default/couchpotato`
* (Ubuntu / Debian) Change the paths inside the default file `sudo nano /etc/default/couchpotato`
* (Ubuntu / Debian) Make it executable `sudo chmod +x /etc/init.d/couchpotato`
* (Ubuntu / Debian) Add it to defaults `sudo update-rc.d couchpotato defaults`
* (systemd) To run on boot copy the systemd config `sudo cp CouchPotatoServer/init/couchpotato.fedora.service /etc/systemd/system/couchpotato.service`
* (systemd) Update the systemd config file with your user and path to CouchPotato.py
* (systemd) Enable it at boot with `sudo systemctl enable couchpotato`
* Open your browser and go to `http://localhost:5050/`
Docker:
* You can use [razorgirl's Dockerfile](https://github.com/razorgirl/docker-couchpotato) to quickly build your own isolated app container. It's based on the Linux instructions above. For more info about Docker check out the [official website](https://www.docker.com).
FreeBSD :

View File

@@ -13,6 +13,8 @@ Lastly, for anything related to CouchPotato, feel free to stop by the [forum](ht
## 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:
* **FILL IN ALL THE FIELDS ASKED FOR**
* **POST MORE THAN A SINGLE LINE LOG**, if you do, you'd better have a easy reproducable bug
* 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.
@@ -25,12 +27,14 @@ Before you submit an issue, please go through the following checklist:
* 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!
* Do not "bump" issues with "Any updates on this" or whatever. Yes I've seen it, you don't have to remind me of it. There will be an update when the code is done or I need information. If you feel the need to do so, you'd better have more info on the issue.
The more relevant information you provide, the more likely that your issue will be resolved.
If you don't follow any of the checks above, I'll close the issue. If you are wondering why (and ask) I'll block you from posting new issues and the repo.
## 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?
* Does your PR have any limitations we should know of?
* Does your PR have any limitations I should know of?
* Is your PR up-to-date with the branch you're trying to push into?

View File

@@ -143,6 +143,8 @@ class ApiHandler(RequestHandler):
else:
self.write(result)
self.finish()
except UnicodeDecodeError:
log.error('Failed proper encode: %s', traceback.format_exc())
except:
log.debug('Failed doing request, probably already closed: %s', (traceback.format_exc()))
try: self.finish({'success': False, 'error': 'Failed returning results'})

View File

@@ -181,13 +181,13 @@ class Core(Plugin):
return '%sapi/%s' % (self.createBaseUrl(), Env.setting('api_key'))
def version(self):
ver = fireEvent('updater.info', single = True)
ver = fireEvent('updater.info', single = True) or {'version': {}}
if os.name == 'nt': platf = 'windows'
elif 'Darwin' in platform.platform(): platf = 'osx'
else: platf = 'linux'
return '%s - %s-%s - v2' % (platf, ver.get('version')['type'], ver.get('version')['hash'])
return '%s - %s-%s - v2' % (platf, ver.get('version').get('type') or 'unknown', ver.get('version').get('hash') or 'unknown')
def versionView(self, **kwargs):
return {
@@ -286,13 +286,13 @@ config = [{
'name': 'permission_folder',
'default': '0755',
'label': 'Folder CHMOD',
'description': 'Can be either decimal (493) or octal (leading zero: 0755)',
'description': 'Can be either decimal (493) or octal (leading zero: 0755). <a target="_blank" href="http://permissions-calculator.org/">Calculate the correct value</a>',
},
{
'name': 'permission_file',
'default': '0755',
'default': '0644',
'label': 'File CHMOD',
'description': 'Same as Folder CHMOD but for files',
'description': 'See Folder CHMOD description, but for files',
},
],
},

View File

@@ -205,19 +205,28 @@ class GitUpdater(BaseUpdater):
def getVersion(self):
if not self.version:
hash = None
date = None
branch = self.branch
try:
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
}
hash = output.hash[:8]
date = output.getDate()
branch = self.repo.getCurrentBranch().name
except Exception as e:
log.error('Failed using GIT updater, running from source, you need to have GIT installed. %s', e)
return 'No GIT'
self.version = {
'repr': 'git:(%s:%s % s) %s (%s)' % (self.repo_user, self.repo_name, branch, hash or 'unknown_hash', datetime.fromtimestamp(date) if date else 'unknown_date'),
'hash': hash,
'date': date,
'type': 'git',
'branch': branch
}
return self.version

View File

@@ -2,6 +2,7 @@ import json
import os
import time
import traceback
from sqlite3 import OperationalError
from CodernityDB.database import RecordNotFound
from CodernityDB.index import IndexException, IndexNotFoundException, IndexConflict
@@ -9,7 +10,7 @@ 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
from couchpotato.core.helpers.variable import getImdb, tryInt, randomString
log = CPLog(__name__)
@@ -32,6 +33,7 @@ class Database(object):
addEvent('database.setup.after', self.startup_compact)
addEvent('database.setup_index', self.setupIndex)
addEvent('database.delete_corrupted', self.deleteCorrupted)
addEvent('app.migrate', self.migrate)
addEvent('app.after_shutdown', self.close)
@@ -147,6 +149,17 @@ class Database(object):
return results
def deleteCorrupted(self, _id, traceback_error = ''):
db = self.getDB()
try:
log.debug('Deleted corrupted document "%s": %s', (_id, traceback_error))
corrupted = db.get('id', _id, with_storage = False)
db._delete_id_index(corrupted.get('_id'), corrupted.get('_rev'), None)
except:
log.debug('Failed deleting corrupted: %s', traceback.format_exc())
def reindex(self, **kwargs):
success = True
@@ -299,309 +312,328 @@ class Database(object):
}
migrate_data = {}
rename_old = False
c = conn.cursor()
try:
for ml in migrate_list:
migrate_data[ml] = {}
rows = migrate_list[ml]
c = conn.cursor()
try:
c.execute('SELECT %s FROM `%s`' % ('`' + '`,`'.join(rows) + '`', ml))
except:
# ignore faulty destination_id database
if ml == 'category':
migrate_data[ml] = {}
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:
rename_old = True
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:
raise
new_profiles_by_label[x['doc']['label']] = x['_id']
for p in c.fetchall():
columns = {}
for row in migrate_list[ml]:
columns[row] = p[rows.index(row)]
profiles = migrate_data['profile']
profile_link = {}
for x in profiles:
p = profiles[x]
if not migrate_data[ml].get(p[0]):
migrate_data[ml][p[0]] = columns
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:
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()
new_profile = {
'_t': 'profile',
'label': p.get('label'),
'order': int(p.get('order', 999)),
'core': p.get('core', False),
'qualities': [],
'wait_for': [],
'finish': []
}
log.info('Getting data took %s', time.time() - migrate_start)
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'])
db = self.getDB()
if not db.opened:
return
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'))
# 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'))
# 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']
# Categories
categories = migrate_data.get('category', [])
log.info('Importing %s categories', len(categories))
category_link = {}
for x in categories:
c = categories[x]
qualities = migrate_data['quality']
quality_link = {}
for x in qualities:
q = qualities[x]
q_id = new_qualities_by_identifier[q.get('identifier')]
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', '')),
})
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)
category_link[x] = new_c.get('_id')
quality_link[x] = quality
# Profiles
log.info('Importing profiles')
new_profiles = db.all('profile', with_doc = True)
new_profiles_by_label = {}
for x in new_profiles:
# 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')
# 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']
# Releases
releaseinfos = migrate_data['releaseinfo']
for x in releaseinfos:
info = releaseinfos[x]
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:
# Skip if release doesn't exist for this info
if not migrate_data['release'].get(info.get('release_id')):
continue
release_status = statuses.get(rel.get('status_id')).get('identifier')
if not migrate_data['release'][info.get('release_id')].get('info'):
migrate_data['release'][info.get('release_id')]['info'] = {}
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,
}
migrate_data['release'][info.get('release_id')]['info'][info.get('identifier')] = info.get('value')
# 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': {}
}
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')] = []
# Add downloader info if provided
try:
release['download_info'] = rel['info']['download_info']
del rel['download_info']
except:
pass
releases_by_media[release.get('movie_id')].append(release)
# Add files
release_files = releases_files.get(rel.get('id'), [])
if not isinstance(release_files, list):
release_files = [release_files]
# Type ids
types = migrate_data['filetype']
type_by_id = {}
for t in types:
type = types[t]
type_by_id[type.get('id')] = type
if len(release_files) == 0:
# 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
for f in release_files:
rfile = all_files[f.get('file_id')]
file_type = type_by_id.get(rfile.get('type_id')).get('identifier')
release_status = statuses.get(rel.get('status_id')).get('identifier')
if not release['files'].get(file_type):
release['files'][file_type] = []
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,
}
release['files'][file_type].append(rfile.get('path'))
# 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': {}
}
try:
rls = db.get('release_identifier', rel.get('identifier'), with_doc = True)['doc']
rls.update(release)
db.update(rls)
except:
db.insert(release)
# 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.get(f.get('file_id'))
if not rfile:
continue
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 = True
except OperationalError:
log.error('Migrating from faulty database, probably a (too) old version: %s', traceback.format_exc())
rename_old = True
except:
log.error('Migration failed: %s', traceback.format_exc())
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 rename_old:
random = randomString()
log.info('Renaming old database to %s ', '%s.%s_old' % (old_db, random))
os.rename(old_db, '%s.%s_old' % (old_db, random))
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')
if os.path.isfile(old_db + '-wal'):
os.rename(old_db + '-wal', '%s-wal.%s_old' % (old_db, random))
if os.path.isfile(old_db + '-shm'):
os.rename(old_db + '-shm', '%s-shm.%s_old' % (old_db, random))

View File

@@ -27,6 +27,11 @@ class Deluge(DownloaderBase):
def connect(self, reconnect = False):
# Load host from config and split out port.
host = cleanHost(self.conf('host'), protocol = False).split(':')
# Force host assignment
if len(host) == 1:
host.append(80)
if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.')
return False

View File

@@ -1,16 +1,10 @@
from base64 import b64encode
from urllib2 import URLError
import os
from uuid import uuid4
import hashlib
import httplib
import json
import os
import socket
import ssl
import sys
import time
import traceback
import urllib2
from requests import HTTPError
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
from couchpotato.core.helpers.encoding import tryUrlencode, sp
@@ -35,13 +29,17 @@ class NZBVortex(DownloaderBase):
# Send the nzb
try:
nzb_filename = self.createFileName(data, filedata, media)
self.call('nzb/add', files = {'file': (nzb_filename, filedata)})
nzb_filename = self.createFileName(data, filedata, media, unique_tag = True)
response = self.call('nzb/add', files = {'file': (nzb_filename, filedata, 'application/octet-stream')}, parameters = {
'name': nzb_filename,
'groupname': self.conf('group')
})
time.sleep(10)
raw_statuses = self.call('nzb')
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)
if response and response.get('result', '').lower() == 'ok':
return self.downloadReturnId(nzb_filename)
log.error('Something went wrong sending the NZB file. Response: %s', response)
return False
except:
log.error('Something went wrong sending the NZB file: %s', traceback.format_exc())
return False
@@ -60,7 +58,8 @@ class NZBVortex(DownloaderBase):
release_downloads = ReleaseDownloadList(self)
for nzb in raw_statuses.get('nzbs', []):
if nzb['id'] in ids:
nzb_id = os.path.basename(nzb['nzbFileName'])
if nzb_id in ids:
# Check status
status = 'busy'
@@ -70,7 +69,8 @@ class NZBVortex(DownloaderBase):
status = 'failed'
release_downloads.append({
'id': nzb['id'],
'temp_id': nzb['id'],
'id': nzb_id,
'name': nzb['uiTitle'],
'status': status,
'original_status': nzb['state'],
@@ -85,7 +85,7 @@ class NZBVortex(DownloaderBase):
log.info('%s failed downloading, deleting...', release_download['name'])
try:
self.call('nzb/%s/cancel' % release_download['id'])
self.call('nzb/%s/cancel' % release_download['temp_id'])
except:
log.error('Failed deleting: %s', traceback.format_exc(0))
return False
@@ -114,7 +114,7 @@ class NZBVortex(DownloaderBase):
log.error('Login failed, please check you api-key')
return False
def call(self, call, parameters = None, repeat = False, auth = True, *args, **kwargs):
def call(self, call, parameters = None, is_repeat = False, auth = True, *args, **kwargs):
# Login first
if not parameters: parameters = {}
@@ -127,19 +127,20 @@ class NZBVortex(DownloaderBase):
params = tryUrlencode(parameters)
url = cleanHost(self.conf('host'), ssl = self.conf('ssl')) + 'api/' + call
url = cleanHost(self.conf('host')) + 'api/' + call
try:
data = self.urlopen('%s?%s' % (url, params), *args, **kwargs)
data = self.getJsonData('%s%s' % (url, '?' + params if params else ''), *args, cache_timeout = 0, show_error = False, **kwargs)
if data:
return json.loads(data)
except URLError as e:
if hasattr(e, 'code') and e.code == 403:
return data
except HTTPError as e:
sc = e.response.status_code
if sc == 403:
# Try login and do again
if not repeat:
if not is_repeat:
self.login()
return self.call(call, parameters = parameters, repeat = True, **kwargs)
return self.call(call, parameters = parameters, is_repeat = True, **kwargs)
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
except:
@@ -151,13 +152,12 @@ class NZBVortex(DownloaderBase):
if not self.api_level:
url = cleanHost(self.conf('host')) + 'api/app/apilevel'
try:
data = self.urlopen(url, show_error = False)
self.api_level = float(json.loads(data).get('apilevel'))
except URLError as e:
if hasattr(e, 'code') and e.code == 403:
data = self.call('app/apilevel', auth = False)
self.api_level = float(data.get('apilevel'))
except HTTPError as e:
sc = e.response.status_code
if sc == 403:
log.error('This version of NZBVortex isn\'t supported. Please update to 2.8.6 or higher')
else:
log.error('NZBVortex doesn\'t seem to be running or maybe the remote option isn\'t enabled yet: %s', traceback.format_exc(1))
@@ -169,29 +169,6 @@ class NZBVortex(DownloaderBase):
return super(NZBVortex, self).isEnabled(manual, data) and self.getApiLevel()
class HTTPSConnection(httplib.HTTPSConnection):
def __init__(self, *args, **kwargs):
httplib.HTTPSConnection.__init__(self, *args, **kwargs)
def connect(self):
sock = socket.create_connection((self.host, self.port), self.timeout)
if sys.version_info < (2, 6, 7):
if hasattr(self, '_tunnel_host'):
self.sock = sock
self._tunnel()
else:
if self._tunnel_host:
self.sock = sock
self._tunnel()
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version = ssl.PROTOCOL_TLSv1)
class HTTPSHandler(urllib2.HTTPSHandler):
def https_open(self, req):
return self.do_open(HTTPSConnection, req)
config = [{
'name': 'nzbvortex',
'groups': [
@@ -211,20 +188,18 @@ config = [{
},
{
'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>',
'default': 'https://localhost:4321',
'description': 'Hostname with port. Usually <strong>https://localhost:4321</strong>',
},
{
'name': 'api_key',
'label': 'Api Key',
},
{
'name': 'group',
'label': 'Group',
'description': 'The group CP places the nzb in. Make sure to create it in NZBVortex.',
},
{
'name': 'manual',
'default': False,

View File

@@ -23,16 +23,14 @@ class Transmission(DownloaderBase):
log = CPLog(__name__)
trpc = None
def connect(self, reconnect = False):
def connect(self):
# Load host from config and split out port.
host = cleanHost(self.conf('host'), protocol = False).split(':')
host = cleanHost(self.conf('host')).rstrip('/').rsplit(':', 1)
if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.')
return False
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'))
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
def download(self, data = None, media = None, filedata = None):
@@ -80,15 +78,17 @@ class Transmission(DownloaderBase):
log.error('Failed sending torrent to Transmission')
return False
data = remote_torrent.get('torrent-added') or remote_torrent.get('torrent-duplicate')
# Change settings of added torrents
if torrent_params:
self.trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
self.trpc.set_torrent(data['hashString'], torrent_params)
log.info('Torrent sent to Transmission successfully.')
return self.downloadReturnId(remote_torrent['torrent-added']['hashString'])
return self.downloadReturnId(data['hashString'])
def test(self):
if self.connect(True) and self.trpc.get_session():
if self.connect() and self.trpc.get_session():
return True
return False
@@ -164,11 +164,11 @@ class Transmission(DownloaderBase):
class TransmissionRPC(object):
"""TransmissionRPC lite library"""
def __init__(self, host = 'localhost', port = 9091, rpc_url = 'transmission', username = None, password = None):
def __init__(self, host = 'http://localhost', port = 9091, rpc_url = 'transmission', username = None, password = None):
super(TransmissionRPC, self).__init__()
self.url = 'http://' + host + ':' + str(port) + '/' + rpc_url + '/rpc'
self.url = host + ':' + str(port) + '/' + rpc_url + '/rpc'
self.tag = 0
self.session_id = 0
self.session = {}
@@ -276,8 +276,8 @@ config = [{
},
{
'name': 'host',
'default': 'localhost:9091',
'description': 'Hostname with port. Usually <strong>localhost:9091</strong>',
'default': 'http://localhost:9091',
'description': 'Hostname with port. Usually <strong>http://localhost:9091</strong>',
},
{
'name': 'rpc_url',

View File

@@ -90,7 +90,7 @@ def fireEvent(name, *args, **kwargs):
else:
e = Event(name = name, threads = 10, exc_info = True, traceback = True, lock = threading.RLock())
e = Event(name = name, threads = 10, exc_info = True, traceback = True)
for event in events[name]:
e.handle(event['handler'], priority = event['priority'])

View File

@@ -5,6 +5,7 @@ import re
import traceback
import unicodedata
from chardet import detect
from couchpotato.core.logger import CPLog
import six
@@ -35,6 +36,9 @@ def toUnicode(original, *args):
return six.text_type(original, *args)
except:
try:
detected = detect(original)
if detected.get('encoding') == 'utf-8':
return original.decode('utf-8')
return ek(original, *args)
except:
raise
@@ -52,7 +56,10 @@ def ss(original, *args):
return u_original.encode(Env.get('encoding'))
except Exception as e:
log.debug('Failed ss encoding char, force UTF8: %s', e)
return u_original.encode('UTF-8')
try:
return u_original.encode(Env.get('encoding'), 'replace')
except:
return u_original.encode('utf-8', 'replace')
def sp(path, *args):

34
couchpotato/core/helpers/variable.py Normal file → Executable file
View File

@@ -41,11 +41,11 @@ def symlink(src, dst):
def getUserDir():
try:
import pwd
os.environ['HOME'] = pwd.getpwuid(os.geteuid()).pw_dir
os.environ['HOME'] = sp(pwd.getpwuid(os.geteuid()).pw_dir)
except:
pass
return os.path.expanduser('~')
return sp(os.path.expanduser('~'))
def getDownloadDir():
@@ -380,3 +380,33 @@ def getFreeSpace(directories):
free_space[folder] = size
return free_space
def getSize(paths):
single = not isinstance(paths, (tuple, list))
if single:
paths = [paths]
total_size = 0
for path in paths:
path = sp(path)
if os.path.isdir(path):
total_size = 0
for dirpath, _, filenames in os.walk(path):
for f in filenames:
total_size += os.path.getsize(sp(os.path.join(dirpath, f)))
elif os.path.isfile(path):
total_size += os.path.getsize(path)
return total_size / 1048576 # MB
def find(func, iterable):
for item in iterable:
if func(item):
return item
return None

View File

@@ -59,15 +59,14 @@ class CPLog(object):
msg = ss(msg)
try:
msg = msg % replace_tuple
except:
try:
if isinstance(replace_tuple, tuple):
msg = msg % tuple([ss(x) for x in list(replace_tuple)])
else:
msg = msg % ss(replace_tuple)
except Exception as e:
self.logger.error('Failed encoding stuff to log "%s": %s' % (msg, e))
if isinstance(replace_tuple, tuple):
msg = msg % tuple([ss(x) if not isinstance(x, (int, float)) else x for x in list(replace_tuple)])
elif isinstance(replace_tuple, dict):
msg = msg % dict((k, ss(v) if not isinstance(v, (int, float)) else v) for k, v in replace_tuple.iteritems())
else:
msg = msg % ss(replace_tuple)
except Exception as e:
self.logger.error('Failed encoding stuff to log "%s": %s' % (msg, e))
self.setup()
if not self.is_develop:

23
couchpotato/core/media/__init__.py Normal file → Executable file
View File

@@ -26,9 +26,9 @@ class MediaBase(Plugin):
def onComplete():
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)
if media:
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())
@@ -39,9 +39,9 @@ class MediaBase(Plugin):
def notifyFront():
try:
media = fireEvent('media.get', media_id, single = True)
event_name = '%s.update' % media.get('type')
fireEvent('notify.frontend', type = event_name, data = media)
if media:
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())
@@ -65,10 +65,13 @@ class MediaBase(Plugin):
return def_title or 'UNKNOWN'
def getPoster(self, image_urls, existing_files):
image_type = 'poster'
def getPoster(self, media, image_urls):
if 'files' not in media:
media['files'] = {}
# Remove non-existing files
existing_files = media['files']
image_type = 'poster'
file_type = 'image_%s' % image_type
# Make existing unique
@@ -92,7 +95,7 @@ class MediaBase(Plugin):
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]
existing_files[file_type] = [toUnicode(file_path)]
break
else:
break

110
couchpotato/core/media/_base/library/main.py Normal file → Executable file
View File

@@ -1,10 +1,47 @@
from couchpotato import get_db
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.library.base import LibraryBase
log = CPLog(__name__)
class Library(LibraryBase):
def __init__(self):
addEvent('library.title', self.title)
addEvent('library.related', self.related)
addEvent('library.tree', self.tree)
addEvent('library.root', self.root)
addApiView('library.query', self.queryView)
addApiView('library.related', self.relatedView)
addApiView('library.tree', self.treeView)
def queryView(self, media_id, **kwargs):
db = get_db()
media = db.get('id', media_id)
return {
'result': fireEvent('library.query', media, single = True)
}
def relatedView(self, media_id, **kwargs):
db = get_db()
media = db.get('id', media_id)
return {
'result': fireEvent('library.related', media, single = True)
}
def treeView(self, media_id, **kwargs):
db = get_db()
media = db.get('id', media_id)
return {
'result': fireEvent('library.tree', media, single = True)
}
def title(self, library):
return fireEvent(
@@ -16,3 +53,76 @@ class Library(LibraryBase):
include_identifier = False,
single = True
)
def related(self, media):
result = {self.key(media['type']): media}
db = get_db()
cur = media
while cur and cur.get('parent_id'):
cur = db.get('id', cur['parent_id'])
result[self.key(cur['type'])] = cur
children = db.get_many('media_children', media['_id'], with_doc = True)
for item in children:
key = self.key(item['doc']['type']) + 's'
if key not in result:
result[key] = []
result[key].append(item['doc'])
return result
def root(self, media):
db = get_db()
cur = media
while cur and cur.get('parent_id'):
cur = db.get('id', cur['parent_id'])
return cur
def tree(self, media = None, media_id = None):
db = get_db()
if media:
result = media
elif media_id:
result = db.get('id', media_id, with_doc = True)
else:
return None
# Find children
items = db.get_many('media_children', result['_id'], with_doc = True)
keys = []
# Build children arrays
for item in items:
key = self.key(item['doc']['type']) + 's'
if key not in result:
result[key] = {}
elif type(result[key]) is not dict:
result[key] = {}
if key not in keys:
keys.append(key)
result[key][item['_id']] = fireEvent('library.tree', item['doc'], single = True)
# Unique children
for key in keys:
result[key] = result[key].values()
# Include releases
result['releases'] = fireEvent('release.for_media', result['_id'], single = True)
return result
def key(self, media_type):
parts = media_type.split('.')
return parts[-1]

View File

@@ -40,7 +40,7 @@ class Matcher(MatcherBase):
return False
def correctTitle(self, chain, media):
root_library = media['library']['root_library']
root = fireEvent('library.root', media, single = True)
if 'show_name' not in chain.info or not len(chain.info['show_name']):
log.info('Wrong: missing show name in parsed result')
@@ -50,10 +50,10 @@ class Matcher(MatcherBase):
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']
titles = root['info']['titles']
# Add year suffix titles (will result in ['<name_one>', '<name_one> <suffix_one>', '<name_two>', ...])
suffixes = [None, root_library['info']['year']]
suffixes = [None, root['info']['year']]
titles = [
title + ((' %s' % suffix) if suffix else '')

156
couchpotato/core/media/_base/media/main.py Normal file → Executable file
View File

@@ -1,10 +1,9 @@
from datetime import timedelta
from operator import itemgetter
import time
import traceback
from string import ascii_lowercase
from CodernityDB.database import RecordNotFound
from CodernityDB.database import RecordNotFound, RecordDeleted
from couchpotato import tryInt, get_db
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
@@ -44,15 +43,15 @@ class MediaPlugin(MediaBase):
'desc': 'List media',
'params': {
'type': {'type': 'string', 'desc': 'Media type to filter on.'},
'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'},
'release_status': {'type': 'array or csv', 'desc': 'Filter movie by status of its releases. Example:"snatched,available"'},
'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'},
'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'},
'search': {'desc': 'Search movie title'},
'status': {'type': 'array or csv', 'desc': 'Filter media by status. Example:"active,done"'},
'release_status': {'type': 'array or csv', 'desc': 'Filter media by status of its releases. Example:"snatched,available"'},
'limit_offset': {'desc': 'Limit and offset the media list. Examples: "50" or "50,30"'},
'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all media starting with the letter "a"'},
'search': {'desc': 'Search media title'},
},
'return': {'type': 'object', 'example': """{
'success': True,
'empty': bool, any movies returned or not,
'empty': bool, any media returned or not,
'media': array, media found,
}"""}
})
@@ -78,6 +77,7 @@ class MediaPlugin(MediaBase):
addEvent('app.load', self.addSingleListView, priority = 100)
addEvent('app.load', self.addSingleCharView, priority = 100)
addEvent('app.load', self.addSingleDeleteView, priority = 100)
addEvent('app.load', self.cleanupFaults)
addEvent('media.get', self.get)
addEvent('media.with_status', self.withStatus)
@@ -88,6 +88,18 @@ class MediaPlugin(MediaBase):
addEvent('media.tag', self.tag)
addEvent('media.untag', self.unTag)
# Wrongly tagged media files
def cleanupFaults(self):
medias = fireEvent('media.with_status', 'ignored', single = True) or []
db = get_db()
for media in medias:
try:
media['status'] = 'done'
db.update(media)
except:
pass
def refresh(self, id = '', **kwargs):
handlers = []
ids = splitString(id)
@@ -109,7 +121,7 @@ class MediaPlugin(MediaBase):
try:
media = get_db().get('id', media_id)
event = '%s.update_info' % media.get('type')
event = '%s.update' % media.get('type')
def handler():
fireEvent(event, media_id = media_id, on_complete = self.createOnComplete(media_id))
@@ -146,7 +158,7 @@ class MediaPlugin(MediaBase):
return media
except RecordNotFound:
except (RecordNotFound, RecordDeleted):
log.error('Media with id "%s" not found', media_id)
except:
raise
@@ -160,10 +172,13 @@ class MediaPlugin(MediaBase):
'media': media,
}
def withStatus(self, status, with_doc = True):
def withStatus(self, status, types = None, with_doc = True):
db = get_db()
if types and not isinstance(types, (list, tuple)):
types = [types]
status = list(status if isinstance(status, (list, tuple)) else [status])
for s in status:
@@ -171,24 +186,29 @@ class MediaPlugin(MediaBase):
if with_doc:
try:
doc = db.get('id', ms['_id'])
if types and doc.get('type') not in types:
continue
yield doc
except RecordNotFound:
except (RecordDeleted, RecordNotFound):
log.debug('Record not found, skipping: %s', ms['_id'])
except (ValueError, EOFError):
fireEvent('database.delete_corrupted', ms.get('_id'), traceback_error = traceback.format_exc(0))
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
return db.get('media', '%s-%s' % (x, identifiers[x]), with_doc = with_doc)
except:
pass
log.debug('No media found with identifiers: %s', identifiers)
return False
def list(self, types = None, status = None, release_status = None, status_or = False, limit_offset = None, with_tags = None, starts_with = None, search = None):
@@ -253,10 +273,6 @@ class MediaPlugin(MediaBase):
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, []
offset = 0
limit = -1
if limit_offset:
@@ -275,6 +291,10 @@ class MediaPlugin(MediaBase):
media = fireEvent('media.get', media_id, single = True)
# Skip if no media has been found
if not media:
continue
# Merge releases with movie dict
medias.append(media)
@@ -282,11 +302,30 @@ class MediaPlugin(MediaBase):
media_ids.remove(media_id)
if len(media_ids) == 0 or len(medias) == limit: break
return total_count, medias
# Sort media by type and return result
result = {}
# Create keys for media types we are listing
if types:
for media_type in types:
result['%ss' % media_type] = []
else:
for media_type in fireEvent('media.types', merge = True):
result['%ss' % media_type] = []
total_count = len(medias)
if total_count == 0:
return 0, result
for kind in medias:
result['%ss' % kind['type']].append(kind)
return total_count, result
def listView(self, **kwargs):
total_movies, movies = self.list(
total_count, result = self.list(
types = splitString(kwargs.get('type')),
status = splitString(kwargs.get('status')),
release_status = splitString(kwargs.get('release_status')),
@@ -297,19 +336,32 @@ class MediaPlugin(MediaBase):
search = kwargs.get('search')
)
return {
'success': True,
'empty': len(movies) == 0,
'total': total_movies,
'movies': movies,
}
results = result
results['success'] = True
results['empty'] = len(result) == 0
results['total'] = total_count
return results
def addSingleListView(self):
for media_type in fireEvent('media.types', merge = True):
def tempList(*args, **kwargs):
return self.listView(types = media_type, **kwargs)
addApiView('%s.list' % media_type, tempList)
tempList = lambda *args, **kwargs : self.listView(type = media_type, **kwargs)
addApiView('%s.list' % media_type, tempList, docs = {
'desc': 'List media',
'params': {
'status': {'type': 'array or csv', 'desc': 'Filter ' + media_type + ' by status. Example:"active,done"'},
'release_status': {'type': 'array or csv', 'desc': 'Filter ' + media_type + ' by status of its releases. Example:"snatched,available"'},
'limit_offset': {'desc': 'Limit and offset the ' + media_type + ' list. Examples: "50" or "50,30"'},
'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all ' + media_type + 's starting with the letter "a"'},
'search': {'desc': 'Search ' + media_type + ' title'},
},
'return': {'type': 'object', 'example': """{
'success': True,
'empty': bool, any """ + media_type + """s returned or not,
'media': array, media found,
}"""}
})
def availableChars(self, types = None, status = None, release_status = None):
@@ -355,7 +407,7 @@ class MediaPlugin(MediaBase):
if x['_id'] in media_ids:
chars.add(x['key'])
if len(chars) == 25:
if len(chars) == 27:
break
return list(chars)
@@ -376,8 +428,7 @@ class MediaPlugin(MediaBase):
def addSingleCharView(self):
for media_type in fireEvent('media.types', merge = True):
def tempChar(*args, **kwargs):
return self.charView(types = media_type, **kwargs)
tempChar = lambda *args, **kwargs : self.charView(type = media_type, **kwargs)
addApiView('%s.available_chars' % media_type, tempChar)
def delete(self, media_id, delete_from = None):
@@ -415,11 +466,16 @@ class MediaPlugin(MediaBase):
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'):
if (total_releases == total_deleted) 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
# Remove profile (no use for in manage)
if new_media_status == 'done':
media['profile_id'] = None
db.update(media)
fireEvent('media.untag', media['_id'], 'recent', single = True)
@@ -446,11 +502,16 @@ class MediaPlugin(MediaBase):
def addSingleDeleteView(self):
for media_type in fireEvent('media.types', merge = True):
def tempDelete(*args, **kwargs):
return self.deleteView(types = media_type, *args, **kwargs)
addApiView('%s.delete' % media_type, tempDelete)
tempDelete = lambda *args, **kwargs : self.deleteView(type = media_type, **kwargs)
addApiView('%s.delete' % media_type, tempDelete, docs = {
'desc': 'Delete a ' + media_type + ' from the wanted list',
'params': {
'id': {'desc': 'Media ID(s) you want to delete.', 'type': 'int (comma separated)'},
'delete_from': {'desc': 'Delete ' + media_type + ' from this page', 'type': 'string: all (default), wanted, manage'},
}
})
def restatus(self, media_id):
def restatus(self, media_id, tag_recent = True, allowed_restatus = None):
try:
db = get_db()
@@ -470,12 +531,13 @@ class MediaPlugin(MediaBase):
done_releases = [release for release in media_releases if release.get('status') == 'done']
if done_releases:
# Only look at latest added release
release = sorted(done_releases, key = itemgetter('last_edit'), reverse = True)[0]
# 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'
for release in done_releases:
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'
break
elif previous_status == 'done':
m['status'] = 'done'
@@ -484,22 +546,26 @@ class MediaPlugin(MediaBase):
m['status'] = previous_status
# Only update when status has changed
if previous_status != m['status']:
if previous_status != m['status'] and (not allowed_restatus or m['status'] in allowed_restatus):
db.update(m)
# Tag media as recent
self.tag(media_id, 'recent')
if tag_recent:
self.tag(media_id, 'recent', update_edited = True)
return m['status']
except:
log.error('Failed restatus: %s', traceback.format_exc())
def tag(self, media_id, tag):
def tag(self, media_id, tag, update_edited = False):
try:
db = get_db()
m = db.get('id', media_id)
if update_edited:
m['last_edit'] = int(time.time())
tags = m.get('tags') or []
if tag not in tags:
tags.append(tag)

View File

@@ -45,7 +45,7 @@ class Base(NZBProvider, RSS):
def _searchOnHost(self, host, media, quality, results):
query = self.buildUrl(media, host)
url = '%s&%s' % (self.getUrl(host['host']), query)
url = '%s%s' % (self.getUrl(host['host']), query)
nzbs = self.getRSSData(url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()})
for nzb in nzbs:
@@ -83,7 +83,7 @@ class Base(NZBProvider, RSS):
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)
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')
@@ -187,11 +187,12 @@ class Base(NZBProvider, RSS):
self.limits_reached[host] = False
return data
except HTTPError as e:
if e.code == 503:
sc = e.response.status_code
if sc in [503, 429]:
response = e.read().lower()
if 'maximum api' in response or 'download limit' in response:
if sc == 429 or '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)
log.error('Limit reached / to many requests for newznab provider: %s', host)
self.limits_reached[host] = time.time()
return 'try_next'
@@ -220,7 +221,7 @@ config = [{
'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>',
<a href="https://www.nzbfinder.ws" target="_blank">NZBFinder</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAACVBMVEVjhwD///86aRovd/sBAAAAMklEQVQI12NgAIPQUCCRmQkjssDEShiRuRIqwZqZGcDAGBrqANUhGgIkWAOABKMDxCAA24UK50b26SAAAAAASUVORK5CYII=',
'options': [
@@ -231,30 +232,30 @@ config = [{
},
{
'name': 'use',
'default': '0,0,0,0,0,0'
'default': '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',
'default': 'api.nzb.su,api.dognzb.cr,nzbs.org,https://api.nzbgeek.info,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',
'default': '0,0,0,0,0',
'description': 'Starting score for each release found via this provider.',
},
{
'name': 'custom_tag',
'advanced': True,
'label': 'Custom tag',
'default': ',,,,,',
'default': ',,,,',
'description': 'Add custom tags, for example add rls=1 to get only scene releases from nzbs.org',
},
{
'name': 'api_key',
'default': ',,,,,',
'default': ',,,,',
'label': 'Api Key',
'description': 'Can be found on your profile page',
'type': 'combined',

View File

@@ -1,126 +0,0 @@
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

@@ -61,7 +61,7 @@ class Base(TorrentProvider):
'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()),
'size': tryInt(entry.find('size').get_text()) / 1048576,
'seeders': tryInt(entry.find('seeders').get_text()),
'leechers': tryInt(entry.find('leechers').get_text()),
'score': torrentscore

View File

@@ -22,6 +22,9 @@ class Base(TorrentProvider):
http_time_between_calls = 1 # Seconds
only_tables_tags = SoupStrainer('table')
torrent_name_cell = 1
torrent_download_cell = 2
def _searchOnTitle(self, title, movie, quality, results):
url = self.urls['search'] % self.buildUrl(title, movie, quality)
@@ -40,8 +43,8 @@ class Base(TorrentProvider):
all_cells = result.find_all('td')
torrent = all_cells[1].find('a')
download = all_cells[3].find('a')
torrent = all_cells[self.torrent_name_cell].find('a')
download = all_cells[self.torrent_download_cell].find('a')
torrent_id = torrent['href']
torrent_id = torrent_id.replace('details.php?id=', '')
@@ -49,9 +52,9 @@ class Base(TorrentProvider):
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_size = self.parseSize(all_cells[8].getText())
torrent_seeders = tryInt(all_cells[10].getText())
torrent_leechers = tryInt(all_cells[11].getText())
torrent_url = self.urls['baseurl'] % download['href']
torrent_detail_url = self.urls['baseurl'] % torrent['href']

View File

@@ -34,8 +34,7 @@ class Base(TorrentMagnetProvider):
'http://kickass.pw',
'http://kickassto.come.in',
'http://katproxy.ws',
'http://www.kickassunblock.info',
'http://www.kickassproxy.info',
'http://kickass.bitproxy.eu',
'http://katph.eu',
'http://kickassto.come.in',
]

View File

@@ -64,6 +64,10 @@ class Base(TorrentProvider):
torrentdesc += ' HQ'
if self.conf('prefer_golden'):
torrentscore += 5000
if 'FreeleechType' in torrent:
torrentdesc += ' Freeleech'
if self.conf('prefer_freeleech'):
torrentscore += 7000
if 'Scene' in torrent and torrent['Scene']:
torrentdesc += ' Scene'
if self.conf('prefer_scene'):
@@ -223,6 +227,14 @@ config = [{
'default': 1,
'description': 'Favors Golden Popcorn-releases over all other releases.'
},
{
'name': 'prefer_freeleech',
'advanced': True,
'type': 'bool',
'label': 'Prefer Freeleech',
'default': 1,
'description': 'Favors torrents marked as freeleech over all other releases.'
},
{
'name': 'prefer_scene',
'advanced': True,

View File

@@ -24,16 +24,16 @@ class Base(TorrentMagnetProvider):
http_time_between_calls = 0
proxy_list = [
'https://nobay.net',
'https://dieroschtibay.org',
'https://thebay.al',
'https://thepiratebay.se',
'http://thepiratebay.cd',
'http://thepiratebay.se.net',
'http://thebootlegbay.com',
'http://www.tpb.gr',
'http://tpbproxy.co.uk',
'http://tpb.ninja.so',
'http://proxybay.fr',
'http://pirateproxy.in',
'http://www.getpirate.com',
'http://piratebay.io',
'http://piratebay.skey.sk',
'http://pirateproxy.be',
'http://bayproxy.li',
'http://proxybay.pw',
]

View File

@@ -1,126 +0,0 @@
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

@@ -13,12 +13,12 @@ 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',
'test': 'http://torrentshack.eu/',
'login': 'http://torrentshack.eu/login.php',
'login_check': 'http://torrentshack.eu/inbox.php',
'detail': 'http://torrentshack.eu/torrent/%s',
'search': 'http://torrentshack.eu/torrents.php?action=advanced&searchstr=%s&scene=%s&filter_cat[%d]=1',
'download': 'http://torrentshack.eu/%s',
}
http_time_between_calls = 1 # Seconds
@@ -42,6 +42,7 @@ class Base(TorrentProvider):
link = result.find('span', attrs = {'class': 'torrent_name_link'}).parent
url = result.find('td', attrs = {'class': 'torrent_td'}).find('a')
tds = result.find_all('td')
results.append({
'id': link['href'].replace('torrents.php?torrentid=', ''),
@@ -49,8 +50,8 @@ class Base(TorrentProvider):
'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),
'seeders': tryInt(tds[len(tds)-2].string),
'leechers': tryInt(tds[len(tds)-1].string),
})
except:
@@ -80,7 +81,7 @@ config = [{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'TorrentShack',
'description': '<a href="https://www.torrentshack.net/">TorrentShack</a>',
'description': '<a href="http://torrentshack.eu/">TorrentShack</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABmElEQVQoFQXBzY2cVRiE0afqvd84CQiAnxWWtyxsS6ThINBYg2Dc7mZBMEjE4mzs6e9WcY5+ePNuVFJJodQAoLo+SaWCy9rcV8cmjah3CI6iYu7oRU30kE5xxELRfamklY3k1NL19sSm7vPzP/ZdNZzKVDaY2sPZJBh9fv5ITrmG2+Vp4e1sPchVqTCQZJnVXi+/L4uuAJGly1+Pw8CprLbi8Om7tbT19/XRqJUk11JP9uHj9ulxhXbvJbI9qJvr5YkGXFG2IBT8tXczt+sfzDZCp3765f3t9tHEHGEDACma77+8o4oATKk+/PfW9YmHruRFjWoVSFsVsGu1YSKq6Oc37+n98unPZSRlY7vsKDqN+92X3yR9+PdXee3iJNKMStqdcZqoTJbUSi5JOkpfRlhSI0mSpEmCFKoU7FqSNOLAk54uGwCStMUCgLrVic62g7oDoFmmdI+P3S0pDe1xvDqb6XrZqbtzShWNoh9fv/XQHaDdM9OqrZi2M7M3UrB2vlkPS1IbdEBk7UiSoD6VlZ6aKWer4aH4f/AvKoHUTjuyAAAAAElFTkSuQmCC',
'options': [

View File

@@ -73,4 +73,24 @@ config = [{
],
},
],
}, {
'name': 'torrent',
'groups': [
{
'tab': 'searcher',
'name': 'searcher',
'wizard': True,
'options': [
{
'name': 'minimum_seeders',
'advanced': True,
'label': 'Minimum seeders',
'description': 'Ignore torrents with seeders below this number',
'default': 1,
'type': 'int',
'unit': 'seeders'
},
],
},
],
}]

View File

@@ -129,7 +129,11 @@ class Searcher(SearcherBase):
# Try guessing via quality tags
guess = fireEvent('quality.guess', [nzb.get('name')], single = True)
return threed == guess.get('is_3d')
if guess:
return threed == guess.get('is_3d')
# If no quality guess, assume not 3d
else:
return threed == False
def correctYear(self, haystack, year, year_range):
@@ -174,6 +178,25 @@ class Searcher(SearcherBase):
return False
def containsWords(self, rel_name, rel_words, conf, media):
# Make sure it has required words
words = splitString(self.conf('%s_words' % conf, section = 'searcher').lower())
try: words = removeDuplicate(words + splitString(media['category'][conf].lower()))
except: pass
req_match = 0
for req_set in words:
if len(req_set) >= 2 and (req_set[:1] + req_set[-1:]) == '//':
if re.search(req_set[1:-1], rel_name):
log.debug('Regex match: %s', req_set[1:-1])
req_match += 1
else:
req = splitString(req_set, '&')
req_match += len(list(set(rel_words) & set(req))) == len(req)
return words, req_match > 0
def correctWords(self, rel_name, media):
media_title = fireEvent('searcher.get_search_title', media, single = True)
media_words = re.split('\W+', simplifyString(media_title))
@@ -181,31 +204,13 @@ class Searcher(SearcherBase):
rel_name = simplifyString(rel_name)
rel_words = re.split('\W+', rel_name)
# Make sure it has required words
required_words = splitString(self.conf('required_words', section = 'searcher').lower())
try: required_words = removeDuplicate(required_words + splitString(media['category']['required'].lower()))
except: pass
req_match = 0
for req_set in required_words:
req = splitString(req_set, '&')
req_match += len(list(set(rel_words) & set(req))) == len(req)
if len(required_words) > 0 and req_match == 0:
required_words, contains_required = self.containsWords(rel_name, rel_words, 'required', media)
if len(required_words) > 0 and not contains_required:
log.info2('Wrong: Required word missing: %s', rel_name)
return False
# Ignore releases
ignored_words = splitString(self.conf('ignored_words', section = 'searcher').lower())
try: ignored_words = removeDuplicate(ignored_words + splitString(media['category']['ignored'].lower()))
except: pass
ignored_match = 0
for ignored_set in ignored_words:
ignored = splitString(ignored_set, '&')
ignored_match += len(list(set(rel_words) & set(ignored))) == len(ignored)
if len(ignored_words) > 0 and ignored_match:
ignored_words, contains_ignored = self.containsWords(rel_name, rel_words, 'ignored', media)
if len(ignored_words) > 0 and contains_ignored:
log.info2("Wrong: '%s' contains 'ignored words'", rel_name)
return False

57
couchpotato/core/media/movie/_base/main.py Normal file → Executable file
View File

@@ -1,4 +1,3 @@
import os
import traceback
import time
@@ -28,6 +27,10 @@ class MovieBase(MovieTypeBase):
addApiView('movie.add', self.addView, docs = {
'desc': 'Add new movie to the wanted list',
'return': {'type': 'object', 'example': """{
'success': True,
'movie': object
}"""},
'params': {
'identifier': {'desc': 'IMDB id of the movie your want to add.'},
'profile_id': {'desc': 'ID of quality profile you want the add the movie in. If empty will use the default profile.'},
@@ -46,7 +49,7 @@ class MovieBase(MovieTypeBase):
})
addEvent('movie.add', self.add)
addEvent('movie.update_info', self.updateInfo)
addEvent('movie.update', self.update)
addEvent('movie.update_release_dates', self.updateReleaseDate)
def add(self, params = None, force_readd = True, search_after = True, update_after = True, notify_after = True, status = None):
@@ -151,8 +154,7 @@ class MovieBase(MovieTypeBase):
for release in fireEvent('release.for_media', m['_id'], single = True):
if release.get('status') in ['downloaded', 'snatched', 'seeding', 'done']:
if params.get('ignore_previous', False):
release['status'] = 'ignored'
db.update(release)
fireEvent('release.update_status', release['_id'], status = 'ignored')
else:
fireEvent('release.delete', release['_id'], single = True)
@@ -172,7 +174,7 @@ class MovieBase(MovieTypeBase):
# Trigger update info
if added and update_after:
# Do full update to get images etc
fireEventAsync('movie.update_info', m['_id'], default_title = params.get('title'), on_complete = onComplete)
fireEventAsync('movie.update', m['_id'], default_title = params.get('title'), on_complete = onComplete)
# Remove releases
for rel in fireEvent('release.for_media', m['_id'], single = True):
@@ -180,6 +182,9 @@ class MovieBase(MovieTypeBase):
db.delete(rel)
movie_dict = fireEvent('media.get', m['_id'], single = True)
if not movie_dict:
log.debug('Failed adding media, can\'t find it anymore')
return False
if do_search and search_after:
onComplete = self.createOnComplete(m['_id'])
@@ -256,7 +261,7 @@ class MovieBase(MovieTypeBase):
'success': False,
}
def updateInfo(self, media_id = None, identifier = None, default_title = None, extended = False):
def update(self, media_id = None, identifier = None, default_title = None, extended = False):
"""
Update movie information inside media['doc']['info']
@@ -269,6 +274,10 @@ class MovieBase(MovieTypeBase):
if self.shuttingDown():
return
lock_key = 'media.get.%s' % media_id if media_id else identifier
self.acquireLock(lock_key)
media = {}
try:
db = get_db()
@@ -312,42 +321,16 @@ class MovieBase(MovieTypeBase):
media['title'] = def_title
# Files
images = info.get('images', [])
media['files'] = media.get('files', {})
for image_type in ['poster']:
image_urls = info.get('images', [])
# Remove non-existing files
file_type = 'image_%s' % image_type
existing_files = list(set(media['files'].get(file_type, [])))
for ef in media['files'].get(file_type, []):
if not os.path.isfile(ef):
existing_files.remove(ef)
# Replace new files list
media['files'][file_type] = existing_files
if len(existing_files) == 0:
del media['files'][file_type]
# Loop over type
for image in images.get(image_type, []):
if not isinstance(image, (str, unicode)):
continue
if file_type not in media['files'] or len(media['files'].get(file_type, [])) == 0:
file_path = fireEvent('file.download', url = image, single = True)
if file_path:
media['files'][file_type] = [file_path]
break
else:
break
self.getPoster(media, image_urls)
db.update(media)
return media
except:
log.error('Failed update media: %s', traceback.format_exc())
return {}
self.releaseLock(lock_key)
return media
def updateReleaseDate(self, media_id):
"""
@@ -363,7 +346,7 @@ class MovieBase(MovieTypeBase):
media = db.get('id', media_id)
if not media.get('info'):
media = self.updateInfo(media_id)
media = self.update(media_id)
dates = media.get('info', {}).get('release_date')
else:
dates = media.get('info').get('release_date')

View File

@@ -115,8 +115,15 @@ MA.Release = new Class({
self.releases = null;
if(self.options_container){
self.options_container.destroy();
self.options_container = null;
// Releases are currently displayed
if(self.options_container.isDisplayed()){
self.options_container.destroy();
self.createReleases();
}
else {
self.options_container.destroy();
self.options_container = null;
}
}
});
@@ -131,10 +138,10 @@ MA.Release = new Class({
},
createReleases: function(){
createReleases: function(refresh){
var self = this;
if(!self.options_container){
if(!self.options_container || refresh){
self.options_container = new Element('div.options').grab(
self.release_container = new Element('div.releases.table')
);

View File

@@ -54,13 +54,21 @@ var Movie = new Class({
// Reload when releases have updated
self.global_events['release.update_status'] = function(notification){
var data = notification.data;
if(data && self.data._id == data.movie_id){
if(data && self.data._id == data.media_id){
if(!self.data.releases)
self.data.releases = [];
self.data.releases.push({'quality': data.quality, 'status': data.status});
self.updateReleases();
var updated = false;
self.data.releases.each(function(release){
if(release._id == data._id){
release['status'] = data.status;
updated = true;
}
});
if(updated)
self.updateReleases();
}
};
@@ -159,7 +167,7 @@ var Movie = new Class({
}
}
}),
self.thumbnail = (self.data.files && self.data.files.image_poster) ? new Element('img', {
self.thumbnail = (self.data.files && self.data.files.image_poster && self.data.files.image_poster.length > 0) ? new Element('img', {
'class': 'type_image poster',
'src': Api.createUrl('file.cache') + self.data.files.image_poster[0].split(Api.getOption('path_sep')).pop()
}): null,

View File

@@ -21,13 +21,6 @@ config = [{
'type': 'int',
'description': 'Maximum number of items displayed from each chart.',
},
{
'name': 'update_interval',
'default': 12,
'type': 'int',
'advanced': True,
'description': '(hours)',
},
{
'name': 'hide_wanted',
'default': False,

View File

@@ -1,6 +1,5 @@
import time
from couchpotato import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent,fireEvent
@@ -13,13 +12,14 @@ log = CPLog(__name__)
class Charts(Plugin):
update_in_progress = False
update_interval = 72 # hours
def __init__(self):
addApiView('charts.view', self.automationView)
addEvent('app.load', self.setCrons)
def setCrons(self):
fireEvent('schedule.interval', 'charts.update_cache', self.updateViewCache, hours = self.conf('update_interval', default = 12))
fireEvent('schedule.interval', 'charts.update_cache', self.updateViewCache, hours = self.update_interval)
def automationView(self, force_update = False, **kwargs):
@@ -52,7 +52,7 @@ class Charts(Plugin):
for chart in charts:
chart['hide_wanted'] = self.conf('hide_wanted')
chart['hide_library'] = self.conf('hide_library')
self.setCache('charts_cached', charts, timeout = 7200 * tryInt(self.conf('update_interval', default = 12)))
self.setCache('charts_cached', charts, timeout = self.update_interval * 3600)
except:
log.error('Failed refreshing charts')

View File

@@ -264,3 +264,11 @@
height: 40px;
}
@media all and (max-width: 480px) {
.toggle_menu h2 {
font-size: 16px;
text-align: center;
height: 30px;
}
}

View File

@@ -2,6 +2,8 @@ var Charts = new Class({
Implements: [Options, Events],
shown_once: false,
initialize: function(options){
var self = this;
self.setOptions(options);
@@ -40,17 +42,13 @@ var Charts = new Class({
)
);
if( Cookie.read('suggestions_charts_menu_selected') === 'charts')
self.el.show();
if( Cookie.read('suggestions_charts_menu_selected') === 'charts'){
self.show();
self.fireEvent.delay(0, self, 'created');
}
else
self.el.hide();
self.api_request = Api.request('charts.view', {
'onComplete': self.fill.bind(self)
});
self.fireEvent.delay(0, self, 'created');
},
fill: function(json){
@@ -157,6 +155,24 @@ var Charts = new Class({
},
show: function(){
var self = this;
self.el.show();
if(!self.shown_once){
self.api_request = Api.request('charts.view', {
'onComplete': self.fill.bind(self)
});
self.shown_once = true;
}
},
hide: function(){
this.el.hide();
},
afterAdded: function(m){
$(m).getElement('div.chart_number')

View File

@@ -1,3 +1,5 @@
import traceback
from bs4 import BeautifulSoup
from couchpotato import fireEvent
from couchpotato.core.helpers.rss import RSS
@@ -5,6 +7,7 @@ from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.media.movie.providers.automation.base import Automation
log = CPLog(__name__)
autoload = 'Bluray'
@@ -34,27 +37,49 @@ class Bluray(Automation, RSS):
try:
# Stop if the release year is before the minimal year
page_year = soup.body.find_all('center')[3].table.tr.find_all('td', recursive = False)[3].h3.get_text().split(', ')[1]
if tryInt(page_year) < self.getMinimal('year'):
brk = False
h3s = soup.body.find_all('h3')
for h3 in h3s:
if h3.parent.name != 'a':
try:
page_year = tryInt(h3.get_text()[-4:])
if page_year > 0 and page_year < self.getMinimal('year'):
brk = True
except:
log.error('Failed determining page year: %s', traceback.format_exc())
brk = True
break
if brk:
break
for table in soup.body.find_all('center')[3].table.tr.find_all('td', recursive = False)[3].find_all('table')[1:20]:
name = table.h3.get_text().lower().split('blu-ray')[0].strip()
year = table.small.get_text().split('|')[1].strip()
for h3 in h3s:
try:
if h3.parent.name == 'a':
name = h3.get_text().lower().split('blu-ray')[0].strip()
if not name.find('/') == -1: # make sure it is not a double movie release
continue
if not name.find('/') == -1: # make sure it is not a double movie release
continue
if tryInt(year) < self.getMinimal('year'):
continue
if not h3.parent.parent.small: # ignore non-movie tables
continue
imdb = self.search(name, year)
year = h3.parent.parent.small.get_text().split('|')[1].strip()
if imdb:
if self.isMinimalMovie(imdb):
movies.append(imdb['imdb'])
if tryInt(year) < self.getMinimal('year'):
continue
imdb = self.search(name, year)
if imdb:
if self.isMinimalMovie(imdb):
movies.append(imdb['imdb'])
except:
log.debug('Error parsing movie html: %s', traceback.format_exc())
break
except:
log.debug('Error loading page: %s', page)
log.debug('Error loading page %s: %s', (page, traceback.format_exc()))
break
self.conf('backlog', value = False)
@@ -134,7 +159,7 @@ config = [{
{
'name': 'backlog',
'advanced': True,
'description': 'Parses the history until the minimum movie year is reached. (Will be disabled once it has completed)',
'description': ('Parses the history until the minimum movie year is reached. (Takes a while)', 'Will be disabled once it has completed'),
'default': False,
'type': 'bool',
},

View File

@@ -2,7 +2,7 @@ import base64
import time
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.encoding import tryUrlencode, ss
from couchpotato.core.logger import CPLog
from couchpotato.core.media.movie.providers.base import MovieProvider
from couchpotato.environment import Env
@@ -66,7 +66,7 @@ class CouchPotatoApi(MovieProvider):
if not name:
return
name_enc = base64.b64encode(name)
name_enc = base64.b64encode(ss(name))
return self.getJsonData(self.urls['validate'] % name_enc, headers = self.getRequestHeaders())
def isMovie(self, identifier = None):

View File

@@ -4,6 +4,7 @@ from couchpotato import tryInt
from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.media.movie.providers.base import MovieProvider
from requests import HTTPError
log = CPLog(__name__)
@@ -23,22 +24,23 @@ class FanartTV(MovieProvider):
def __init__(self):
addEvent('movie.info', self.getArt, priority = 1)
def getArt(self, identifier = None, **kwargs):
def getArt(self, identifier = None, extended = True, **kwargs):
log.debug("Getting Extra Artwork from Fanart.tv...")
if not identifier:
if not identifier or not extended:
return {}
images = {}
try:
url = self.urls['api'] % identifier
fanart_data = self.getJsonData(url)
fanart_data = self.getJsonData(url, show_error = False)
if fanart_data:
log.debug('Found images for %s', fanart_data.get('name'))
images = self._parseMovie(fanart_data)
except HTTPError as e:
log.debug('Failed getting extra art for %s: %s',
(identifier, e))
except:
log.error('Failed getting extra art for %s: %s',
(identifier, traceback.format_exc()))

View File

@@ -1,11 +1,10 @@
import traceback
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import simplifyString, toUnicode, ss
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode, ss, tryUrlencode
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.media.movie.providers.base import MovieProvider
import tmdb3
log = CPLog(__name__)
@@ -13,54 +12,66 @@ autoload = 'TheMovieDb'
class TheMovieDb(MovieProvider):
MAX_EXTRATHUMBS = 4
http_time_between_calls = .35
configuration = {
'images': {
'secure_base_url': 'https://image.tmdb.org/t/p/',
},
}
def __init__(self):
addEvent('info.search', self.search, priority = 3)
addEvent('movie.search', self.search, priority = 3)
addEvent('movie.info', self.getInfo, priority = 3)
addEvent('movie.info_by_tmdb', self.getInfo)
addEvent('app.load', self.config)
# Configure TMDB settings
tmdb3.set_key(self.conf('api_key'))
tmdb3.set_cache('null')
def config(self):
configuration = self.request('configuration')
if configuration:
self.configuration = configuration
def search(self, q, limit = 12):
def search(self, q, limit = 3):
""" Find movie by name """
if self.isDisabled():
return False
search_string = simplifyString(q)
cache_key = 'tmdb.cache.%s.%s' % (search_string, limit)
results = self.getCache(cache_key)
log.debug('Searching for movie: %s', q)
if not results:
log.debug('Searching for movie: %s', q)
raw = None
try:
name_year = fireEvent('scanner.name_year', q, single = True)
raw = self.request('search/movie', {
'query': name_year.get('name', q),
'year': name_year.get('year'),
'search_type': 'ngram' if limit > 1 else 'phrase'
}, return_key = 'results')
except:
log.error('Failed searching TMDB for "%s": %s', (q, traceback.format_exc()))
raw = None
results = []
if raw:
try:
raw = tmdb3.searchMovie(search_string)
except:
log.error('Failed searching TMDB for "%s": %s', (search_string, traceback.format_exc()))
nr = 0
results = []
if raw:
try:
nr = 0
for movie in raw:
parsed_movie = self.parseMovie(movie, extended = False)
if parsed_movie:
results.append(parsed_movie)
for movie in raw:
results.append(self.parseMovie(movie, extended = False))
nr += 1
if nr == limit:
break
nr += 1
if nr == limit:
break
log.info('Found: %s', [result['titles'][0] + ' (' + str(result.get('year', 0)) + ')' for result in results])
log.info('Found: %s', [result['titles'][0] + ' (' + str(result.get('year', 0)) + ')' for result in results])
self.setCache(cache_key, results)
return results
except SyntaxError as e:
log.error('Failed to parse XML response: %s', e)
return False
return results
except SyntaxError as e:
log.error('Failed to parse XML response: %s', e)
return False
return results
@@ -69,101 +80,91 @@ class TheMovieDb(MovieProvider):
if not identifier:
return {}
cache_key = 'tmdb.cache.%s%s' % (identifier, '.ex' if extended else '')
result = self.getCache(cache_key)
result = self.parseMovie({
'id': identifier
}, extended = extended)
if not result:
try:
log.debug('Getting info: %s', cache_key)
# noinspection PyArgumentList
movie = tmdb3.Movie(identifier)
try: exists = movie.title is not None
except: exists = False
if exists:
result = self.parseMovie(movie, extended = extended)
self.setCache(cache_key, result)
else:
result = {}
except:
log.error('Failed getting info for %s: %s', (identifier, traceback.format_exc()))
return result
return result or {}
def parseMovie(self, movie, extended = True):
cache_key = 'tmdb.cache.%s%s' % (movie.id, '.ex' if extended else '')
movie_data = self.getCache(cache_key)
# Do request, append other items
movie = self.request('movie/%s' % movie.get('id'), {
'append_to_response': 'alternative_titles' + (',images,casts' if extended else '')
})
if not movie:
return
if not movie_data:
# Images
poster = self.getImage(movie, type = 'poster', size = 'w154')
poster_original = self.getImage(movie, type = 'poster', size = 'original')
backdrop_original = self.getImage(movie, type = 'backdrop', size = 'original')
extra_thumbs = self.getMultImages(movie, type = 'backdrops', size = 'original') if extended else []
# Images
poster = self.getImage(movie, type = 'poster', size = 'w154')
poster_original = self.getImage(movie, type = 'poster', size = 'original')
backdrop_original = self.getImage(movie, type = 'backdrop', size = 'original')
extra_thumbs = self.getMultImages(movie, type = 'backdrops', size = 'original', n = self.MAX_EXTRATHUMBS, skipfirst = True)
images = {
'poster': [poster] if poster else [],
#'backdrop': [backdrop] if backdrop else [],
'poster_original': [poster_original] if poster_original else [],
'backdrop_original': [backdrop_original] if backdrop_original else [],
'actors': {},
'extra_thumbs': extra_thumbs
}
images = {
'poster': [poster] if poster else [],
#'backdrop': [backdrop] if backdrop else [],
'poster_original': [poster_original] if poster_original else [],
'backdrop_original': [backdrop_original] if backdrop_original else [],
'actors': {},
'extra_thumbs': extra_thumbs
}
# Genres
try:
genres = [genre.get('name') for genre in movie.get('genres', [])]
except:
genres = []
# Genres
try:
genres = [genre.name for genre in movie.genres]
except:
genres = []
# 1900 is the same as None
year = str(movie.get('release_date') or '')[:4]
if not movie.get('release_date') or year == '1900' or year.lower() == 'none':
year = None
# 1900 is the same as None
year = str(movie.releasedate or '')[:4]
if not movie.releasedate or year == '1900' or year.lower() == 'none':
year = None
# Gather actors data
actors = {}
if extended:
# Gather actors data
actors = {}
if extended:
for cast_item in movie.cast:
try:
actors[toUnicode(cast_item.name)] = toUnicode(cast_item.character)
images['actors'][toUnicode(cast_item.name)] = self.getImage(cast_item, type = 'profile', size = 'original')
except:
log.debug('Error getting cast info for %s: %s', (cast_item, traceback.format_exc()))
# Full data
cast = movie.get('casts', {}).get('cast', [])
movie_data = {
'type': 'movie',
'via_tmdb': True,
'tmdb_id': movie.id,
'titles': [toUnicode(movie.title)],
'original_title': movie.originaltitle,
'images': images,
'imdb': movie.imdb,
'runtime': movie.runtime,
'released': str(movie.releasedate),
'year': tryInt(year, None),
'plot': movie.overview,
'genres': genres,
'collection': getattr(movie.collection, 'name', None),
'actor_roles': actors
}
for cast_item in cast:
try:
actors[toUnicode(cast_item.get('name'))] = toUnicode(cast_item.get('character'))
images['actors'][toUnicode(cast_item.get('name'))] = self.getImage(cast_item, type = 'profile', size = 'original')
except:
log.debug('Error getting cast info for %s: %s', (cast_item, traceback.format_exc()))
movie_data = dict((k, v) for k, v in movie_data.items() if v)
movie_data = {
'type': 'movie',
'via_tmdb': True,
'tmdb_id': movie.get('id'),
'titles': [toUnicode(movie.get('title'))],
'original_title': movie.get('original_title'),
'images': images,
'imdb': movie.get('imdb_id'),
'runtime': movie.get('runtime'),
'released': str(movie.get('release_date')),
'year': tryInt(year, None),
'plot': movie.get('overview'),
'genres': genres,
'collection': getattr(movie.get('belongs_to_collection'), 'name', None),
'actor_roles': actors
}
# Add alternative names
if movie_data['original_title'] and movie_data['original_title'] not in movie_data['titles']:
movie_data['titles'].append(movie_data['original_title'])
movie_data = dict((k, v) for k, v in movie_data.items() if v)
if extended:
for alt in movie.alternate_titles:
alt_name = alt.title
if alt_name and alt_name not in movie_data['titles'] and alt_name.lower() != 'none' and alt_name is not None:
movie_data['titles'].append(alt_name)
# Add alternative names
if movie_data['original_title'] and movie_data['original_title'] not in movie_data['titles']:
movie_data['titles'].append(movie_data['original_title'])
# Cache movie parsed
self.setCache(cache_key, movie_data)
# Add alternative titles
alternate_titles = movie.get('alternative_titles', {}).get('titles', [])
for alt in alternate_titles:
alt_name = alt.get('title')
if alt_name and alt_name not in movie_data['titles'] and alt_name.lower() != 'none' and alt_name is not None:
movie_data['titles'].append(alt_name)
return movie_data
@@ -171,36 +172,41 @@ class TheMovieDb(MovieProvider):
image_url = ''
try:
image_url = getattr(movie, type).geturl(size = size)
path = movie.get('%s_path' % type)
image_url = '%s%s%s' % (self.configuration['images']['secure_base_url'], size, path)
except:
log.debug('Failed getting %s.%s for "%s"', (type, size, ss(str(movie))))
return image_url
def getMultImages(self, movie, type = 'backdrops', size = 'original', n = -1, skipfirst = False):
"""
If n < 0, return all images. Otherwise return n images.
If n > len(getattr(movie, type)), then return all images.
If skipfirst is True, then it will skip getattr(movie, type)[0]. This
is because backdrops[0] is typically backdrop.
"""
def getMultImages(self, movie, type = 'backdrops', size = 'original'):
image_urls = []
try:
images = getattr(movie, type)
if n < 0 or n > len(images):
num_images = len(images)
else:
num_images = n
for i in range(int(skipfirst), num_images + int(skipfirst)):
image_urls.append(images[i].geturl(size = size))
for image in movie.get('images', {}).get(type, [])[1:5]:
image_urls.append(self.getImage(image, 'file', size))
except:
log.debug('Failed getting %i %s.%s for "%s"', (n, type, size, ss(str(movie))))
log.debug('Failed getting %s.%s for "%s"', (type, size, ss(str(movie))))
return image_urls
def request(self, call = '', params = {}, return_key = None):
params = dict((k, v) for k, v in params.items() if v)
params = tryUrlencode(params)
try:
url = 'http://api.themoviedb.org/3/%s?api_key=%s%s' % (call, self.conf('api_key'), '&%s' % params if params else '')
data = self.getJsonData(url, show_error = False)
except:
log.debug('Movie not found: %s, %s', (call, params))
data = None
if data and return_key and return_key in data:
data = data.get(return_key)
return data
def isDisabled(self):
if self.conf('api_key') == '':
log.error('No API key provided.')

View File

@@ -28,7 +28,7 @@ class MovieMetaData(MetaDataBase):
# Update library to get latest info
try:
group['media'] = fireEvent('movie.update_info', group['media'].get('_id'), identifier = getIdentifier(group['media']), extended = True, single = True)
group['media'] = fireEvent('movie.update', group['media'].get('_id'), identifier = getIdentifier(group['media']), extended = True, single = True)
except:
log.error('Failed to update movie, before creating metadata: %s', traceback.format_exc())

View File

@@ -1,30 +0,0 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.media._base.providers.nzb.nzbindex import Base
from couchpotato.core.media.movie.providers.base import MovieProvider
from couchpotato.environment import Env
log = CPLog(__name__)
autoload = 'NzbIndex'
class NzbIndex(MovieProvider, Base):
def buildUrl(self, media, quality):
title = fireEvent('library.query', media, include_year = False, single = True)
year = media['info']['year']
query = tryUrlencode({
'q': '"%s %s" | "%s (%s)"' % (title, year, title, year),
'age': Env.setting('retention', 'nzb'),
'sort': 'agedesc',
'minsize': quality.get('size_min'),
'maxsize': quality.get('size_max'),
'rating': 1,
'max': 250,
'more': 1,
'complete': 1,
})
return query

View File

@@ -13,7 +13,7 @@ class IPTorrents(MovieProvider, Base):
([87], ['3d']),
([48], ['720p', '1080p', 'bd50']),
([72], ['cam', 'ts', 'tc', 'r5', 'scr']),
([7], ['dvdrip', 'brrip']),
([7,48], ['dvdrip', 'brrip']),
([6], ['dvdr']),
]

View File

@@ -13,7 +13,7 @@ class PassThePopcorn(MovieProvider, Base):
'bd50': {'media': 'Blu-ray', 'format': 'BD50'},
'1080p': {'resolution': '1080p'},
'720p': {'resolution': '720p'},
'brrip': {'media': 'Blu-ray'},
'brrip': {'resolution': 'anyhd'},
'dvdr': {'resolution': 'anysd'},
'dvdrip': {'media': 'DVD'},
'scr': {'media': 'DVD-Screener'},
@@ -27,7 +27,7 @@ class PassThePopcorn(MovieProvider, Base):
'bd50': {'Codec': ['BD50']},
'1080p': {'Resolution': ['1080p']},
'720p': {'Resolution': ['720p']},
'brrip': {'Source': ['Blu-ray'], 'Quality': ['High Definition'], 'Container': ['!ISO']},
'brrip': {'Quality': ['High Definition'], 'Container': ['!ISO']},
'dvdr': {'Codec': ['DVD5', 'DVD9']},
'dvdrip': {'Source': ['DVD'], 'Codec': ['!DVD5', '!DVD9']},
'scr': {'Source': ['DVD-Screener']},

View File

@@ -1,27 +0,0 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.torrentleech import Base
from couchpotato.core.media.movie.providers.base import MovieProvider
log = CPLog(__name__)
autoload = 'TorrentLeech'
class TorrentLeech(MovieProvider, Base):
cat_ids = [
([13], ['720p', '1080p']),
([8], ['cam']),
([9], ['ts', 'tc']),
([10], ['r5', 'scr']),
([11], ['dvdrip']),
([14], ['brrip']),
([12], ['dvdr']),
]
def buildUrl(self, title, media, quality):
return (
tryUrlencode(title.replace(':', '')),
self.getCatId(quality)[0]
)

View File

@@ -3,7 +3,7 @@ import re
from bs4 import SoupStrainer, BeautifulSoup
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import mergeDicts, getTitle
from couchpotato.core.helpers.variable import mergeDicts, getTitle, getIdentifier
from couchpotato.core.logger import CPLog
from couchpotato.core.media.movie.providers.trailer.base import TrailerProvider
from requests import HTTPError
@@ -29,7 +29,7 @@ class HDTrailers(TrailerProvider):
url = self.urls['api'] % self.movieUrlName(movie_name)
try:
data = self.getCache('hdtrailers.%s' % group['identifier'], url, show_error = False)
data = self.getCache('hdtrailers.%s' % getIdentifier(group), url, show_error = False)
except HTTPError:
log.debug('No page found for: %s', movie_name)
data = None
@@ -59,7 +59,7 @@ class HDTrailers(TrailerProvider):
url = "%s?%s" % (self.urls['backup'], tryUrlencode({'s':movie_name}))
try:
data = self.getCache('hdtrailers.alt.%s' % group['identifier'], url, show_error = False)
data = self.getCache('hdtrailers.alt.%s' % getIdentifier(group), url, show_error = False)
except HTTPError:
log.debug('No alternative page found for: %s', movie_name)
data = None
@@ -68,7 +68,7 @@ class HDTrailers(TrailerProvider):
return results
try:
html = BeautifulSoup(data, 'html.parser', parse_only = self.only_tables_tags)
html = BeautifulSoup(data, parse_only = self.only_tables_tags)
result_table = html.find_all('h2', text = re.compile(movie_name))
for h2 in result_table:
@@ -90,7 +90,7 @@ class HDTrailers(TrailerProvider):
results = {'480p':[], '720p':[], '1080p':[]}
try:
html = BeautifulSoup(data, 'html.parser', parse_only = self.only_tables_tags)
html = BeautifulSoup(data, parse_only = self.only_tables_tags)
result_table = html.find('table', attrs = {'class':'bottomTable'})
for tr in result_table.find_all('tr'):

View File

@@ -25,6 +25,6 @@ class Filmstarts(UserscriptBase):
name = html.find("meta", {"property":"og:title"})['content']
# Year of production is not available in the meta data, so get it from the table
year = table.find("tr", text="Produktionsjahr").parent.parent.parent.td.text
year = table.find(text="Produktionsjahr").parent.parent.next_sibling.text
return self.search(name, year)
return self.search(name, year)

58
couchpotato/core/media/movie/searcher.py Normal file → Executable file
View File

@@ -74,7 +74,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
self.in_progress = True
fireEvent('notify.frontend', type = 'movie.searcher.started', data = True, message = 'Full search started')
medias = [x['_id'] for x in fireEvent('media.with_status', 'active', with_doc = False, single = True)]
medias = [x['_id'] for x in fireEvent('media.with_status', 'active', types = 'movie', with_doc = False, single = True)]
random.shuffle(medias)
total = len(medias)
@@ -89,12 +89,13 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
for media_id in medias:
media = fireEvent('media.get', media_id, single = True)
if not media: continue
try:
self.single(media, search_protocols, manual = manual)
except IndexError:
log.error('Forcing library update for %s, if you see this often, please report: %s', (getIdentifier(media), traceback.format_exc()))
fireEvent('movie.update_info', media_id)
fireEvent('movie.update', media_id)
except:
log.error('Search failed for %s: %s', (getIdentifier(media), traceback.format_exc()))
@@ -140,17 +141,17 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
previous_releases = movie.get('releases', [])
too_early_to_search = []
outside_eta_results = 0
alway_search = self.conf('always_search')
always_search = self.conf('always_search')
ignore_eta = manual
total_result_count = 0
fireEvent('notify.frontend', type = 'movie.searcher.started', data = {'_id': movie['_id']}, message = 'Searching for "%s"' % default_title)
# Ignore eta once every 7 days
if not alway_search:
if not always_search:
prop_name = 'last_ignored_eta.%s' % movie['_id']
last_ignored_eta = float(Env.prop(prop_name, default = 0))
if last_ignored_eta > time.time() - 604800:
if last_ignored_eta < time.time() - 604800:
ignore_eta = True
Env.prop(prop_name, value = time.time())
@@ -165,11 +166,12 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
'quality': q_identifier,
'finish': profile['finish'][index],
'wait_for': tryInt(profile['wait_for'][index]),
'3d': profile['3d'][index] if profile.get('3d') else False
'3d': profile['3d'][index] if profile.get('3d') else False,
'minimum_score': profile.get('minimum_score', 1),
}
could_not_be_released = not self.couldBeReleased(q_identifier in pre_releases, release_dates, movie['info']['year'])
if not alway_search and could_not_be_released:
if not always_search and could_not_be_released:
too_early_to_search.append(q_identifier)
# Skip release, if ETA isn't ignored
@@ -195,19 +197,12 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
break
quality = fireEvent('quality.single', identifier = q_identifier, single = True)
log.info('Search for %s in %s%s', (default_title, quality['label'], ' ignoring ETA' if alway_search or ignore_eta else ''))
log.info('Search for %s in %s%s', (default_title, quality['label'], ' ignoring ETA' if always_search or ignore_eta else ''))
# Extend quality with profile customs
quality['custom'] = quality_custom
results = fireEvent('searcher.search', search_protocols, movie, quality, single = True) or []
results_count = len(results)
total_result_count += results_count
if results_count == 0:
log.debug('Nothing found for %s in %s', (default_title, quality['label']))
# Keep track of releases found outside ETA window
outside_eta_results += results_count if could_not_be_released else 0
# Check if movie isn't deleted while searching
if not fireEvent('media.get', movie.get('_id'), single = True):
@@ -215,14 +210,20 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
# Add them to this movie releases list
found_releases += fireEvent('release.create_from_search', results, movie, quality, single = True)
results_count = len(found_releases)
total_result_count += results_count
if results_count == 0:
log.debug('Nothing found for %s in %s', (default_title, quality['label']))
# Keep track of releases found outside ETA window
outside_eta_results += results_count if could_not_be_released else 0
# Don't trigger download, but notify user of available releases
if could_not_be_released:
if results_count > 0:
log.debug('Found %s releases for "%s", but ETA isn\'t correct yet.', (results_count, default_title))
if could_not_be_released and results_count > 0:
log.debug('Found %s releases for "%s", but ETA isn\'t correct yet.', (results_count, default_title))
# Try find a valid result and download it
if (force_download or not could_not_be_released or alway_search) and fireEvent('release.try_download_result', results, movie, quality_custom, single = True):
if (force_download or not could_not_be_released or always_search) and fireEvent('release.try_download_result', results, movie, quality_custom, single = True):
ret = True
# Remove releases that aren't found anymore
@@ -240,7 +241,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
break
if total_result_count > 0:
fireEvent('media.tag', movie['_id'], 'recent', single = True)
fireEvent('media.tag', movie['_id'], 'recent', update_edited = True, single = True)
if len(too_early_to_search) > 0:
log.info2('Too early to search for %s, %s', (too_early_to_search, default_title))
@@ -277,7 +278,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
# Contains lower quality string
contains_other = fireEvent('searcher.contains_other_quality', nzb, movie_year = media['info']['year'], preferred_quality = preferred_quality, single = True)
if contains_other != False:
if contains_other and isinstance(contains_other, dict):
log.info2('Wrong: %s, looking for %s, found %s', (nzb['name'], quality['label'], [x for x in contains_other] if contains_other else 'no quality'))
return False
@@ -381,16 +382,17 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
def tryNextRelease(self, media_id, manual = False, force_download = False):
try:
db = get_db()
rels = fireEvent('media.with_status', ['snatched', 'done'], single = True)
rels = fireEvent('release.for_media', media_id, single = True)
for rel in rels:
rel['status'] = 'ignored'
db.update(rel)
if rel.get('status') in ['snatched', 'done']:
fireEvent('release.update_status', rel.get('_id'), status = 'ignored')
movie_dict = fireEvent('media.get', media_id, single = True)
log.info('Trying next release for: %s', getTitle(movie_dict))
self.single(movie_dict, manual = manual, force_download = force_download)
media = fireEvent('media.get', media_id, single = True)
if media:
log.info('Trying next release for: %s', getTitle(media))
self.single(media, manual = manual, force_download = force_download)
return True

2
couchpotato/core/media/movie/suggestion/main.py Normal file → Executable file
View File

@@ -27,7 +27,7 @@ class Suggestion(Plugin):
else:
if not movies or len(movies) == 0:
active_movies = fireEvent('media.with_status', ['active', 'done'], single = True)
active_movies = fireEvent('media.with_status', ['active', 'done'], types = 'movie', single = True)
movies = [getIdentifier(x) for x in active_movies]
if not ignored or len(ignored) == 0:

View File

@@ -2,6 +2,8 @@ var SuggestList = new Class({
Implements: [Options, Events],
shown_once: false,
initialize: function(options){
var self = this;
self.setOptions(options);
@@ -44,12 +46,13 @@ var SuggestList = new Class({
}
});
var cookie_menu_select = Cookie.read('suggestions_charts_menu_selected');
if( cookie_menu_select === 'suggestions' || cookie_menu_select === null ) self.el.show(); else self.el.hide();
var cookie_menu_select = Cookie.read('suggestions_charts_menu_selected') || 'suggestions';
if( cookie_menu_select === 'suggestions')
self.show();
else
self.hide();
self.api_request = Api.request('suggestion.view', {
'onComplete': self.fill.bind(self)
});
self.fireEvent('created');
},
@@ -145,6 +148,24 @@ var SuggestList = new Class({
},
show: function(){
var self = this;
self.el.show();
if(!self.shown_once){
self.api_request = Api.request('suggestion.view', {
'onComplete': self.fill.bind(self)
});
self.shown_once = true;
}
},
hide: function(){
this.el.hide();
},
toElement: function(){
return this.el;
}

View File

@@ -0,0 +1,11 @@
from couchpotato.core.media import MediaBase
class ShowTypeBase(MediaBase):
_type = 'show'
def getType(self):
if hasattr(self, 'type') and self.type != self._type:
return '%s.%s' % (self._type, self.type)
return self._type

View File

@@ -0,0 +1,4 @@
from .main import ShowBase
def autoload():
return ShowBase()

View File

@@ -0,0 +1,109 @@
from couchpotato import get_db
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.logger import CPLog
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.media import MediaBase
log = CPLog(__name__)
autoload = 'Episode'
class Episode(MediaBase):
def __init__(self):
addEvent('show.episode.add', self.add)
addEvent('show.episode.update', self.update)
addEvent('show.episode.update_extras', self.updateExtras)
def add(self, parent_id, info = None, update_after = True, status = None):
if not info: info = {}
identifiers = info.pop('identifiers', None)
if not identifiers:
log.warning('Unable to add episode, missing identifiers (info provider mismatch?)')
return
# Add Season
episode_info = {
'_t': 'media',
'type': 'show.episode',
'identifiers': identifiers,
'status': status if status else 'active',
'parent_id': parent_id,
'info': info, # Returned dict by providers
}
# Check if season already exists
existing_episode = fireEvent('media.with_identifiers', identifiers, with_doc = True, single = True)
db = get_db()
if existing_episode:
s = existing_episode['doc']
s.update(episode_info)
episode = db.update(s)
else:
episode = db.insert(episode_info)
# Update library info
if update_after is not False:
handle = fireEventAsync if update_after is 'async' else fireEvent
handle('show.episode.update_extras', episode, info, store = True, single = True)
return episode
def update(self, media_id = None, identifiers = None, info = None):
if not info: info = {}
if self.shuttingDown():
return
db = get_db()
episode = db.get('id', media_id)
# Get new info
if not info:
season = db.get('id', episode['parent_id'])
show = db.get('id', season['parent_id'])
info = fireEvent(
'episode.info', show.get('identifiers'), {
'season_identifiers': season.get('identifiers'),
'season_number': season.get('info', {}).get('number'),
'episode_identifiers': episode.get('identifiers'),
'episode_number': episode.get('info', {}).get('number'),
'absolute_number': episode.get('info', {}).get('absolute_number')
},
merge = True
)
info['season_number'] = season.get('info', {}).get('number')
identifiers = info.pop('identifiers', None) or identifiers
# Update/create media
episode['identifiers'].update(identifiers)
episode.update({'info': info})
self.updateExtras(episode, info)
db.update(episode)
return episode
def updateExtras(self, episode, info, store=False):
db = get_db()
# Get images
image_urls = info.get('images', [])
existing_files = episode.get('files', {})
self.getPoster(image_urls, existing_files)
if store:
db.update(episode)

View File

@@ -0,0 +1,291 @@
import time
import traceback
from couchpotato import get_db
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
from couchpotato.core.helpers.variable import getTitle, find
from couchpotato.core.logger import CPLog
from couchpotato.core.media import MediaBase
log = CPLog(__name__)
class ShowBase(MediaBase):
_type = 'show'
def __init__(self):
super(ShowBase, self).__init__()
self.initType()
addApiView('show.add', self.addView, docs = {
'desc': 'Add new show to the wanted list',
'params': {
'identifier': {'desc': 'IMDB id of the show your want to add.'},
'profile_id': {'desc': 'ID of quality profile you want the add the show in. If empty will use the default profile.'},
'category_id': {'desc': 'ID of category you want the add the show in.'},
'title': {'desc': 'Title of the show to use for search and renaming'},
}
})
addEvent('show.add', self.add)
addEvent('show.update', self.update)
addEvent('show.update_extras', self.updateExtras)
def addView(self, **kwargs):
add_dict = self.add(params = kwargs)
return {
'success': True if add_dict else False,
'show': add_dict,
}
def add(self, params = None, force_readd = True, search_after = True, update_after = True, notify_after = True, status = None):
if not params: params = {}
# Identifiers
if not params.get('identifiers'):
msg = 'Can\'t add show without at least 1 identifier.'
log.error(msg)
fireEvent('notify.frontend', type = 'show.no_identifier', message = msg)
return False
info = params.get('info')
if not info or (info and len(info.get('titles', [])) == 0):
info = fireEvent('show.info', merge = True, identifiers = params.get('identifiers'))
# Add Show
try:
m, added = self.create(info, params, force_readd, search_after, update_after)
result = fireEvent('media.get', m['_id'], single = True)
if added and notify_after:
if params.get('title'):
message = 'Successfully added "%s" to your wanted list.' % params.get('title', '')
else:
title = getTitle(m)
if title:
message = 'Successfully added "%s" to your wanted list.' % title
else:
message = 'Successfully added to your wanted list.'
fireEvent('notify.frontend', type = 'show.added', data = result, message = message)
return result
except:
log.error('Failed adding media: %s', traceback.format_exc())
def create(self, info, params = None, force_readd = True, search_after = True, update_after = True, notify_after = True, status = None):
# Set default title
def_title = self.getDefaultTitle(info)
# Default profile and category
default_profile = {}
if not params.get('profile_id'):
default_profile = fireEvent('profile.default', single = True)
cat_id = params.get('category_id')
media = {
'_t': 'media',
'type': 'show',
'title': def_title,
'identifiers': info.get('identifiers'),
'status': status if status else 'active',
'profile_id': params.get('profile_id', default_profile.get('_id')),
'category_id': cat_id if cat_id is not None and len(cat_id) > 0 and cat_id != '-1' else None
}
identifiers = info.pop('identifiers', {})
seasons = info.pop('seasons', {})
# Update media with info
self.updateInfo(media, info)
existing_show = fireEvent('media.with_identifiers', params.get('identifiers'), with_doc = True)
db = get_db()
if existing_show:
s = existing_show['doc']
s.update(media)
show = db.update(s)
else:
show = db.insert(media)
# Update dict to be usable
show.update(media)
added = True
do_search = False
search_after = search_after and self.conf('search_on_add', section = 'showsearcher')
onComplete = None
if existing_show:
if search_after:
onComplete = self.createOnComplete(show['_id'])
search_after = False
elif force_readd:
# Clean snatched history
for release in fireEvent('release.for_media', show['_id'], single = True):
if release.get('status') in ['downloaded', 'snatched', 'done']:
if params.get('ignore_previous', False):
release['status'] = 'ignored'
db.update(release)
else:
fireEvent('release.delete', release['_id'], single = True)
show['profile_id'] = params.get('profile_id', default_profile.get('id'))
show['category_id'] = media.get('category_id')
show['last_edit'] = int(time.time())
do_search = True
db.update(show)
else:
params.pop('info', None)
log.debug('Show already exists, not updating: %s', params)
added = False
# Create episodes
self.createEpisodes(show, seasons)
# Trigger update info
if added and update_after:
# Do full update to get images etc
fireEventAsync('show.update_extras', show.copy(), info, store = True, on_complete = onComplete)
# Remove releases
for rel in fireEvent('release.for_media', show['_id'], single = True):
if rel['status'] is 'available':
db.delete(rel)
if do_search and search_after:
onComplete = self.createOnComplete(show['_id'])
onComplete()
return show, added
def createEpisodes(self, m, seasons_info):
# Add Seasons
for season_nr in seasons_info:
season_info = seasons_info[season_nr]
episodes = season_info.get('episodes', {})
season = fireEvent('show.season.add', m.get('_id'), season_info, update_after = False, single = True)
# Add Episodes
for episode_nr in episodes:
episode_info = episodes[episode_nr]
episode_info['season_number'] = season_nr
fireEvent('show.episode.add', season.get('_id'), episode_info, update_after = False, single = True)
def update(self, media_id = None, media = None, identifiers = None, info = None):
"""
Update movie information inside media['doc']['info']
@param media_id: document id
@param identifiers: identifiers from multiple providers
{
'thetvdb': 123,
'imdb': 'tt123123',
..
}
@param extended: update with extended info (parses more info, actors, images from some info providers)
@return: dict, with media
"""
if not info: info = {}
if not identifiers: identifiers = {}
db = get_db()
if self.shuttingDown():
return
if media is None and media_id:
media = db.get('id', media_id)
else:
log.error('missing "media" and "media_id" parameters, unable to update')
return
if not info:
info = fireEvent('show.info', identifiers = media.get('identifiers'), merge = True)
try:
identifiers = info.pop('identifiers', {})
seasons = info.pop('seasons', {})
self.updateInfo(media, info)
self.updateEpisodes(media, seasons)
self.updateExtras(media, info)
db.update(media)
return media
except:
log.error('Failed update media: %s', traceback.format_exc())
return {}
def updateInfo(self, media, info):
db = get_db()
# Remove season info for later use (save separately)
info.pop('in_wanted', None)
info.pop('in_library', None)
if not info or len(info) == 0:
log.error('Could not update, no show info to work with: %s', media.get('identifier'))
return False
# Update basic info
media['info'] = info
def updateEpisodes(self, media, seasons):
# Fetch current season/episode tree
show_tree = fireEvent('library.tree', media_id = media['_id'], single = True)
# Update seasons
for season_num in seasons:
season_info = seasons[season_num]
episodes = season_info.get('episodes', {})
# Find season that matches number
season = find(lambda s: s.get('info', {}).get('number', 0) == season_num, show_tree.get('seasons', []))
if not season:
log.warning('Unable to find season "%s"', season_num)
continue
# Update season
fireEvent('show.season.update', season['_id'], info = season_info, single = True)
# Update episodes
for episode_num in episodes:
episode_info = episodes[episode_num]
episode_info['season_number'] = season_num
# Find episode that matches number
episode = find(lambda s: s.get('info', {}).get('number', 0) == episode_num, season.get('episodes', []))
if not episode:
log.debug('Creating new episode %s in season %s', (episode_num, season_num))
fireEvent('show.episode.add', season.get('_id'), episode_info, update_after = False, single = True)
continue
fireEvent('show.episode.update', episode['_id'], info = episode_info, single = True)
def updateExtras(self, media, info, store=False):
db = get_db()
# Update image file
image_urls = info.get('images', [])
self.getPoster(media, image_urls)
if store:
db.update(media)

View File

@@ -0,0 +1,94 @@
from couchpotato import get_db
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.logger import CPLog
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.media import MediaBase
log = CPLog(__name__)
autoload = 'Season'
class Season(MediaBase):
def __init__(self):
addEvent('show.season.add', self.add)
addEvent('show.season.update', self.update)
addEvent('show.season.update_extras', self.updateExtras)
def add(self, parent_id, info = None, update_after = True, status = None):
if not info: info = {}
identifiers = info.pop('identifiers', None)
info.pop('episodes', None)
# Add Season
season_info = {
'_t': 'media',
'type': 'show.season',
'identifiers': identifiers,
'status': status if status else 'active',
'parent_id': parent_id,
'info': info, # Returned dict by providers
}
# Check if season already exists
existing_season = fireEvent('media.with_identifiers', identifiers, with_doc = True, single = True)
db = get_db()
if existing_season:
s = existing_season['doc']
s.update(season_info)
season = db.update(s)
else:
season = db.insert(season_info)
# Update library info
if update_after is not False:
handle = fireEventAsync if update_after is 'async' else fireEvent
handle('show.season.update_extras', season, info, store = True, single = True)
return season
def update(self, media_id = None, identifiers = None, info = None):
if not info: info = {}
if self.shuttingDown():
return
db = get_db()
season = db.get('id', media_id)
show = db.get('id', season['parent_id'])
# Get new info
if not info:
info = fireEvent('season.info', show.get('identifiers'), {
'season_number': season.get('info', {}).get('number', 0)
}, merge = True)
identifiers = info.pop('identifiers', None) or identifiers
info.pop('episodes', None)
# Update/create media
season['identifiers'].update(identifiers)
season.update({'info': info})
self.updateExtras(season, info)
db.update(season)
return season
def updateExtras(self, season, info, store=False):
db = get_db()
# Get images
image_urls = info.get('images', [])
existing_files = season.get('files', {})
self.getPoster(image_urls, existing_files)
if store:
db.update(season)

View File

@@ -0,0 +1,28 @@
Page.Shows = new Class({
Extends: PageBase,
name: 'shows',
title: 'Gimmy gimmy gimmy!',
folder_browser: null,
indexAction: function(){
var self = this;
if(!self.wanted){
// Wanted movies
self.wanted = new ShowList({
'identifier': 'wanted',
'status': 'active',
'type': 'show',
'actions': [MA.IMDB, MA.Trailer, MA.Release, MA.Edit, MA.Refresh, MA.Readd, MA.Delete],
'add_new': true,
'on_empty_element': App.createUserscriptButtons().addClass('empty_wanted')
});
$(self.wanted).inject(self.el);
}
}
});

View File

@@ -0,0 +1,474 @@
var EpisodeAction = new Class({
Implements: [Options],
class_name: 'item-action icon2',
initialize: function(episode, options){
var self = this;
self.setOptions(options);
self.show = episode.show;
self.episode = episode;
self.create();
if(self.el)
self.el.addClass(self.class_name)
},
create: function(){},
disable: function(){
if(this.el)
this.el.addClass('disable')
},
enable: function(){
if(this.el)
this.el.removeClass('disable')
},
getTitle: function(){
var self = this;
try {
return self.show.getTitle();
}
catch(e){
try {
return self.show.original_title ? self.show.original_title : self.show.titles[0];
}
catch(e){
return 'Unknown';
}
}
},
get: function(key){
var self = this;
try {
return self.show.get(key)
}
catch(e){
return self.show[key]
}
},
createMask: function(){
var self = this;
self.mask = new Element('div.mask', {
'styles': {
'z-index': '1'
}
}).inject(self.show, 'top').fade('hide');
},
toElement: function(){
return this.el || null
}
});
var EA = {};
EA.IMDB = new Class({
Extends: EpisodeAction,
id: null,
create: function(){
var self = this;
self.id = self.show.getIdentifier ? self.show.getIdentifier() : self.get('imdb');
self.el = new Element('a.imdb', {
'title': 'Go to the IMDB page of ' + self.getTitle(),
'href': 'http://www.imdb.com/title/'+self.id+'/',
'target': '_blank'
});
if(!self.id) self.disable();
}
});
EA.Release = new Class({
Extends: EpisodeAction,
create: function(){
var self = this;
self.el = new Element('a.releases.download', {
'title': 'Show the releases that are available for ' + self.getTitle(),
'events': {
'click': self.toggle.bind(self)
}
});
self.options = new Element('div.episode-options').inject(self.episode.el);
if(!self.episode.data.releases || self.episode.data.releases.length == 0)
self.el.hide();
else
self.showHelper();
App.on('show.searcher.ended', function(notification){
if(self.show.data._id != notification.data._id) return;
self.releases = null;
if(self.options_container){
self.options_container.destroy();
self.options_container = null;
}
});
},
toggle: function(e){
var self = this;
if(self.options && self.options.hasClass('expanded')) {
self.close();
} else {
self.open();
}
},
open: function(e){
var self = this;
if(e)
(e).preventDefault();
self.createReleases();
},
close: function(e) {
var self = this;
if(e)
(e).preventDefault();
self.options.setStyle('height', 0)
.removeClass('expanded');
},
createReleases: function(){
var self = this;
if(!self.releases_table){
self.options.adopt(
self.releases_table = new Element('div.releases.table')
);
// Header
new Element('div.item.head').adopt(
new Element('span.name', {'text': 'Release name'}),
new Element('span.status', {'text': 'Status'}),
new Element('span.quality', {'text': 'Quality'}),
new Element('span.size', {'text': 'Size'}),
new Element('span.age', {'text': 'Age'}),
new Element('span.score', {'text': 'Score'}),
new Element('span.provider', {'text': 'Provider'})
).inject(self.releases_table);
if(self.episode.data.releases)
self.episode.data.releases.each(function(release){
var quality = Quality.getQuality(release.quality) || {},
info = release.info || {},
provider = self.get(release, 'provider') + (info['provider_extra'] ? self.get(release, 'provider_extra') : '');
var release_name = self.get(release, 'name');
if(release.files && release.files.length > 0){
try {
var movie_file = release.files.filter(function(file){
var type = File.Type.get(file.type_id);
return type && type.identifier == 'movie'
}).pick();
release_name = movie_file.path.split(Api.getOption('path_sep')).getLast();
}
catch(e){}
}
// Create release
release['el'] = new Element('div', {
'class': 'item '+release.status,
'id': 'release_'+release._id
}).adopt(
new Element('span.name', {'text': release_name, 'title': release_name}),
new Element('span.status', {'text': release.status, 'class': 'status '+release.status}),
new Element('span.quality', {'text': quality.label + (release.is_3d ? ' 3D' : '') || 'n/a'}),
new Element('span.size', {'text': info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}),
new Element('span.age', {'text': self.get(release, 'age')}),
new Element('span.score', {'text': self.get(release, 'score')}),
new Element('span.provider', { 'text': provider, 'title': provider }),
info['detail_url'] ? new Element('a.info.icon2', {
'href': info['detail_url'],
'target': '_blank'
}) : new Element('a'),
new Element('a.download.icon2', {
'events': {
'click': function(e){
(e).preventDefault();
if(!this.hasClass('completed'))
self.download(release);
}
}
}),
new Element('a.delete.icon2', {
'events': {
'click': function(e){
(e).preventDefault();
self.ignore(release);
}
}
})
).inject(self.releases_table);
if(release.status == 'ignored' || release.status == 'failed' || release.status == 'snatched'){
if(!self.last_release || (self.last_release && self.last_release.status != 'snatched' && release.status == 'snatched'))
self.last_release = release;
}
else if(!self.next_release && release.status == 'available'){
self.next_release = release;
}
var update_handle = function(notification) {
if(notification.data._id != release._id) return;
var q = self.show.quality.getElement('.q_' + release.quality),
new_status = notification.data.status;
release.el.set('class', 'item ' + new_status);
var status_el = release.el.getElement('.release_status');
status_el.set('class', 'release_status ' + new_status);
status_el.set('text', new_status);
if(!q && (new_status == 'snatched' || new_status == 'seeding' || new_status == 'done'))
q = self.addQuality(release.quality_id);
if(q && !q.hasClass(new_status)) {
q.removeClass(release.status).addClass(new_status);
q.set('title', q.get('title').replace(release.status, new_status));
}
};
App.on('release.update_status', update_handle);
});
if(self.last_release)
self.releases_table.getElements('#release_'+self.last_release._id).addClass('last_release');
if(self.next_release)
self.releases_table.getElements('#release_'+self.next_release._id).addClass('next_release');
if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status) === false)){
self.trynext_container = new Element('div.buttons.try_container').inject(self.releases_table, 'top');
var nr = self.next_release,
lr = self.last_release;
self.trynext_container.adopt(
new Element('span.or', {
'text': 'If anything went wrong, download'
}),
lr ? new Element('a.button.orange', {
'text': 'the same release again',
'events': {
'click': function(){
self.download(lr);
}
}
}) : null,
nr && lr ? new Element('span.or', {
'text': ','
}) : null,
nr ? [new Element('a.button.green', {
'text': lr ? 'another release' : 'the best release',
'events': {
'click': function(){
self.download(nr);
}
}
}),
new Element('span.or', {
'text': 'or pick one below'
})] : null
)
}
self.last_release = null;
self.next_release = null;
self.episode.el.addEvent('outerClick', function(){
self.close();
});
}
self.options.setStyle('height', self.releases_table.getSize().y)
.addClass('expanded');
},
showHelper: function(e){
var self = this;
if(e)
(e).preventDefault();
var has_available = false,
has_snatched = false;
if(self.episode.data.releases)
self.episode.data.releases.each(function(release){
if(has_available && has_snatched) return;
if(['snatched', 'downloaded', 'seeding'].contains(release.status))
has_snatched = true;
if(['available'].contains(release.status))
has_available = true;
});
if(has_available || has_snatched){
self.trynext_container = new Element('div.buttons.trynext').inject(self.show.info_container);
self.trynext_container.adopt(
has_available ? [new Element('a.icon2.readd', {
'text': has_snatched ? 'Download another release' : 'Download the best release',
'events': {
'click': self.tryNextRelease.bind(self)
}
}),
new Element('a.icon2.download', {
'text': 'pick one yourself',
'events': {
'click': function(){
self.show.quality.fireEvent('click');
}
}
})] : null,
new Element('a.icon2.completed', {
'text': 'mark this movie done',
'events': {
'click': self.markMovieDone.bind(self)
}
})
)
}
},
get: function(release, type){
return (release.info && release.info[type] !== undefined) ? release.info[type] : 'n/a'
},
download: function(release){
var self = this;
var release_el = self.releases_table.getElement('#release_'+release._id),
icon = release_el.getElement('.download.icon2');
if(icon)
icon.addClass('icon spinner').removeClass('download');
Api.request('release.manual_download', {
'data': {
'id': release._id
},
'onComplete': function(json){
if(icon)
icon.removeClass('icon spinner');
if(json.success){
if(icon)
icon.addClass('completed');
release_el.getElement('.release_status').set('text', 'snatched');
}
else
if(icon)
icon.addClass('attention').set('title', 'Something went wrong when downloading, please check logs.');
}
});
},
ignore: function(release){
Api.request('release.ignore', {
'data': {
'id': release._id
}
})
},
markMovieDone: function(){
var self = this;
Api.request('media.delete', {
'data': {
'id': self.show.get('_id'),
'delete_from': 'wanted'
},
'onComplete': function(){
var movie = $(self.show);
movie.set('tween', {
'duration': 300,
'onComplete': function(){
self.show.destroy()
}
});
movie.tween('height', 0);
}
});
},
tryNextRelease: function(){
var self = this;
Api.request('movie.searcher.try_next', {
'data': {
'media_id': self.show.get('_id')
}
});
}
});
EA.Refresh = new Class({
Extends: EpisodeAction,
create: function(){
var self = this;
self.el = new Element('a.refresh', {
'title': 'Refresh the movie info and do a forced search',
'events': {
'click': self.doRefresh.bind(self)
}
});
},
doRefresh: function(e){
var self = this;
(e).preventDefault();
Api.request('media.refresh', {
'data': {
'id': self.episode.get('_id')
}
});
}
});

View File

@@ -0,0 +1,128 @@
var Episode = new Class({
Extends: BlockBase,
action: {},
initialize: function(show, options, data){
var self = this;
self.setOptions(options);
self.show = show;
self.options = options;
self.data = data;
self.profile = self.show.profile;
self.el = new Element('div.item.episode').adopt(
self.detail = new Element('div.item.data')
);
self.create();
},
create: function(){
var self = this;
self.detail.set('id', 'episode_'+self.data._id);
self.detail.adopt(
new Element('span.episode', {'text': (self.data.info.number || 0)}),
new Element('span.name', {'text': self.getTitle()}),
new Element('span.firstaired', {'text': self.data.info.firstaired}),
self.quality = new Element('span.quality', {
'events': {
'click': function(e){
var releases = self.detail.getElement('.item-actions .releases');
if(releases.isVisible())
releases.fireEvent('click', [e])
}
}
}),
self.actions = new Element('div.item-actions')
);
// Add profile
if(self.profile.data) {
self.profile.getTypes().each(function(type){
var q = self.addQuality(type.get('quality'), type.get('3d'));
if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){
q.addClass('finish');
q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.')
}
});
}
// Add releases
self.updateReleases();
Object.each(self.options.actions, function(action, key){
self.action[key.toLowerCase()] = action = new self.options.actions[key](self);
if(action.el)
self.actions.adopt(action)
});
},
updateReleases: function(){
var self = this;
if(!self.data.releases || self.data.releases.length == 0) return;
self.data.releases.each(function(release){
var q = self.quality.getElement('.q_'+ release.quality+(release.is_3d ? '.is_3d' : ':not(.is_3d)')),
status = release.status;
if(!q && (status == 'snatched' || status == 'seeding' || status == 'done'))
q = self.addQuality(release.quality, release.is_3d || false);
if (q && !q.hasClass(status)){
q.addClass(status);
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status)
}
});
},
addQuality: function(quality, is_3d){
var self = this,
q = Quality.getQuality(quality);
return new Element('span', {
'text': q.label + (is_3d ? ' 3D' : ''),
'class': 'q_'+q.identifier + (is_3d ? ' is_3d' : ''),
'title': ''
}).inject(self.quality);
},
getTitle: function(){
var self = this;
var title = '';
if(self.data.info.titles && self.data.info.titles.length > 0) {
title = self.data.info.titles[0];
} else {
title = 'Episode ' + self.data.info.number;
}
return title;
},
getIdentifier: function(){
var self = this;
try {
return self.get('identifiers').imdb;
}
catch (e){ }
return self.get('imdb');
},
get: function(attr){
return this.data[attr] || this.data.info[attr]
}
});

View File

@@ -0,0 +1,636 @@
var ShowList = new Class({
Implements: [Events, Options],
options: {
navigation: true,
limit: 50,
load_more: true,
loader: true,
menu: [],
add_new: false,
force_view: false
},
movies: [],
movies_added: {},
total_movies: 0,
letters: {},
filter: null,
initialize: function(options){
var self = this;
self.setOptions(options);
self.offset = 0;
self.filter = self.options.filter || {
'starts_with': null,
'search': null
};
self.el = new Element('div.shows').adopt(
self.title = self.options.title ? new Element('h2', {
'text': self.options.title,
'styles': {'display': 'none'}
}) : null,
self.description = self.options.description ? new Element('div.description', {
'html': self.options.description,
'styles': {'display': 'none'}
}) : null,
self.movie_list = new Element('div.list'),
self.load_more = self.options.load_more ? new Element('a.load_more', {
'events': {
'click': self.loadMore.bind(self)
}
}) : null
);
if($(window).getSize().x <= 480 && !self.options.force_view)
self.changeView('list');
else
self.changeView(self.getSavedView() || self.options.view || 'details');
self.getMovies();
App.on('movie.added', self.movieAdded.bind(self));
App.on('movie.deleted', self.movieDeleted.bind(self))
},
movieDeleted: function(notification){
var self = this;
if(self.movies_added[notification.data._id]){
self.movies.each(function(movie){
if(movie.get('_id') == notification.data._id){
movie.destroy();
delete self.movies_added[notification.data._id];
self.setCounter(self.counter_count-1);
self.total_movies--;
}
})
}
self.checkIfEmpty();
},
movieAdded: function(notification){
var self = this;
self.fireEvent('movieAdded', notification);
if(self.options.add_new && !self.movies_added[notification.data._id] && notification.data.status == self.options.status){
window.scroll(0,0);
self.createShow(notification.data, 'top');
self.setCounter(self.counter_count+1);
self.checkIfEmpty();
}
},
create: function(){
var self = this;
// Create the alphabet nav
if(self.options.navigation)
self.createNavigation();
if(self.options.load_more)
self.scrollspy = new ScrollSpy({
min: function(){
var c = self.load_more.getCoordinates();
return c.top - window.document.getSize().y - 300
},
onEnter: self.loadMore.bind(self)
});
self.created = true;
},
addMovies: function(movies, total){
var self = this;
if(!self.created) self.create();
// do scrollspy
if(movies.length < self.options.limit && self.scrollspy){
self.load_more.hide();
self.scrollspy.stop();
}
Object.each(movies, function(movie){
self.createShow(movie);
});
self.total_movies += total;
self.setCounter(total);
},
setCounter: function(count){
var self = this;
if(!self.navigation_counter) return;
self.counter_count = count;
self.navigation_counter.set('text', (count || 0) + ' shows');
if (self.empty_message) {
self.empty_message.destroy();
self.empty_message = null;
}
if(self.total_movies && count == 0 && !self.empty_message){
var message = (self.filter.search ? 'for "'+self.filter.search+'"' : '') +
(self.filter.starts_with ? ' in <strong>'+self.filter.starts_with+'</strong>' : '');
self.empty_message = new Element('.message', {
'html': 'No shows found ' + message + '.<br/>'
}).grab(
new Element('a', {
'text': 'Reset filter',
'events': {
'click': function(){
self.filter = {
'starts_with': null,
'search': null
};
self.navigation_search_input.set('value', '');
self.reset();
self.activateLetter();
self.getMovies(true);
self.last_search_value = '';
}
}
})
).inject(self.movie_list);
}
},
createShow: function(show, inject_at){
var self = this;
var m = new Show(self, {
'actions': self.options.actions,
'view': self.current_view,
'onSelect': self.calculateSelected.bind(self)
}, show);
$(m).inject(self.movie_list, inject_at || 'bottom');
m.fireEvent('injected');
self.movies.include(m);
self.movies_added[show._id] = true;
},
createNavigation: function(){
var self = this;
var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
self.el.addClass('with_navigation');
self.navigation = new Element('div.alph_nav').adopt(
self.mass_edit_form = new Element('div.mass_edit_form').adopt(
new Element('span.select').adopt(
self.mass_edit_select = new Element('input[type=checkbox].inlay', {
'events': {
'change': self.massEditToggleAll.bind(self)
}
}),
self.mass_edit_selected = new Element('span.count', {'text': 0}),
self.mass_edit_selected_label = new Element('span', {'text': 'selected'})
),
new Element('div.quality').adopt(
self.mass_edit_quality = new Element('select'),
new Element('a.button.orange', {
'text': 'Change quality',
'events': {
'click': self.changeQualitySelected.bind(self)
}
})
),
new Element('div.delete').adopt(
new Element('span[text=or]'),
new Element('a.button.red', {
'text': 'Delete',
'events': {
'click': self.deleteSelected.bind(self)
}
})
),
new Element('div.refresh').adopt(
new Element('span[text=or]'),
new Element('a.button.green', {
'text': 'Refresh',
'events': {
'click': self.refreshSelected.bind(self)
}
})
)
),
new Element('div.menus').adopt(
self.navigation_counter = new Element('span.counter[title=Total]'),
self.filter_menu = new Block.Menu(self, {
'class': 'filter'
}),
self.navigation_actions = new Element('ul.actions', {
'events': {
'click:relay(li)': function(e, el){
var a = 'active';
self.navigation_actions.getElements('.'+a).removeClass(a);
self.changeView(el.get('data-view'));
this.addClass(a);
el.inject(el.getParent(), 'top');
el.getSiblings().hide();
setTimeout(function(){
el.getSiblings().setStyle('display', null);
}, 100)
}
}
}),
self.navigation_menu = new Block.Menu(self, {
'class': 'extra'
})
)
).inject(self.el, 'top');
// Mass edit
self.mass_edit_select_class = new Form.Check(self.mass_edit_select);
Quality.getActiveProfiles().each(function(profile){
new Element('option', {
'value': profile.get('_id'),
'text': profile.get('label')
}).inject(self.mass_edit_quality)
});
self.filter_menu.addLink(
self.navigation_search_input = new Element('input', {
'title': 'Search through ' + self.options.identifier,
'placeholder': 'Search through ' + self.options.identifier,
'events': {
'keyup': self.search.bind(self),
'change': self.search.bind(self)
}
})
).addClass('search');
var available_chars;
self.filter_menu.addEvent('open', function(){
self.navigation_search_input.focus();
// Get available chars and highlight
if(!available_chars && (self.navigation.isDisplayed() || self.navigation.isVisible()))
Api.request('media.available_chars', {
'data': Object.merge({
'type': 'show',
'status': self.options.status
}, self.filter),
'onSuccess': function(json){
available_chars = json.chars;
available_chars.each(function(c){
self.letters[c.capitalize()].addClass('available')
})
}
});
});
self.filter_menu.addLink(
self.navigation_alpha = new Element('ul.numbers', {
'events': {
'click:relay(li.available)': function(e, el){
self.activateLetter(el.get('data-letter'));
self.getMovies(true)
}
}
})
);
// Actions
['mass_edit', 'details', 'list'].each(function(view){
var current = self.current_view == view;
new Element('li', {
'class': 'icon2 ' + view + (current ? ' active ' : ''),
'data-view': view
}).inject(self.navigation_actions, current ? 'top' : 'bottom');
});
// All
self.letters['all'] = new Element('li.letter_all.available.active', {
'text': 'ALL'
}).inject(self.navigation_alpha);
// Chars
chars.split('').each(function(c){
self.letters[c] = new Element('li', {
'text': c,
'class': 'letter_'+c,
'data-letter': c
}).inject(self.navigation_alpha);
});
// Add menu or hide
if (self.options.menu.length > 0)
self.options.menu.each(function(menu_item){
self.navigation_menu.addLink(menu_item);
});
else
self.navigation_menu.hide();
},
calculateSelected: function(){
var self = this;
var selected = 0,
movies = self.movies.length;
self.movies.each(function(movie){
selected += movie.isSelected() ? 1 : 0
});
var indeterminate = selected > 0 && selected < movies,
checked = selected == movies && selected > 0;
self.mass_edit_select.set('indeterminate', indeterminate);
self.mass_edit_select_class[checked ? 'check' : 'uncheck']();
self.mass_edit_select_class.element[indeterminate ? 'addClass' : 'removeClass']('indeterminate');
self.mass_edit_selected.set('text', selected);
},
deleteSelected: function(){
var self = this,
ids = self.getSelectedMovies(),
help_msg = self.identifier == 'wanted' ? 'If you do, you won\'t be able to watch them, as they won\'t get downloaded!' : 'Your files will be safe, this will only delete the reference from the CouchPotato manage list';
var qObj = new Question('Are you sure you want to delete '+ids.length+' movie'+ (ids.length != 1 ? 's' : '') +'?', help_msg, [{
'text': 'Yes, delete '+(ids.length != 1 ? 'them' : 'it'),
'class': 'delete',
'events': {
'click': function(e){
(e).preventDefault();
this.set('text', 'Deleting..');
Api.request('media.delete', {
'method': 'post',
'data': {
'id': ids.join(','),
'delete_from': self.options.identifier
},
'onSuccess': function(){
qObj.close();
var erase_movies = [];
self.movies.each(function(movie){
if (movie.isSelected()){
$(movie).destroy();
erase_movies.include(movie);
}
});
erase_movies.each(function(movie){
self.movies.erase(movie);
movie.destroy();
self.setCounter(self.counter_count-1);
self.total_movies--;
});
self.calculateSelected();
}
});
}
}
}, {
'text': 'Cancel',
'cancel': true
}]);
},
changeQualitySelected: function(){
var self = this;
var ids = self.getSelectedMovies();
Api.request('movie.edit', {
'method': 'post',
'data': {
'id': ids.join(','),
'profile_id': self.mass_edit_quality.get('value')
},
'onSuccess': self.search.bind(self)
});
},
refreshSelected: function(){
var self = this;
var ids = self.getSelectedMovies();
Api.request('media.refresh', {
'method': 'post',
'data': {
'id': ids.join(',')
}
});
},
getSelectedMovies: function(){
var self = this;
var ids = [];
self.movies.each(function(movie){
if (movie.isSelected())
ids.include(movie.get('_id'))
});
return ids
},
massEditToggleAll: function(){
var self = this;
var select = self.mass_edit_select.get('checked');
self.movies.each(function(movie){
movie.select(select)
});
self.calculateSelected()
},
reset: function(){
var self = this;
self.movies = [];
if(self.mass_edit_select)
self.calculateSelected();
if(self.navigation_alpha)
self.navigation_alpha.getElements('.active').removeClass('active');
self.offset = 0;
if(self.scrollspy){
//self.load_more.show();
self.scrollspy.start();
}
},
activateLetter: function(letter){
var self = this;
self.reset();
self.letters[letter || 'all'].addClass('active');
self.filter.starts_with = letter;
},
changeView: function(new_view){
var self = this;
self.el
.removeClass(self.current_view+'_list')
.addClass(new_view+'_list');
self.current_view = new_view;
Cookie.write(self.options.identifier+'_view2', new_view, {duration: 1000});
},
getSavedView: function(){
var self = this;
return Cookie.read(self.options.identifier+'_view2');
},
search: function(){
var self = this;
if(self.search_timer) clearTimeout(self.search_timer);
self.search_timer = (function(){
var search_value = self.navigation_search_input.get('value');
if (search_value == self.last_search_value) return;
self.reset();
self.activateLetter();
self.filter.search = search_value;
self.getMovies(true);
self.last_search_value = search_value;
}).delay(250);
},
update: function(){
var self = this;
self.reset();
self.getMovies(true);
},
getMovies: function(reset){
var self = this;
if(self.scrollspy){
self.scrollspy.stop();
self.load_more.set('text', 'loading...');
}
if(self.movies.length == 0 && self.options.loader){
self.loader_first = new Element('div.loading').adopt(
new Element('div.message', {'text': self.options.title ? 'Loading \'' + self.options.title + '\'' : 'Loading...'})
).inject(self.el, 'top');
createSpinner(self.loader_first, {
radius: 4,
length: 4,
width: 1
});
self.el.setStyle('min-height', 93);
}
Api.request(self.options.api_call || 'media.list', {
'data': Object.merge({
'type': self.options.type || 'movie',
'status': self.options.status,
'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null
}, self.filter),
'onSuccess': function(json){
if(reset)
self.movie_list.empty();
if(self.loader_first){
var lf = self.loader_first;
self.loader_first.addClass('hide');
self.loader_first = null;
setTimeout(function(){
lf.destroy();
}, 20000);
self.el.setStyle('min-height', null);
}
self.store(json.shows);
self.addMovies(json.shows, json.total || json.shows.length);
if(self.scrollspy) {
self.load_more.set('text', 'load more movies');
self.scrollspy.start();
}
self.checkIfEmpty();
self.fireEvent('loaded');
}
});
},
loadMore: function(){
var self = this;
if(self.offset >= self.options.limit)
self.getMovies()
},
store: function(movies){
var self = this;
self.offset += movies.length;
},
checkIfEmpty: function(){
var self = this;
var is_empty = self.movies.length == 0 && (self.total_movies == 0 || self.total_movies === undefined);
if(self.title)
self.title[is_empty ? 'hide' : 'show']();
if(self.description)
self.description.setStyle('display', [is_empty ? 'none' : '']);
if(is_empty && self.options.on_empty_element){
self.options.on_empty_element.inject(self.loader_first || self.title || self.movie_list, 'after');
if(self.navigation)
self.navigation.hide();
self.empty_element = self.options.on_empty_element;
}
else if(self.empty_element){
self.empty_element.destroy();
if(self.navigation)
self.navigation.show();
}
},
toElement: function(){
return this.el;
}
});

View File

@@ -0,0 +1,230 @@
Block.Search.ShowItem = new Class({
Implements: [Options, Events],
initialize: function(info, options){
var self = this;
self.setOptions(options);
self.info = info;
self.alternative_titles = [];
self.create();
},
create: function(){
var self = this,
info = self.info;
self.el = new Element('div.media_result', {
'id': info.id
}).adopt(
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
'src': info.images.poster[0],
'height': null,
'width': null
}) : null,
self.options_el = new Element('div.options.inlay'),
self.data_container = new Element('div.data', {
'events': {
'click': self.showOptions.bind(self)
}
}).adopt(
self.info_container = new Element('div.info').adopt(
new Element('h2').adopt(
self.title = new Element('span.title', {
'text': info.titles && info.titles.length > 0 ? info.titles[0] : 'Unknown'
}),
self.year = info.year ? new Element('span.year', {
'text': info.year
}) : null
)
)
)
)
if(info.titles)
info.titles.each(function(title){
self.alternativeTitle({
'title': title
});
})
},
alternativeTitle: function(alternative){
var self = this;
self.alternative_titles.include(alternative);
},
getTitle: function(){
var self = this;
try {
return self.info.original_title ? self.info.original_title : self.info.titles[0];
}
catch(e){
return 'Unknown';
}
},
get: function(key){
return this.info[key]
},
showOptions: function(){
var self = this;
self.createOptions();
self.data_container.addClass('open');
self.el.addEvent('outerClick', self.closeOptions.bind(self))
},
closeOptions: function(){
var self = this;
self.data_container.removeClass('open');
self.el.removeEvents('outerClick')
},
add: function(e){
var self = this;
if(e)
(e).preventDefault();
self.loadingMask();
Api.request('show.add', {
'data': {
'identifiers': self.info.identifiers,
'type': self.info.type,
'title': self.title_select.get('value'),
'profile_id': self.profile_select.get('value'),
'category_id': self.category_select.get('value')
},
'onComplete': function(json){
self.options_el.empty();
self.options_el.adopt(
new Element('div.message', {
'text': json.success ? 'Show successfully added.' : 'Show didn\'t add properly. Check logs'
})
);
self.mask.fade('out');
self.fireEvent('added');
},
'onFailure': function(){
self.options_el.empty();
self.options_el.adopt(
new Element('div.message', {
'text': 'Something went wrong, check the logs for more info.'
})
);
self.mask.fade('out');
}
});
},
createOptions: function(){
var self = this,
info = self.info;
if(!self.options_el.hasClass('set')){
if(self.info.in_library){
var in_library = [];
self.info.in_library.releases.each(function(release){
in_library.include(release.quality.label)
});
}
self.options_el.grab(
new Element('div', {
'class': self.info.in_wanted && self.info.in_wanted.profile_id || in_library ? 'in_library_wanted' : ''
}).adopt(
self.info.in_wanted && self.info.in_wanted.profile_id ? new Element('span.in_wanted', {
'text': 'Already in wanted list: ' + Quality.getProfile(self.info.in_wanted.profile_id).get('label')
}) : (in_library ? new Element('span.in_library', {
'text': 'Already in library: ' + in_library.join(', ')
}) : null),
self.title_select = new Element('select', {
'name': 'title'
}),
self.profile_select = new Element('select', {
'name': 'profile'
}),
self.category_select = new Element('select', {
'name': 'category'
}).grab(
new Element('option', {'value': -1, 'text': 'None'})
),
self.add_button = new Element('a.button', {
'text': 'Add',
'events': {
'click': self.add.bind(self)
}
})
)
);
Array.each(self.alternative_titles, function(alt){
new Element('option', {
'text': alt.title
}).inject(self.title_select)
})
// Fill categories
var categories = CategoryList.getAll();
if(categories.length == 0)
self.category_select.hide();
else {
self.category_select.show();
categories.each(function(category){
new Element('option', {
'value': category.data._id,
'text': category.data.label
}).inject(self.category_select);
});
}
// Fill profiles
var profiles = Quality.getActiveProfiles();
if(profiles.length == 1)
self.profile_select.hide();
profiles.each(function(profile){
new Element('option', {
'value': profile.get('_id'),
'text': profile.get('label')
}).inject(self.profile_select)
});
self.options_el.addClass('set');
if(categories.length == 0 && self.title_select.getElements('option').length == 1 && profiles.length == 1 &&
!(self.info.in_wanted && self.info.in_wanted.profile_id || in_library))
self.add();
}
},
loadingMask: function(){
var self = this;
self.mask = new Element('div.mask').inject(self.el).fade('hide')
createSpinner(self.mask)
self.mask.fade('in')
},
toElement: function(){
return this.el
}
});

View File

@@ -0,0 +1,127 @@
var Season = new Class({
Extends: BlockBase,
action: {},
initialize: function(show, options, data){
var self = this;
self.setOptions(options);
self.show = show;
self.options = options;
self.data = data;
self.profile = self.show.profile;
self.el = new Element('div.item.season').adopt(
self.detail = new Element('div.item.data')
);
self.create();
},
create: function(){
var self = this;
self.detail.set('id', 'season_'+self.data._id);
self.detail.adopt(
new Element('span.name', {'text': self.getTitle()}),
self.quality = new Element('span.quality', {
'events': {
'click': function(e){
var releases = self.detail.getElement('.item-actions .releases');
if(releases.isVisible())
releases.fireEvent('click', [e])
}
}
}),
self.actions = new Element('div.item-actions')
);
// Add profile
if(self.profile.data) {
self.profile.getTypes().each(function(type){
var q = self.addQuality(type.get('quality'), type.get('3d'));
if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){
q.addClass('finish');
q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.')
}
});
}
// Add releases
self.updateReleases();
Object.each(self.options.actions, function(action, key){
self.action[key.toLowerCase()] = action = new self.options.actions[key](self);
if(action.el)
self.actions.adopt(action)
});
},
updateReleases: function(){
var self = this;
if(!self.data.releases || self.data.releases.length == 0) return;
self.data.releases.each(function(release){
var q = self.quality.getElement('.q_'+ release.quality+(release.is_3d ? '.is_3d' : ':not(.is_3d)')),
status = release.status;
if(!q && (status == 'snatched' || status == 'seeding' || status == 'done'))
q = self.addQuality(release.quality, release.is_3d || false);
if (q && !q.hasClass(status)){
q.addClass(status);
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status)
}
});
},
addQuality: function(quality, is_3d){
var self = this,
q = Quality.getQuality(quality);
return new Element('span', {
'text': q.label + (is_3d ? ' 3D' : ''),
'class': 'q_'+q.identifier + (is_3d ? ' is_3d' : ''),
'title': ''
}).inject(self.quality);
},
getTitle: function(){
var self = this;
var title = '';
if(self.data.info.number) {
title = 'Season ' + self.data.info.number;
} else {
// Season 0 / Specials
title = 'Specials';
}
return title;
},
getIdentifier: function(){
var self = this;
try {
return self.get('identifiers').imdb;
}
catch (e){ }
return self.get('imdb');
},
get: function(attr){
return this.data[attr] || this.data.info[attr]
}
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,92 @@
var Episodes = new Class({
initialize: function(show, options) {
var self = this;
self.show = show;
self.options = options;
},
open: function(){
var self = this;
if(!self.container){
self.container = new Element('div.options').grab(
self.episodes_container = new Element('div.episodes.table')
);
self.container.inject(self.show, 'top');
Api.request('library.tree', {
'data': {
'media_id': self.show.data._id
},
'onComplete': function(json){
self.data = json.result;
self.createEpisodes();
}
});
}
self.show.slide('in', self.container, true);
},
createEpisodes: function() {
var self = this;
self.data.seasons.sort(self.sortSeasons);
self.data.seasons.each(function(season) {
self.createSeason(season);
season.episodes.sort(self.sortEpisodes);
season.episodes.each(function(episode) {
self.createEpisode(episode);
});
});
},
createSeason: function(season) {
var self = this,
s = new Season(self.show, self.options, season);
$(s).inject(self.episodes_container);
},
createEpisode: function(episode){
var self = this,
e = new Episode(self.show, self.options, episode);
$(e).inject(self.episodes_container);
},
sortSeasons: function(a, b) {
// Move "Specials" to the bottom of the list
if(!a.info.number) {
return 1;
}
if(!b.info.number) {
return -1;
}
// Order seasons descending
if(a.info.number < b.info.number)
return -1;
if(a.info.number > b.info.number)
return 1;
return 0;
},
sortEpisodes: function(a, b) {
// Order episodes descending
if(a.info.number < b.info.number)
return -1;
if(a.info.number > b.info.number)
return 1;
return 0;
}
});

View File

@@ -0,0 +1,370 @@
var Show = new Class({
Extends: BlockBase,
action: {},
initialize: function(list, options, data){
var self = this;
self.data = data;
self.view = options.view || 'details';
self.list = list;
self.el = new Element('div.show');
self.episodes = new Episodes(self, {
'actions': [EA.IMDB, EA.Release, EA.Refresh]
});
self.profile = Quality.getProfile(data.profile_id) || {};
self.category = CategoryList.getCategory(data.category_id) || {};
self.parent(self, options);
self.addEvents();
},
addEvents: function(){
var self = this;
self.global_events = {};
// Do refresh with new data
self.global_events['movie.update'] = function(notification){
if(self.data._id != notification.data._id) return;
self.busy(false);
self.removeView();
self.update.delay(2000, self, notification);
};
App.on('movie.update', self.global_events['movie.update']);
// Add spinner on load / search
['media.busy', 'movie.searcher.started'].each(function(listener){
self.global_events[listener] = function(notification){
if(notification.data && (self.data._id == notification.data._id || (typeOf(notification.data._id) == 'array' && notification.data._id.indexOf(self.data._id) > -1)))
self.busy(true);
};
App.on(listener, self.global_events[listener]);
});
// Remove spinner
self.global_events['movie.searcher.ended'] = function(notification){
if(notification.data && self.data._id == notification.data._id)
self.busy(false)
};
App.on('movie.searcher.ended', self.global_events['movie.searcher.ended']);
// Reload when releases have updated
self.global_events['release.update_status'] = function(notification){
var data = notification.data;
if(data && self.data._id == data.movie_id){
if(!self.data.releases)
self.data.releases = [];
self.data.releases.push({'quality': data.quality, 'status': data.status});
self.updateReleases();
}
};
App.on('release.update_status', self.global_events['release.update_status']);
},
destroy: function(){
var self = this;
self.el.destroy();
delete self.list.movies_added[self.get('id')];
self.list.movies.erase(self);
self.list.checkIfEmpty();
// Remove events
Object.each(self.global_events, function(handle, listener){
App.off(listener, handle);
});
},
busy: function(set_busy, timeout){
var self = this;
if(!set_busy){
setTimeout(function(){
if(self.spinner){
self.mask.fade('out');
setTimeout(function(){
if(self.mask)
self.mask.destroy();
if(self.spinner)
self.spinner.el.destroy();
self.spinner = null;
self.mask = null;
}, timeout || 400);
}
}, timeout || 1000)
}
else if(!self.spinner) {
self.createMask();
self.spinner = createSpinner(self.mask);
self.mask.fade('in');
}
},
createMask: function(){
var self = this;
self.mask = new Element('div.mask', {
'styles': {
'z-index': 4
}
}).inject(self.el, 'top').fade('hide');
},
update: function(notification){
var self = this;
self.data = notification.data;
self.el.empty();
self.removeView();
self.profile = Quality.getProfile(self.data.profile_id) || {};
self.category = CategoryList.getCategory(self.data.category_id) || {};
self.create();
self.busy(false);
},
create: function(){
var self = this;
self.el.addClass('status_'+self.get('status'));
var eta = null,
eta_date = null,
now = Math.round(+new Date()/1000);
if(self.data.info.release_date)
[self.data.info.release_date.dvd, self.data.info.release_date.theater].each(function(timestamp){
if (timestamp > 0 && (eta == null || Math.abs(timestamp - now) < Math.abs(eta - now)))
eta = timestamp;
});
if(eta){
eta_date = new Date(eta * 1000);
eta_date = eta_date.toLocaleString('en-us', { month: "long" }) + ' ' + eta_date.getFullYear();
}
self.el.adopt(
self.select_checkbox = new Element('input[type=checkbox].inlay', {
'events': {
'change': function(){
self.fireEvent('select')
}
}
}),
self.thumbnail = (self.data.files && self.data.files.image_poster) ? new Element('img', {
'class': 'type_image poster',
'src': Api.createUrl('file.cache') + self.data.files.image_poster[0].split(Api.getOption('path_sep')).pop()
}): null,
self.data_container = new Element('div.data.inlay.light').adopt(
self.info_container = new Element('div.info').adopt(
new Element('div.title').adopt(
self.title = new Element('a', {
'events': {
'click': function(e){
self.episodes.open();
}
},
'text': self.getTitle() || 'n/a'
}),
self.year = new Element('div.year', {
'text': self.data.info.year || 'n/a'
})
),
self.description = new Element('div.description.tiny_scroll', {
'text': self.data.info.plot
}),
self.eta = eta_date && (now+8035200 > eta) ? new Element('div.eta', {
'text': eta_date,
'title': 'ETA'
}) : null,
self.quality = new Element('div.quality', {
'events': {
'click': function(e){
var releases = self.el.getElement('.actions .releases');
if(releases.isVisible())
releases.fireEvent('click', [e])
}
}
})
),
self.actions = new Element('div.actions')
)
);
if(!self.thumbnail)
self.el.addClass('no_thumbnail');
//self.changeView(self.view);
self.select_checkbox_class = new Form.Check(self.select_checkbox);
// Add profile
if(self.profile.data)
self.profile.getTypes().each(function(type){
var q = self.addQuality(type.get('quality'), type.get('3d'));
if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){
q.addClass('finish');
q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.')
}
});
// Add releases
self.updateReleases();
Object.each(self.options.actions, function(action, key){
self.action[key.toLowerCase()] = action = new self.options.actions[key](self);
if(action.el)
self.actions.adopt(action)
});
},
updateReleases: function(){
var self = this;
if(!self.data.releases || self.data.releases.length == 0) return;
self.data.releases.each(function(release){
var q = self.quality.getElement('.q_'+ release.quality+(release.is_3d ? '.is_3d' : ':not(.is_3d)')),
status = release.status;
if(!q && (status == 'snatched' || status == 'seeding' || status == 'done'))
q = self.addQuality(release.quality, release.is_3d || false);
if (q && !q.hasClass(status)){
q.addClass(status);
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status)
}
});
},
addQuality: function(quality, is_3d){
var self = this;
var q = Quality.getQuality(quality);
return new Element('span', {
'text': q.label + (is_3d ? ' 3D' : ''),
'class': 'q_'+q.identifier + (is_3d ? ' is_3d' : ''),
'title': ''
}).inject(self.quality);
},
getTitle: function(){
var self = this;
if(self.data.title)
return self.getUnprefixedTitle(self.data.title);
else if(self.data.info.titles.length > 0)
return self.getUnprefixedTitle(self.data.info.titles[0]);
return 'Unknown movie'
},
getUnprefixedTitle: function(t){
if(t.substr(0, 4).toLowerCase() == 'the ')
t = t.substr(4) + ', The';
else if(t.substr(0, 3).toLowerCase() == 'an ')
t = t.substr(3) + ', An';
else if(t.substr(0, 2).toLowerCase() == 'a ')
t = t.substr(2) + ', A';
return t;
},
slide: function(direction, el, expand){
var self = this;
if(direction == 'in'){
self.temp_view = self.view;
self.changeView('details');
self.el.addEvent('outerClick', function(){
self.removeView();
self.slide('out')
});
el.show();
if(expand === true) {
self.el.addClass('expanded');
self.el.getElements('.table').addClass('expanded');
}
self.data_container.addClass('hide_right');
}
else {
self.el.removeEvents('outerClick');
setTimeout(function(){
if(self.el)
{
self.el.getElements('> :not(.data):not(.poster):not(.movie_container)').hide();
self.el.getElements('.table').removeClass('expanded');
}
}, 600);
self.el.removeClass('expanded');
self.data_container.removeClass('hide_right');
}
},
changeView: function(new_view){
var self = this;
if(self.el)
self.el
.removeClass(self.view+'_view')
.addClass(new_view+'_view');
self.view = new_view;
},
removeView: function(){
var self = this;
self.el.removeClass(self.view+'_view')
},
getIdentifier: function(){
var self = this;
try {
return self.get('identifiers').imdb;
}
catch (e){ }
return self.get('imdb');
},
get: function(attr){
return this.data[attr] || this.data.info[attr]
},
select: function(bool){
var self = this;
self.select_checkbox_class[bool ? 'check' : 'uncheck']()
},
isSelected: function(){
return this.select_checkbox.get('checked');
},
toElement: function(){
return this.el;
}
});

View File

@@ -0,0 +1,71 @@
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.library.base import LibraryBase
log = CPLog(__name__)
autoload = 'EpisodeLibraryPlugin'
class EpisodeLibraryPlugin(LibraryBase):
def __init__(self):
addEvent('library.query', self.query)
addEvent('library.identifier', self.identifier)
def query(self, media, first = True, condense = True, include_identifier = True, **kwargs):
if media.get('type') != 'show.episode':
return
related = fireEvent('library.related', media, single = True)
# Get season titles
titles = fireEvent(
'library.query', related['season'],
first = False,
include_identifier = include_identifier,
condense = condense,
single = True
)
# Add episode identifier to titles
if include_identifier:
identifier = fireEvent('library.identifier', media, single = True)
if identifier and identifier.get('episode'):
titles = [title + ('E%02d' % identifier['episode']) for title in titles]
if first:
return titles[0] if titles else None
return titles
def identifier(self, media):
if media.get('type') != 'show.episode':
return
identifier = {
'season': None,
'episode': None
}
# TODO identifier mapping
# scene_map = media['info'].get('map_episode', {}).get('scene')
# if scene_map:
# # Use scene mappings if they are available
# identifier['season'] = scene_map.get('season_nr')
# identifier['episode'] = scene_map.get('episode_nr')
# else:
# Fallback to normal season/episode numbers
identifier['season'] = media['info'].get('season_number')
identifier['episode'] = media['info'].get('number')
# Cast identifiers to integers
# TODO this will need changing to support identifiers with trailing 'a', 'b' characters
identifier['season'] = tryInt(identifier['season'], None)
identifier['episode'] = tryInt(identifier['episode'], None)
return identifier

View File

@@ -0,0 +1,52 @@
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.library.base import LibraryBase
log = CPLog(__name__)
autoload = 'SeasonLibraryPlugin'
class SeasonLibraryPlugin(LibraryBase):
def __init__(self):
addEvent('library.query', self.query)
addEvent('library.identifier', self.identifier)
def query(self, media, first = True, condense = True, include_identifier = True, **kwargs):
if media.get('type') != 'show.season':
return
related = fireEvent('library.related', media, single = True)
# Get show titles
titles = fireEvent(
'library.query', related['show'],
first = False,
condense = condense,
single = True
)
# TODO map_names
# Add season identifier to titles
if include_identifier:
identifier = fireEvent('library.identifier', media, single = True)
if identifier and identifier.get('season') is not None:
titles = [title + (' S%02d' % identifier['season']) for title in titles]
if first:
return titles[0] if titles else None
return titles
def identifier(self, media):
if media.get('type') != 'show.season':
return
return {
'season': tryInt(media['info']['number'], None)
}

View File

@@ -0,0 +1,38 @@
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import simplifyString
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.library.base import LibraryBase
from qcond import QueryCondenser
log = CPLog(__name__)
autoload = 'ShowLibraryPlugin'
class ShowLibraryPlugin(LibraryBase):
query_condenser = QueryCondenser()
def __init__(self):
addEvent('library.query', self.query)
def query(self, media, first = True, condense = True, include_identifier = True, **kwargs):
if media.get('type') != 'show':
return
titles = media['info']['titles']
if condense:
# Use QueryCondenser to build a list of optimal search titles
condensed_titles = self.query_condenser.distinct(titles)
if condensed_titles:
# Use condensed titles if we got a valid result
titles = condensed_titles
else:
# Fallback to simplifying titles
titles = [simplifyString(title) for title in titles]
if first:
return titles[0] if titles else None
return titles

View File

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

View File

@@ -0,0 +1,72 @@
from couchpotato import fireEvent, CPLog, tryInt
from couchpotato.core.event import addEvent
from couchpotato.core.media._base.matcher.base import MatcherBase
log = CPLog(__name__)
class Base(MatcherBase):
# TODO come back to this later, think this could be handled better, this is starting to get out of hand....
quality_map = {
'bluray_1080p': {'resolution': ['1080p'], 'source': ['bluray']},
'bluray_720p': {'resolution': ['720p'], 'source': ['bluray']},
'bdrip_1080p': {'resolution': ['1080p'], 'source': ['BDRip']},
'bdrip_720p': {'resolution': ['720p'], 'source': ['BDRip']},
'brrip_1080p': {'resolution': ['1080p'], 'source': ['BRRip']},
'brrip_720p': {'resolution': ['720p'], 'source': ['BRRip']},
'webdl_1080p': {'resolution': ['1080p'], 'source': ['webdl', ['web', 'dl']]},
'webdl_720p': {'resolution': ['720p'], 'source': ['webdl', ['web', 'dl']]},
'webdl_480p': {'resolution': ['480p'], 'source': ['webdl', ['web', 'dl']]},
'hdtv_720p': {'resolution': ['720p'], 'source': ['hdtv']},
'hdtv_sd': {'resolution': ['480p', None], 'source': ['hdtv']},
}
def __init__(self):
super(Base, self).__init__()
addEvent('%s.matcher.correct_identifier' % self.type, self.correctIdentifier)
def correct(self, chain, release, media, quality):
log.info("Checking if '%s' is valid", release['name'])
log.info2('Release parsed as: %s', chain.info)
if not fireEvent('matcher.correct_quality', chain, quality, self.quality_map, single = True):
log.info('Wrong: %s, quality does not match', release['name'])
return False
if not fireEvent('%s.matcher.correct_identifier' % self.type, chain, media):
log.info('Wrong: %s, identifier does not match', release['name'])
return False
if not fireEvent('matcher.correct_title', chain, media):
log.info("Wrong: '%s', undetermined naming.", (' '.join(chain.info['show_name'])))
return False
return True
def correctIdentifier(self, chain, media):
raise NotImplementedError()
def getChainIdentifier(self, chain):
if 'identifier' not in chain.info:
return None
identifier = self.flattenInfo(chain.info['identifier'])
# Try cast values to integers
for key, value in identifier.items():
if isinstance(value, list):
if len(value) <= 1:
value = value[0]
else:
log.warning('Wrong: identifier contains multiple season or episode values, unsupported')
return None
identifier[key] = tryInt(value, value)
return identifier

View File

@@ -0,0 +1,30 @@
from couchpotato import fireEvent, CPLog
from couchpotato.core.media.show.matcher.base import Base
log = CPLog(__name__)
class Episode(Base):
type = 'show.episode'
def correctIdentifier(self, chain, media):
identifier = self.getChainIdentifier(chain)
if not identifier:
log.info2('Wrong: release identifier is not valid (unsupported or missing identifier)')
return False
# TODO - Parse episode ranges from identifier to determine if they are multi-part episodes
if any([x in identifier for x in ['episode_from', 'episode_to']]):
log.info2('Wrong: releases with identifier ranges are not supported yet')
return False
required = fireEvent('library.identifier', media, single = True)
# TODO - Support air by date episodes
# TODO - Support episode parts
if identifier != required:
log.info2('Wrong: required identifier (%s) does not match release identifier (%s)', (required, identifier))
return False
return True

View File

@@ -0,0 +1,9 @@
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.matcher.episode import Episode
from couchpotato.core.media.show.matcher.season import Season
class ShowMatcher(MultiProvider):
def getTypes(self):
return [Season, Episode]

View File

@@ -0,0 +1,27 @@
from couchpotato import fireEvent, CPLog
from couchpotato.core.media.show.matcher.base import Base
log = CPLog(__name__)
class Season(Base):
type = 'show.season'
def correctIdentifier(self, chain, media):
identifier = self.getChainIdentifier(chain)
if not identifier:
log.info2('Wrong: release identifier is not valid (unsupported or missing identifier)')
return False
# TODO - Parse episode ranges from identifier to determine if they are season packs
if any([x in identifier for x in ['episode_from', 'episode_to']]):
log.info2('Wrong: releases with identifier ranges are not supported yet')
return False
required = fireEvent('library.identifier', media, single = True)
if identifier != required:
log.info2('Wrong: required identifier (%s) does not match release identifier (%s)', (required, identifier))
return False
return True

View File

@@ -0,0 +1,13 @@
from couchpotato.core.media._base.providers.info.base import BaseInfoProvider
class ShowProvider(BaseInfoProvider):
type = 'show'
class SeasonProvider(BaseInfoProvider):
type = 'show.season'
class EpisodeProvider(BaseInfoProvider):
type = 'show.episode'

View File

@@ -0,0 +1,372 @@
from datetime import datetime
import os
import traceback
from couchpotato import Env
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
from couchpotato.core.helpers.variable import splitString, tryInt, tryFloat
from couchpotato.core.logger import CPLog
from couchpotato.core.media.show.providers.base import ShowProvider
from tvdb_api import tvdb_exceptions
from tvdb_api.tvdb_api import Tvdb, Show
log = CPLog(__name__)
autoload = 'TheTVDb'
class TheTVDb(ShowProvider):
# TODO: Consider grabbing zips to put less strain on tvdb
# TODO: Unicode stuff (check)
# TODO: Notigy frontend on error (tvdb down at monent)
# TODO: Expose apikey in setting so it can be changed by user
def __init__(self):
addEvent('show.info', self.getShowInfo, priority = 1)
addEvent('season.info', self.getSeasonInfo, priority = 1)
addEvent('episode.info', self.getEpisodeInfo, priority = 1)
self.tvdb_api_parms = {
'apikey': self.conf('api_key'),
'banners': True,
'language': 'en',
'cache': os.path.join(Env.get('cache_dir'), 'thetvdb_api'),
}
self._setup()
def _setup(self):
self.tvdb = Tvdb(**self.tvdb_api_parms)
self.valid_languages = self.tvdb.config['valid_languages']
def getShow(self, identifier = None):
show = None
try:
log.debug('Getting show: %s', identifier)
show = self.tvdb[int(identifier)]
except (tvdb_exceptions.tvdb_error, IOError), e:
log.error('Failed to getShowInfo for show id "%s": %s', (identifier, traceback.format_exc()))
return None
return show
def getShowInfo(self, identifiers = None):
"""
@param identifiers: dict with identifiers per provider
@return: Full show info including season and episode info
"""
if not identifiers or not identifiers.get('thetvdb'):
return None
identifier = tryInt(identifiers.get('thetvdb'))
cache_key = 'thetvdb.cache.show.%s' % identifier
result = None #self.getCache(cache_key)
if result:
return result
show = self.getShow(identifier = identifier)
if show:
result = self._parseShow(show)
self.setCache(cache_key, result)
return result or {}
def getSeasonInfo(self, identifiers = None, params = {}):
"""Either return a list of all seasons or a single season by number.
identifier is the show 'id'
"""
if not identifiers or not identifiers.get('thetvdb'):
return None
season_number = params.get('season_number', None)
identifier = tryInt(identifiers.get('thetvdb'))
cache_key = 'thetvdb.cache.%s.%s' % (identifier, season_number)
log.debug('Getting SeasonInfo: %s', cache_key)
result = self.getCache(cache_key) or {}
if result:
return result
try:
show = self.tvdb[int(identifier)]
except (tvdb_exceptions.tvdb_error, IOError), e:
log.error('Failed parsing TheTVDB SeasonInfo for "%s" id "%s": %s', (show, identifier, traceback.format_exc()))
return False
result = []
for number, season in show.items():
if season_number is not None and number == season_number:
result = self._parseSeason(show, number, season)
self.setCache(cache_key, result)
return result
else:
result.append(self._parseSeason(show, number, season))
self.setCache(cache_key, result)
return result
def getEpisodeInfo(self, identifier = None, params = {}):
"""Either return a list of all episodes or a single episode.
If episode_identifer contains an episode number to search for
"""
season_number = self.getIdentifier(params.get('season_number', None))
episode_identifier = self.getIdentifier(params.get('episode_identifiers', None))
identifier = self.getIdentifier(identifier)
if not identifier and season_number is None:
return False
# season_identifier must contain the 'show id : season number' since there is no tvdb id
# for season and we need a reference to both the show id and season number
if not identifier and season_number:
try:
identifier, season_number = season_number.split(':')
season_number = int(season_number)
except: return None
cache_key = 'thetvdb.cache.%s.%s.%s' % (identifier, episode_identifier, season_number)
log.debug('Getting EpisodeInfo: %s', cache_key)
result = self.getCache(cache_key) or {}
if result:
return result
try:
show = self.tvdb[int(identifier)]
except (tvdb_exceptions.tvdb_error, IOError), e:
log.error('Failed parsing TheTVDB EpisodeInfo for "%s" id "%s": %s', (show, identifier, traceback.format_exc()))
return False
result = []
for number, season in show.items():
if season_number is not None and number != season_number:
continue
for episode in season.values():
if episode_identifier is not None and episode['id'] == toUnicode(episode_identifier):
result = self._parseEpisode(episode)
self.setCache(cache_key, result)
return result
else:
result.append(self._parseEpisode(episode))
self.setCache(cache_key, result)
return result
def getIdentifier(self, value):
if type(value) is dict:
return value.get('thetvdb')
return value
def _parseShow(self, show):
#
# NOTE: show object only allows direct access via
# show['id'], not show.get('id')
#
def get(name):
return show.get(name) if not hasattr(show, 'search') else show[name]
## Images
poster = get('poster')
backdrop = get('fanart')
genres = splitString(get('genre'), '|')
if get('firstaired') is not None:
try: year = datetime.strptime(get('firstaired'), '%Y-%m-%d').year
except: year = None
else:
year = None
show_data = {
'identifiers': {
'thetvdb': tryInt(get('id')),
'imdb': get('imdb_id'),
'zap2it': get('zap2it_id'),
},
'type': 'show',
'titles': [get('seriesname')],
'images': {
'poster': [poster] if poster else [],
'backdrop': [backdrop] if backdrop else [],
'poster_original': [],
'backdrop_original': [],
},
'year': year,
'genres': genres,
'network': get('network'),
'plot': get('overview'),
'networkid': get('networkid'),
'air_day': (get('airs_dayofweek') or '').lower(),
'air_time': self.parseTime(get('airs_time')),
'firstaired': get('firstaired'),
'runtime': tryInt(get('runtime')),
'contentrating': get('contentrating'),
'rating': {},
'actors': splitString(get('actors'), '|'),
'status': get('status'),
'language': get('language'),
}
if tryFloat(get('rating')):
show_data['rating']['thetvdb'] = [tryFloat(get('rating')), tryInt(get('ratingcount'))],
show_data = dict((k, v) for k, v in show_data.iteritems() if v)
# Only load season info when available
if type(show) == Show:
# Parse season and episode data
show_data['seasons'] = {}
for season_nr in show:
season = self._parseSeason(show, season_nr, show[season_nr])
season['episodes'] = {}
for episode_nr in show[season_nr]:
season['episodes'][episode_nr] = self._parseEpisode(show[season_nr][episode_nr])
show_data['seasons'][season_nr] = season
# Add alternative titles
# try:
# raw = self.tvdb.search(show['seriesname'])
# if raw:
# for show_info in raw:
# print show_info
# if show_info['id'] == show_data['id'] and show_info.get('aliasnames', None):
# for alt_name in show_info['aliasnames'].split('|'):
# show_data['titles'].append(toUnicode(alt_name))
# except (tvdb_exceptions.tvdb_error, IOError), e:
# log.error('Failed searching TheTVDB for "%s": %s', (show['seriesname'], traceback.format_exc()))
return show_data
def _parseSeason(self, show, number, season):
"""
contains no data
"""
poster = []
try:
temp_poster = {}
for id, data in show.data['_banners']['season']['season'].items():
if data.get('season') == str(number) and data.get('language') == self.tvdb_api_parms['language']:
temp_poster[tryFloat(data.get('rating')) * tryInt(data.get('ratingcount'))] = data.get('_bannerpath')
#break
poster.append(temp_poster[sorted(temp_poster, reverse = True)[0]])
except:
pass
season_data = {
'identifiers': {
'thetvdb': show['id'] if show.get('id') else show[number][1]['seasonid']
},
'number': tryInt(number),
'images': {
'poster': poster,
},
}
season_data = dict((k, v) for k, v in season_data.iteritems() if v)
return season_data
def _parseEpisode(self, episode):
"""
('episodenumber', u'1'),
('thumb_added', None),
('rating', u'7.7'),
('overview',
u'Experienced waitress Max Black meets her new co-worker, former rich-girl Caroline Channing, and puts her skills to the test at an old but re-emerging Brooklyn diner. Despite her initial distaste for Caroline, Max eventually softens and the two team up for a new business venture.'),
('dvd_episodenumber', None),
('dvd_discid', None),
('combined_episodenumber', u'1'),
('epimgflag', u'7'),
('id', u'4099506'),
('seasonid', u'465948'),
('thumb_height', u'225'),
('tms_export', u'1374789754'),
('seasonnumber', u'1'),
('writer', u'|Michael Patrick King|Whitney Cummings|'),
('lastupdated', u'1371420338'),
('filename', u'http://thetvdb.com/banners/episodes/248741/4099506.jpg'),
('absolute_number', u'1'),
('ratingcount', u'102'),
('combined_season', u'1'),
('thumb_width', u'400'),
('imdb_id', u'tt1980319'),
('director', u'James Burrows'),
('dvd_chapter', None),
('dvd_season', None),
('gueststars',
u'|Brooke Lyons|Noah Mills|Shoshana Bush|Cale Hartmann|Adam Korson|Alex Enriquez|Matt Cook|Bill Parks|Eugene Shaw|Sergey Brusilovsky|Greg Lewis|Cocoa Brown|Nick Jameson|'),
('seriesid', u'248741'),
('language', u'en'),
('productioncode', u'296793'),
('firstaired', u'2011-09-19'),
('episodename', u'Pilot')]
"""
def get(name, default = None):
return episode.get(name, default)
poster = get('filename', [])
episode_data = {
'number': tryInt(get('episodenumber')),
'absolute_number': tryInt(get('absolute_number')),
'identifiers': {
'thetvdb': tryInt(episode['id'])
},
'type': 'episode',
'titles': [get('episodename')] if get('episodename') else [],
'images': {
'poster': [poster] if poster else [],
},
'released': get('firstaired'),
'plot': get('overview'),
'firstaired': get('firstaired'),
'language': get('language'),
}
if get('imdb_id'):
episode_data['identifiers']['imdb'] = get('imdb_id')
episode_data = dict((k, v) for k, v in episode_data.iteritems() if v)
return episode_data
def parseTime(self, time):
return time
def isDisabled(self):
if self.conf('api_key') == '':
log.error('No API key provided.')
return True
else:
return False
config = [{
'name': 'thetvdb',
'groups': [
{
'tab': 'providers',
'name': 'tmdb',
'label': 'TheTVDB',
'hidden': True,
'description': 'Used for all calls to TheTVDB.',
'options': [
{
'name': 'api_key',
'default': '7966C02F860586D2',
'label': 'Api Key',
},
],
},
],
}]

View File

@@ -0,0 +1,86 @@
import urllib
from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.media.show.providers.base import ShowProvider
log = CPLog(__name__)
autoload = 'Trakt'
class Trakt(ShowProvider):
api_key = 'c043de5ada9d180028c10229d2a3ea5b'
base_url = 'http://api.trakt.tv/%%s.json/%s' % api_key
def __init__(self):
addEvent('info.search', self.search, priority = 1)
addEvent('show.search', self.search, priority = 1)
def search(self, q, limit = 12):
if self.isDisabled():
return False
# Check for cached result
cache_key = 'trakt.cache.search.%s.%s' % (q, limit)
results = self.getCache(cache_key) or []
if results:
return results
# Search
log.debug('Searching for show: "%s"', q)
response = self._request('search/shows', query=q, limit=limit)
if not response:
return []
# Parse search results
for show in response:
results.append(self._parseShow(show))
log.info('Found: %s', [result['titles'][0] + ' (' + str(result.get('year', 0)) + ')' for result in results])
self.setCache(cache_key, results)
return results
def _request(self, action, **kwargs):
url = self.base_url % action
if kwargs:
url += '?' + urllib.urlencode(kwargs)
return self.getJsonData(url)
def _parseShow(self, show):
# Images
images = show.get('images', {})
poster = images.get('poster')
backdrop = images.get('backdrop')
# Rating
rating = show.get('ratings', {}).get('percentage')
# Build show dict
show_data = {
'identifiers': {
'thetvdb': show.get('tvdb_id'),
'imdb': show.get('imdb_id'),
'tvrage': show.get('tvrage_id'),
},
'type': 'show',
'titles': [show.get('title')],
'images': {
'poster': [poster] if poster else [],
'backdrop': [backdrop] if backdrop else [],
'poster_original': [],
'backdrop_original': [],
},
'year': show.get('year'),
'rating': {
'trakt': float(rating) / 10
},
}
return dict((k, v) for k, v in show_data.iteritems() if v)

View File

@@ -0,0 +1,216 @@
from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.media.show.providers.base import ShowProvider
log = CPLog(__name__)
autoload = 'Xem'
class Xem(ShowProvider):
'''
Mapping Information
===================
Single
------
You will need the id / identifier of the show e.g. tvdb-id for American Dad! is 73141
the origin is the name of the site/entity the episode, season (and/or absolute) numbers are based on
http://thexem.de/map/single?id=&origin=&episode=&season=&absolute=
episode, season and absolute are all optional but it wont work if you don't provide either episode and season OR absolute in
addition you can provide destination as the name of the wished destination, if not provided it will output all available
When a destination has two or more addresses another entry will be added as _ ... for now the second address gets the index "2"
(the first index is omitted) and so on
http://thexem.de/map/single?id=7529&origin=anidb&season=1&episode=2&destination=trakt
{
"result":"success",
"data":{
"trakt": {"season":1,"episode":3,"absolute":3},
"trakt_2":{"season":1,"episode":4,"absolute":4}
},
"message":"single mapping for 7529 on anidb."
}
All
---
Basically same as "single" just a little easier
The origin address is added into the output too!!
http://thexem.de/map/all?id=7529&origin=anidb
All Names
---------
Get all names xem has to offer
non optional params: origin(an entity string like 'tvdb')
optional params: season, language
- season: a season number or a list like: 1,3,5 or a compare operator like ne,gt,ge,lt,le,eq and a season number. default would
return all
- language: a language string like 'us' or 'jp' default is all
- defaultNames: 1(yes) or 0(no) should the default names be added to the list ? default is 0(no)
http://thexem.de/map/allNames?origin=tvdb&season=le1
{
"result": "success",
"data": {
"248812": ["Dont Trust the Bitch in Apartment 23", "Don't Trust the Bitch in Apartment 23"],
"257571": ["Nazo no Kanojo X"],
"257875": ["Lupin III - Mine Fujiko to Iu Onna", "Lupin III Fujiko to Iu Onna", "Lupin the Third - Mine Fujiko to Iu Onna"]
},
"message": ""
}
'''
def __init__(self):
addEvent('show.info', self.getShowInfo, priority = 5)
addEvent('episode.info', self.getEpisodeInfo, priority = 5)
self.config = {}
self.config['base_url'] = "http://thexem.de"
self.config['url_single'] = u"%(base_url)s/map/single?" % self.config
self.config['url_all'] = u"%(base_url)s/map/all?" % self.config
self.config['url_names'] = u"%(base_url)s/map/names?" % self.config
self.config['url_all_names'] = u"%(base_url)s/map/allNames?" % self.config
def getShowInfo(self, identifiers = None):
if self.isDisabled():
return {}
identifier = identifiers.get('thetvdb')
if not identifier:
return {}
cache_key = 'xem.cache.%s' % identifier
log.debug('Getting showInfo: %s', cache_key)
result = self.getCache(cache_key) or {}
if result:
return result
result['seasons'] = {}
# Create season/episode and absolute mappings
url = self.config['url_all'] + "id=%s&origin=tvdb" % tryUrlencode(identifier)
response = self.getJsonData(url)
if response and response.get('result') == 'success':
data = response.get('data', None)
self.parseMaps(result, data)
# Create name alias mappings
url = self.config['url_names'] + "id=%s&origin=tvdb" % tryUrlencode(identifier)
response = self.getJsonData(url)
if response and response.get('result') == 'success':
data = response.get('data', None)
self.parseNames(result, data)
self.setCache(cache_key, result)
return result
def getEpisodeInfo(self, identifiers = None, params = {}):
episode_num = params.get('episode_number', None)
if episode_num is None:
return False
season_num = params.get('season_number', None)
if season_num is None:
return False
result = self.getShowInfo(identifiers)
if not result:
return False
# Find season
if season_num not in result['seasons']:
return False
season = result['seasons'][season_num]
# Find episode
if episode_num not in season['episodes']:
return False
return season['episodes'][episode_num]
def parseMaps(self, result, data, master = 'tvdb'):
'''parses xem map and returns a custom formatted dict map
To retreive map for scene:
if 'scene' in map['map_episode'][1][1]:
print map['map_episode'][1][1]['scene']['season']
'''
if not isinstance(data, list):
return
for episode_map in data:
origin = episode_map.pop(master, None)
if origin is None:
continue # No master origin to map to
o_season = origin['season']
o_episode = origin['episode']
# Create season info
if o_season not in result['seasons']:
result['seasons'][o_season] = {}
season = result['seasons'][o_season]
if 'episodes' not in season:
season['episodes'] = {}
# Create episode info
if o_episode not in season['episodes']:
season['episodes'][o_episode] = {}
episode = season['episodes'][o_episode]
episode['episode_map'] = episode_map
def parseNames(self, result, data):
result['title_map'] = data.pop('all', None)
for season, title_map in data.items():
season = int(season)
# Create season info
if season not in result['seasons']:
result['seasons'][season] = {}
season = result['seasons'][season]
season['title_map'] = title_map
def isDisabled(self):
if __name__ == '__main__':
return False
if self.conf('enabled'):
return False
else:
return True
config = [{
'name': 'xem',
'groups': [
{
'tab': 'providers',
'name': 'xem',
'label': 'TheXem',
'hidden': True,
'description': 'Used for all calls to TheXem.',
'options': [
{
'name': 'enabled',
'default': True,
'label': 'Enabled',
},
],
},
],
}]

View File

@@ -0,0 +1,51 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media._base.providers.nzb.binsearch import Base
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.environment import Env
log = CPLog(__name__)
autoload = 'BinSearch'
class BinSearch(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
def buildUrl(self, media, quality):
query = tryUrlencode({
'q': fireEvent('media.search_query', media, single = True),
'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'),
})
return query
class Episode(EpisodeProvider, Base):
def buildUrl(self, media, quality):
query = tryUrlencode({
'q': fireEvent('media.search_query', media, single = True),
'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'),
})
return query

View File

@@ -0,0 +1,49 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.event import fireEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media._base.providers.nzb.newznab import Base
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
log = CPLog(__name__)
autoload = 'Newznab'
class Newznab(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
def buildUrl(self, media, host):
related = fireEvent('library.related', media, single = True)
identifier = fireEvent('library.identifier', media, single = True)
query = tryUrlencode({
't': 'tvsearch',
'apikey': host['api_key'],
'q': related['show']['title'],
'season': identifier['season'],
'extended': 1
})
return query
class Episode(EpisodeProvider, Base):
def buildUrl(self, media, host):
related = fireEvent('library.related', media, single = True)
identifier = fireEvent('library.identifier', media, single = True)
query = tryUrlencode({
't': 'tvsearch',
'apikey': host['api_key'],
'q': related['show']['title'],
'season': identifier['season'],
'ep': identifier['episode'],
'extended': 1
})
return query

View File

@@ -0,0 +1,52 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.core.media._base.providers.nzb.nzbclub import Base
log = CPLog(__name__)
autoload = 'NZBClub'
class NZBClub(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
def buildUrl(self, media):
q = tryUrlencode({
'q': fireEvent('media.search_query', media, single = True),
})
query = tryUrlencode({
'ig': 1,
'rpp': 200,
'st': 5,
'sp': 1,
'ns': 1,
})
return '%s&%s' % (q, query)
class Episode(EpisodeProvider, Base):
def buildUrl(self, media):
q = tryUrlencode({
'q': fireEvent('media.search_query', media, single = True),
})
query = tryUrlencode({
'ig': 1,
'rpp': 200,
'st': 5,
'sp': 1,
'ns': 1,
})
return '%s&%s' % (q, query)

View File

@@ -0,0 +1,51 @@
from couchpotato import Env
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.core.media._base.providers.nzb.nzbindex import Base
log = CPLog(__name__)
autoload = 'NzbIndex'
class NzbIndex(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
def buildUrl(self, media, quality):
query = tryUrlencode({
'q': fireEvent('media.search_query', media, single = True),
'age': Env.setting('retention', 'nzb'),
'sort': 'agedesc',
'minsize': quality.get('size_min'),
'maxsize': quality.get('size_max'),
'rating': 1,
'max': 250,
'more': 1,
'complete': 1,
})
return query
class Episode(EpisodeProvider, Base):
def buildUrl(self, media, quality):
query = tryUrlencode({
'q': fireEvent('media.search_query', media, single = True),
'age': Env.setting('retention', 'nzb'),
'sort': 'agedesc',
'minsize': quality.get('size_min'),
'maxsize': quality.get('size_max'),
'rating': 1,
'max': 250,
'more': 1,
'complete': 1,
})
return query

View File

@@ -0,0 +1,36 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.core.media._base.providers.torrent.bithdtv import Base
log = CPLog(__name__)
autoload = 'BiTHDTV'
class BiTHDTV(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
def buildUrl(self, media):
query = tryUrlencode({
'search': fireEvent('media.search_query', media, single = True),
'cat': 12 # Season cat
})
return query
class Episode(EpisodeProvider, Base):
def buildUrl(self, media):
query = tryUrlencode({
'search': fireEvent('media.search_query', media, single = True),
'cat': 10 # Episode cat
})
return query

View File

@@ -0,0 +1,41 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.core.media._base.providers.torrent.bitsoup import Base
log = CPLog(__name__)
autoload = 'Bitsoup'
class Bitsoup(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
# For season bundles, bitsoup currently only has one category
def buildUrl(self, media, quality):
query = tryUrlencode({
'search': fireEvent('media.search_query', media, single = True),
'cat': 45 # TV-Packs Category
})
return query
class Episode(EpisodeProvider, Base):
cat_ids = [
([42], ['hdtv_720p', 'webdl_720p', 'webdl_1080p', 'bdrip_1080p', 'bdrip_720p', 'brrip_1080p', 'brrip_720p']),
([49], ['hdtv_sd', 'webdl_480p'])
]
cat_backup_id = 0
def buildUrl(self, media, quality):
query = tryUrlencode({
'search': fireEvent('media.search_query', media, single = True),
'cat': self.getCatId(quality['identifier'])[0],
})
return query

View File

@@ -0,0 +1,37 @@
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.core.media._base.providers.torrent.iptorrents import Base
log = CPLog(__name__)
autoload = 'IPTorrents'
class IPTorrents(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
# TODO come back to this later, a better quality system needs to be created
cat_ids = [
([65], [
'bluray_1080p', 'bluray_720p',
'bdrip_1080p', 'bdrip_720p',
'brrip_1080p', 'brrip_720p',
'webdl_1080p', 'webdl_720p', 'webdl_480p',
'hdtv_720p', 'hdtv_sd'
]),
]
class Episode(EpisodeProvider, Base):
# TODO come back to this later, a better quality system needs to be created
cat_ids = [
([5], ['hdtv_720p', 'webdl_720p', 'webdl_1080p']),
([4, 78, 79], ['hdtv_sd'])
]

View File

@@ -0,0 +1,27 @@
from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.core.media._base.providers.torrent.publichd import Base
log = CPLog(__name__)
autoload = 'PublicHD'
class PublicHD(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
def buildUrl(self, media):
return fireEvent('media.search_query', media, single = True)
class Episode(EpisodeProvider, Base):
def buildUrl(self, media):
return fireEvent('media.search_query', media, single = True)

View File

@@ -0,0 +1,60 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.event import fireEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.core.media._base.providers.torrent.sceneaccess import Base
log = CPLog(__name__)
autoload = 'SceneAccess'
class SceneAccess(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
cat_ids = [
([26], ['hdtv_sd', 'hdtv_720p', 'webdl_720p', 'webdl_1080p']),
]
def buildUrl(self, media, quality):
url = self.urls['archive'] % (
self.getCatId(quality['identifier'])[0],
self.getCatId(quality['identifier'])[0]
)
arguments = tryUrlencode({
'search': fireEvent('media.search_query', media, single = True),
'method': 3,
})
query = "%s&%s" % (url, arguments)
return query
class Episode(EpisodeProvider, Base):
cat_ids = [
([27], ['hdtv_720p', 'webdl_720p', 'webdl_1080p']),
([17, 11], ['hdtv_sd'])
]
def buildUrl(self, media, quality):
url = self.urls['search'] % (
self.getCatId(quality['identifier'])[0],
self.getCatId(quality['identifier'])[0]
)
arguments = tryUrlencode({
'search': fireEvent('media.search_query', media, single = True),
'method': 3,
})
query = "%s&%s" % (url, arguments)
return query

View File

@@ -0,0 +1,46 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.core.media._base.providers.torrent.thepiratebay import Base
log = CPLog(__name__)
autoload = 'ThePirateBay'
class ThePirateBay(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
cat_ids = [
([208], ['hdtv_720p', 'webdl_720p', 'webdl_1080p']),
([205], ['hdtv_sd'])
]
def buildUrl(self, media, page, cats):
return (
tryUrlencode('"%s"' % fireEvent('media.search_query', media, single = True)),
page,
','.join(str(x) for x in cats)
)
class Episode(EpisodeProvider, Base):
cat_ids = [
([208], ['hdtv_720p', 'webdl_720p', 'webdl_1080p']),
([205], ['hdtv_sd'])
]
def buildUrl(self, media, page, cats):
return (
tryUrlencode('"%s"' % fireEvent('media.search_query', media, single = True)),
page,
','.join(str(x) for x in cats)
)

View File

@@ -0,0 +1,34 @@
from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.core.media._base.providers.torrent.torrentday import Base
log = CPLog(__name__)
autoload = 'TorrentDay'
class TorrentDay(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
cat_ids = [
([14], ['hdtv_sd', 'hdtv_720p', 'webdl_720p', 'webdl_1080p']),
]
def buildUrl(self, media):
return fireEvent('media.search_query', media, single = True)
class Episode(EpisodeProvider, Base):
cat_ids = [
([7], ['hdtv_720p', 'webdl_720p', 'webdl_1080p']),
([2], [24], [26], ['hdtv_sd'])
]
def buildUrl(self, media):
return fireEvent('media.search_query', media, single = True)

View File

@@ -0,0 +1,42 @@
from couchpotato import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.core.media._base.providers.torrent.torrentleech import Base
log = CPLog(__name__)
autoload = 'TorrentLeech'
class TorrentLeech(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
cat_ids = [
([27], ['hdtv_sd', 'hdtv_720p', 'webdl_720p', 'webdl_1080p']),
]
def buildUrl(self, media, quality):
return (
tryUrlencode(fireEvent('media.search_query', media, single = True)),
self.getCatId(quality['identifier'])[0]
)
class Episode(EpisodeProvider, Base):
cat_ids = [
([32], ['hdtv_720p', 'webdl_720p', 'webdl_1080p']),
([26], ['hdtv_sd'])
]
def buildUrl(self, media, quality):
return (
tryUrlencode(fireEvent('media.search_query', media, single = True)),
self.getCatId(quality['identifier'])[0]
)

View File

@@ -0,0 +1,38 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.core.media._base.providers.torrent.torrentpotato import Base
log = CPLog(__name__)
autoload = 'TorrentPotato'
class TorrentPotato(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
def buildUrl(self, media, host):
arguments = tryUrlencode({
'user': host['name'],
'passkey': host['pass_key'],
'search': fireEvent('media.search_query', media, single = True)
})
return '%s?%s' % (host['host'], arguments)
class Episode(EpisodeProvider, Base):
def buildUrl(self, media, host):
arguments = tryUrlencode({
'user': host['name'],
'passkey': host['pass_key'],
'search': fireEvent('media.search_query', media, single = True)
})
return '%s?%s' % (host['host'], arguments)

View File

@@ -0,0 +1,52 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.base import MultiProvider
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
from couchpotato.core.media._base.providers.torrent.torrentshack import Base
log = CPLog(__name__)
autoload = 'TorrentShack'
class TorrentShack(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
# TorrentShack tv season search categories
# TV-SD Pack - 980
# TV-HD Pack - 981
# Full Blu-ray - 970
cat_ids = [
([980], ['hdtv_sd']),
([981], ['hdtv_720p', 'webdl_720p', 'webdl_1080p', 'bdrip_1080p', 'bdrip_720p', 'brrip_1080p', 'brrip_720p']),
([970], ['bluray_1080p', 'bluray_720p']),
]
cat_backup_id = 980
def buildUrl(self, media, quality):
query = (tryUrlencode(fireEvent('media.search_query', media, single = True)),
self.getCatId(quality['identifier'])[0],
self.getSceneOnly())
return query
class Episode(EpisodeProvider, Base):
# TorrentShack tv episode search categories
# TV/x264-HD - 600
# TV/x264-SD - 620
# TV/DVDrip - 700
cat_ids = [
([600], ['hdtv_720p', 'webdl_720p', 'webdl_1080p', 'bdrip_1080p', 'bdrip_720p', 'brrip_1080p', 'brrip_720p']),
([620], ['hdtv_sd'])
]
cat_backup_id = 620
def buildUrl(self, media, quality):
query = (tryUrlencode(fireEvent('media.search_query', media, single = True)),
self.getCatId(quality['identifier'])[0],
self.getSceneOnly())
return query

View File

@@ -0,0 +1,152 @@
from couchpotato import fireEvent, get_db, Env
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEventAsync
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.searcher.base import SearcherBase
from couchpotato.core.media._base.searcher.main import SearchSetupError
from couchpotato.core.media.show import ShowTypeBase
log = CPLog(__name__)
autoload = 'EpisodeSearcher'
class EpisodeSearcher(SearcherBase, ShowTypeBase):
type = 'episode'
in_progress = False
def __init__(self):
super(EpisodeSearcher, self).__init__()
addEvent('%s.searcher.all' % self.getType(), self.searchAll)
addEvent('%s.searcher.single' % self.getType(), self.single)
addEvent('searcher.correct_release', self.correctRelease)
addApiView('%s.searcher.full_search' % self.getType(), self.searchAllView, docs = {
'desc': 'Starts a full search for all wanted shows',
})
addApiView('%s.searcher.single' % self.getType(), self.singleView)
def searchAllView(self, **kwargs):
fireEventAsync('%s.searcher.all' % self.getType(), manual = True)
return {
'success': not self.in_progress
}
def searchAll(self, manual = False):
pass
def singleView(self, media_id, **kwargs):
db = get_db()
media = db.get('id', media_id)
return {
'result': fireEvent('%s.searcher.single' % self.getType(), media, single = True)
}
def single(self, media, profile = None, quality_order = None, search_protocols = None, manual = False):
db = get_db()
related = fireEvent('library.related', media, single = True)
# TODO search_protocols, profile, quality_order can be moved to a base method
# Find out search type
try:
if not search_protocols:
search_protocols = fireEvent('searcher.protocols', single = True)
except SearchSetupError:
return
if not profile and related['show']['profile_id']:
profile = db.get('id', related['show']['profile_id'])
if not quality_order:
quality_order = fireEvent('quality.order', single = True)
# TODO: check episode status
# TODO: check air date
#if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['library']['year']):
# too_early_to_search.append(quality_type['quality']['identifier'])
# return
ret = False
has_better_quality = None
found_releases = []
too_early_to_search = []
releases = fireEvent('release.for_media', media['_id'], single = True)
query = fireEvent('library.query', media, condense = False, single = True)
index = 0
for q_identifier in profile.get('qualities'):
quality_custom = {
'quality': q_identifier,
'finish': profile['finish'][index],
'wait_for': profile['wait_for'][index],
'3d': profile['3d'][index] if profile.get('3d') else False
}
has_better_quality = 0
# See if better quality is available
for release in releases:
if quality_order.index(release['quality']) <= quality_order.index(q_identifier) and release['status'] not in ['available', 'ignored', 'failed']:
has_better_quality += 1
# Don't search for quality lower then already available.
if has_better_quality is 0:
log.info('Searching for %s in %s', (query, q_identifier))
quality = fireEvent('quality.single', identifier = q_identifier, single = True)
quality['custom'] = quality_custom
results = fireEvent('searcher.search', search_protocols, media, quality, single = True)
if len(results) == 0:
log.debug('Nothing found for %s in %s', (query, q_identifier))
# Add them to this movie releases list
found_releases += fireEvent('release.create_from_search', results, media, quality, single = True)
# Try find a valid result and download it
if fireEvent('release.try_download_result', results, media, quality, single = True):
ret = True
# Remove releases that aren't found anymore
for release in releases:
if release.get('status') == 'available' and release.get('identifier') not in found_releases:
fireEvent('release.delete', release.get('_id'), single = True)
else:
log.info('Better quality (%s) already available or snatched for %s', (q_identifier, query))
fireEvent('media.restatus', media['_id'])
break
# Break if CP wants to shut down
if self.shuttingDown() or ret:
break
if len(too_early_to_search) > 0:
log.info2('Too early to search for %s, %s', (too_early_to_search, query))
def correctRelease(self, release = None, media = None, quality = None, **kwargs):
if media.get('type') != 'show.episode':
return
retention = Env.setting('retention', section = 'nzb')
if release.get('seeders') is None and 0 < retention < release.get('age', 0):
log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (release['age'], retention, release['name']))
return False
# Check for required and ignored words
if not fireEvent('searcher.correct_words', release['name'], media, single = True):
return False
# TODO Matching is quite costly, maybe we should be caching release matches somehow? (also look at caper optimizations)
match = fireEvent('matcher.match', release, media, quality, single = True)
if match:
return match.weight
return False

View File

@@ -0,0 +1,172 @@
from couchpotato import get_db, Env
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.searcher.base import SearcherBase
from couchpotato.core.media.movie.searcher import SearchSetupError
from couchpotato.core.media.show import ShowTypeBase
log = CPLog(__name__)
autoload = 'SeasonSearcher'
class SeasonSearcher(SearcherBase, ShowTypeBase):
type = 'season'
in_progress = False
def __init__(self):
super(SeasonSearcher, self).__init__()
addEvent('%s.searcher.all' % self.getType(), self.searchAll)
addEvent('%s.searcher.single' % self.getType(), self.single)
addEvent('searcher.correct_release', self.correctRelease)
addApiView('%s.searcher.full_search' % self.getType(), self.searchAllView, docs = {
'desc': 'Starts a full search for all wanted seasons',
})
def searchAllView(self, **kwargs):
fireEventAsync('%s.searcher.all' % self.getType(), manual = True)
return {
'success': not self.in_progress
}
def searchAll(self, manual = False):
pass
def single(self, media, profile = None, quality_order = None, search_protocols = None, manual = False):
db = get_db()
related = fireEvent('library.related', media, single = True)
# TODO search_protocols, profile, quality_order can be moved to a base method
# Find out search type
try:
if not search_protocols:
search_protocols = fireEvent('searcher.protocols', single = True)
except SearchSetupError:
return
if not profile and related['show']['profile_id']:
profile = db.get('id', related['show']['profile_id'])
if not quality_order:
quality_order = fireEvent('quality.order', single = True)
# Find 'active' episodes
episodes = related['episodes']
episodes_active = []
for episode in episodes:
if episode.get('status') != 'active':
continue
episodes_active.append(episode)
if len(episodes_active) == len(episodes):
# All episodes are 'active', try and search for full season
if self.search(media, profile, quality_order, search_protocols):
# Success, end season search
return True
else:
log.info('Unable to find season pack, searching for individual episodes...')
# Search for each episode individually
for episode in episodes_active:
fireEvent('show.episode.searcher.single', episode, profile, quality_order, search_protocols, manual)
# TODO (testing) only grab one episode
return True
return True
def search(self, media, profile, quality_order, search_protocols):
# TODO: check episode status
# TODO: check air date
#if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['library']['year']):
# too_early_to_search.append(quality_type['quality']['identifier'])
# return
ret = False
has_better_quality = None
found_releases = []
too_early_to_search = []
releases = fireEvent('release.for_media', media['_id'], single = True)
query = fireEvent('library.query', media, condense = False, single = True)
index = 0
for q_identifier in profile.get('qualities'):
quality_custom = {
'quality': q_identifier,
'finish': profile['finish'][index],
'wait_for': profile['wait_for'][index],
'3d': profile['3d'][index] if profile.get('3d') else False
}
has_better_quality = 0
# See if better quality is available
for release in releases:
if quality_order.index(release['quality']) <= quality_order.index(q_identifier) and release['status'] not in ['available', 'ignored', 'failed']:
has_better_quality += 1
# Don't search for quality lower then already available.
if has_better_quality is 0:
log.info('Searching for %s in %s', (query, q_identifier))
quality = fireEvent('quality.single', identifier = q_identifier, single = True)
quality['custom'] = quality_custom
results = fireEvent('searcher.search', search_protocols, media, quality, single = True)
if len(results) == 0:
log.debug('Nothing found for %s in %s', (query, q_identifier))
# Add them to this movie releases list
found_releases += fireEvent('release.create_from_search', results, media, quality, single = True)
# Try find a valid result and download it
if fireEvent('release.try_download_result', results, media, quality, single = True):
ret = True
# Remove releases that aren't found anymore
for release in releases:
if release.get('status') == 'available' and release.get('identifier') not in found_releases:
fireEvent('release.delete', release.get('_id'), single = True)
else:
log.info('Better quality (%s) already available or snatched for %s', (q_identifier, query))
fireEvent('media.restatus', media['_id'])
break
# Break if CP wants to shut down
if self.shuttingDown() or ret:
break
if len(too_early_to_search) > 0:
log.info2('Too early to search for %s, %s', (too_early_to_search, query))
return len(found_releases) > 0
def correctRelease(self, release = None, media = None, quality = None, **kwargs):
if media.get('type') != 'show.season':
return
retention = Env.setting('retention', section = 'nzb')
if release.get('seeders') is None and 0 < retention < release.get('age', 0):
log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (release['age'], retention, release['name']))
return False
# Check for required and ignored words
if not fireEvent('searcher.correct_words', release['name'], media, single = True):
return False
# TODO Matching is quite costly, maybe we should be caching release matches somehow? (also look at caper optimizations)
match = fireEvent('matcher.match', release, media, quality, single = True)
if match:
return match.weight
return False

View File

@@ -0,0 +1,88 @@
from couchpotato import get_db
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
from couchpotato.core.helpers.variable import getTitle
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.searcher.base import SearcherBase
from couchpotato.core.media._base.searcher.main import SearchSetupError
from couchpotato.core.media.show import ShowTypeBase
log = CPLog(__name__)
autoload = 'ShowSearcher'
class ShowSearcher(SearcherBase, ShowTypeBase):
type = 'show'
in_progress = False
def __init__(self):
super(ShowSearcher, self).__init__()
addEvent('%s.searcher.all' % self.getType(), self.searchAll)
addEvent('%s.searcher.single' % self.getType(), self.single)
addEvent('searcher.get_search_title', self.getSearchTitle)
addApiView('%s.searcher.full_search' % self.getType(), self.searchAllView, docs = {
'desc': 'Starts a full search for all wanted episodes',
})
def searchAllView(self, **kwargs):
fireEventAsync('%s.searcher.all' % self.getType(), manual = True)
return {
'success': not self.in_progress
}
def searchAll(self, manual = False):
pass
def single(self, media, search_protocols = None, manual = False):
# Find out search type
try:
if not search_protocols:
search_protocols = fireEvent('searcher.protocols', single = True)
except SearchSetupError:
return
if not media['profile_id'] or media['status'] == 'done':
log.debug('Show doesn\'t have a profile or already done, assuming in manage tab.')
return
show_title = fireEvent('media.search_query', media, condense = False, single = True)
fireEvent('notify.frontend', type = 'show.searcher.started.%s' % media['_id'], data = True, message = 'Searching for "%s"' % show_title)
show_tree = fireEvent('library.tree', media, single = True)
db = get_db()
profile = db.get('id', media['profile_id'])
quality_order = fireEvent('quality.order', single = True)
for season in show_tree.get('seasons', []):
if not season.get('info'):
continue
# Skip specials (and seasons missing 'number') for now
# TODO: set status for specials to skipped by default
if not season['info'].get('number'):
continue
# Check if full season can be downloaded
fireEvent('show.season.searcher.single', season, profile, quality_order, search_protocols, manual)
# TODO (testing) only snatch one season
return
fireEvent('notify.frontend', type = 'show.searcher.ended.%s' % media['_id'], data = True)
def getSearchTitle(self, media):
if media.get('type') != 'show':
related = fireEvent('library.related', media, single = True)
show = related['show']
else:
show = media
return getTitle(show)

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