61 changed files with 1112 additions and 244 deletions
@ -1,3 +1,5 @@ |
|||
*.pyc |
|||
/data/ |
|||
/_source/ |
|||
/_source/ |
|||
.project |
|||
.pydevproject |
|||
|
@ -0,0 +1,40 @@ |
|||
from .main import Nzbsrus |
|||
|
|||
def start(): |
|||
return Nzbsrus() |
|||
|
|||
config = [{ |
|||
'name': 'nzbsrus', |
|||
'groups': [ |
|||
{ |
|||
'tab': 'searcher', |
|||
'subtab': 'providers', |
|||
'name': 'nzbsrus', |
|||
'label': 'Nzbsrus', |
|||
'description': 'See <a href="https://www.nzbsrus.com/">NZBsRus</a>', |
|||
'wizard': True, |
|||
'options': [ |
|||
{ |
|||
'name': 'enabled', |
|||
'type': 'enabler', |
|||
}, |
|||
{ |
|||
'name': 'userid', |
|||
'label': 'User ID', |
|||
}, |
|||
{ |
|||
'name': 'api_key', |
|||
'default': '', |
|||
'label': 'Api Key', |
|||
}, |
|||
{ |
|||
'name': 'english_only', |
|||
'default': 1, |
|||
'type': 'bool', |
|||
'label': 'English only', |
|||
'description': 'Only search for English spoken movies on Nzbsrus', |
|||
}, |
|||
], |
|||
}, |
|||
], |
|||
}] |
@ -0,0 +1,104 @@ |
|||
from couchpotato.core.event import fireEvent |
|||
from couchpotato.core.helpers.encoding import tryUrlencode |
|||
from couchpotato.core.helpers.rss import RSS |
|||
from couchpotato.core.logger import CPLog |
|||
from couchpotato.core.providers.nzb.base import NZBProvider |
|||
from couchpotato.environment import Env |
|||
import time |
|||
import xml.etree.ElementTree as XMLTree |
|||
|
|||
log = CPLog(__name__) |
|||
|
|||
class Nzbsrus(NZBProvider, RSS): |
|||
|
|||
urls = { |
|||
'download': 'https://www.nzbsrus.com/nzbdownload_rss.php/%s', |
|||
'detail': 'https://www.nzbsrus.com/nzbdetails.php?id=%s', |
|||
'search': 'https://www.nzbsrus.com/api.php?extended=1&xml=1&listname={date,grabs}', |
|||
} |
|||
|
|||
cat_ids = [ |
|||
([90, 45, 51], ['720p', '1080p', 'brrip', 'bd50', 'dvdr']), |
|||
([48, 51], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']), |
|||
] |
|||
cat_backup_id = 240 |
|||
|
|||
def search(self, movie, quality): |
|||
|
|||
results = [] |
|||
|
|||
if self.isDisabled(): |
|||
return results |
|||
|
|||
cat_id_string = '&'.join(['c%s=1' % x for x in self.getCatId(quality.get('identifier'))]) |
|||
|
|||
arguments = tryUrlencode({ |
|||
'searchtext': 'imdb:' + movie['library']['identifier'][2:], |
|||
'uid': self.conf('userid'), |
|||
'key': self.conf('api_key'), |
|||
'age': Env.setting('retention', section = 'nzb'), |
|||
|
|||
}) |
|||
|
|||
# check for english_only |
|||
if self.conf('english_only'): |
|||
arguments += "&lang0=1&lang3=1&lang1=1" |
|||
|
|||
url = "%s&%s&%s" % (self.urls['search'], arguments , cat_id_string) |
|||
|
|||
cache_key = 'nzbsrus_1.%s.%s' % (movie['library'].get('identifier'), cat_id_string) |
|||
single_cat = True |
|||
|
|||
data = self.getCache(cache_key, url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()}) |
|||
if data: |
|||
try: |
|||
try: |
|||
data = XMLTree.fromstring(data) |
|||
nzbs = self.getElements(data, 'results/result') |
|||
except Exception, e: |
|||
log.debug('%s, %s', (self.getName(), e)) |
|||
return results |
|||
|
|||
for nzb in nzbs: |
|||
|
|||
title = self.getTextElement(nzb, "name") |
|||
if 'error' in title.lower(): continue |
|||
|
|||
id = self.getTextElement(nzb, "id") |
|||
size = int(round(int(self.getTextElement(nzb, "size")) / 1048576)) |
|||
age = int(round((time.time() - int(self.getTextElement(nzb, "postdate"))) / 86400)) |
|||
|
|||
new = { |
|||
'id': id, |
|||
'type': 'nzb', |
|||
'provider': self.getName(), |
|||
'name': title, |
|||
'age': age, |
|||
'size': size, |
|||
'url': self.urls['download'] % id + self.getApiExt() + self.getTextElement(nzb, "key"), |
|||
'download': self.download, |
|||
'detail_url': self.urls['detail'] % id, |
|||
'description': self.getTextElement(nzb, "addtext"), |
|||
'check_nzb': True, |
|||
} |
|||
|
|||
is_correct_movie = fireEvent('searcher.correct_movie', |
|||
nzb = new, movie = movie, quality = quality, |
|||
imdb_results = True, single = True) |
|||
|
|||
if is_correct_movie: |
|||
new['score'] = fireEvent('score.calculate', new, movie, single = True) |
|||
results.append(new) |
|||
self.found(new) |
|||
|
|||
return results |
|||
except SyntaxError: |
|||
log.error('Failed to parse XML response from Nzbsrus.com') |
|||
|
|||
return results |
|||
|
|||
def download(self, url = '', nzb_id = ''): |
|||
return self.urlopen(url, headers = {'User-Agent': Env.getIdentifier()}) |
|||
|
|||
def getApiExt(self): |
|||
return '/%s/' % (self.conf('userid')) |
@ -0,0 +1,36 @@ |
|||
from main import PassThePopcorn |
|||
|
|||
def start(): |
|||
return PassThePopcorn() |
|||
|
|||
config = [{ |
|||
'name': 'passthepopcorn', |
|||
'groups': [{ |
|||
'tab': 'searcher', |
|||
'subtab': 'providers', |
|||
'name': 'PassThePopcorn', |
|||
'description': 'See <a href="http://passthepopcorn.me">PassThePopcorn.me</a>', |
|||
'options': [ |
|||
{ |
|||
'name': 'enabled', |
|||
'type': 'enabler', |
|||
'default': False |
|||
}, |
|||
{ |
|||
'name': 'domain', |
|||
'advanced': True, |
|||
'label': 'Proxy server', |
|||
'description': 'Domain for requests (HTTPS only!), keep empty to use default (tls.passthepopcorn.me).', |
|||
}, |
|||
{ |
|||
'name': 'username', |
|||
'default': '', |
|||
}, |
|||
{ |
|||
'name': 'password', |
|||
'default': '', |
|||
'type': 'password', |
|||
} |
|||
], |
|||
}] |
|||
}] |
@ -0,0 +1,254 @@ |
|||
from couchpotato.core.event import fireEvent |
|||
from couchpotato.core.helpers.encoding import tryUrlencode |
|||
from couchpotato.core.helpers.variable import getTitle, tryInt, mergeDicts |
|||
from couchpotato.core.logger import CPLog |
|||
from couchpotato.core.providers.torrent.base import TorrentProvider |
|||
from dateutil.parser import parse |
|||
import cookielib |
|||
import htmlentitydefs |
|||
import json |
|||
import re |
|||
import time |
|||
import traceback |
|||
import urllib2 |
|||
|
|||
log = CPLog(__name__) |
|||
|
|||
|
|||
class PassThePopcorn(TorrentProvider): |
|||
|
|||
urls = { |
|||
'domain': 'https://tls.passthepopcorn.me', |
|||
'detail': 'https://tls.passthepopcorn.me/torrents.php?torrentid=%s', |
|||
'torrent': 'https://tls.passthepopcorn.me/torrents.php', |
|||
'login': 'https://tls.passthepopcorn.me/login.php', |
|||
'search': 'https://tls.passthepopcorn.me/search/%s/0/7/%d' |
|||
} |
|||
|
|||
quality_search_params = { |
|||
'bd50': {'media': 'Blu-ray', 'format': 'BD50'}, |
|||
'1080p': {'resolution': '1080p'}, |
|||
'720p': {'resolution': '720p'}, |
|||
'brrip': {'media': 'Blu-ray'}, |
|||
'dvdr': {'resolution': 'anysd'}, |
|||
'dvdrip': {'media': 'DVD'}, |
|||
'scr': {'media': 'DVD-Screener'}, |
|||
'r5': {'media': 'R5'}, |
|||
'tc': {'media': 'TC'}, |
|||
'ts': {'media': 'TS'}, |
|||
'cam': {'media': 'CAM'} |
|||
} |
|||
|
|||
post_search_filters = { |
|||
'bd50': {'Codec': ['BD50']}, |
|||
'1080p': {'Resolution': ['1080p']}, |
|||
'720p': {'Resolution': ['720p']}, |
|||
'brrip': {'Source': ['Blu-ray'], 'Quality': ['High Definition'], 'Container': ['!ISO']}, |
|||
'dvdr': {'Codec': ['DVD5', 'DVD9']}, |
|||
'dvdrip': {'Source': ['DVD'], 'Codec': ['!DVD5', '!DVD9']}, |
|||
'scr': {'Source': ['DVD-Screener']}, |
|||
'r5': {'Source': ['R5']}, |
|||
'tc': {'Source': ['TC']}, |
|||
'ts': {'Source': ['TS']}, |
|||
'cam': {'Source': ['CAM']} |
|||
} |
|||
|
|||
class NotLoggedInHTTPError(urllib2.HTTPError): |
|||
def __init__(self, url, code, msg, headers, fp): |
|||
urllib2.HTTPError.__init__(self, url, code, msg, headers, fp) |
|||
|
|||
class PTPHTTPRedirectHandler(urllib2.HTTPRedirectHandler): |
|||
def http_error_302(self, req, fp, code, msg, headers): |
|||
log.debug("302 detected; redirected to %s" % headers['Location']) |
|||
if (headers['Location'] != 'login.php'): |
|||
return urllib2.HTTPRedirectHandler.http_error_302(self, req, fp, code, msg, headers) |
|||
else: |
|||
raise PassThePopcorn.NotLoggedInHTTPError(req.get_full_url(), code, msg, headers, fp) |
|||
|
|||
def search(self, movie, quality): |
|||
|
|||
results = [] |
|||
|
|||
if self.isDisabled(): |
|||
return results |
|||
|
|||
movie_title = getTitle(movie['library']) |
|||
quality_id = quality['identifier'] |
|||
|
|||
log.info('Searching for %s at quality %s' % (movie_title, quality_id)) |
|||
|
|||
params = mergeDicts(self.quality_search_params[quality_id].copy(), { |
|||
'order_by': 'relevance', |
|||
'order_way': 'descending', |
|||
'searchstr': movie['library']['identifier'] |
|||
}) |
|||
|
|||
# Do login for the cookies |
|||
if not self.login_opener and not self.login(): |
|||
return results |
|||
|
|||
try: |
|||
url = '%s?json=noredirect&%s' % (self.urls['torrent'], tryUrlencode(params)) |
|||
txt = self.urlopen(url, opener = self.login_opener) |
|||
res = json.loads(txt) |
|||
except: |
|||
log.error('Search on PassThePopcorn.me (%s) failed (could not decode JSON)' % params) |
|||
return [] |
|||
|
|||
try: |
|||
if not 'Movies' in res: |
|||
log.info("PTP search returned nothing for '%s' at quality '%s' with search parameters %s" % (movie_title, quality_id, params)) |
|||
return [] |
|||
|
|||
authkey = res['AuthKey'] |
|||
passkey = res['PassKey'] |
|||
|
|||
for ptpmovie in res['Movies']: |
|||
if not 'Torrents' in ptpmovie: |
|||
log.debug('Movie %s (%s) has NO torrents' % (ptpmovie['Title'], ptpmovie['Year'])) |
|||
continue |
|||
|
|||
log.debug('Movie %s (%s) has %d torrents' % (ptpmovie['Title'], ptpmovie['Year'], len(ptpmovie['Torrents']))) |
|||
for torrent in ptpmovie['Torrents']: |
|||
torrent_id = tryInt(torrent['Id']) |
|||
torrentdesc = '%s %s %s' % (torrent['Resolution'], torrent['Source'], torrent['Codec']) |
|||
|
|||
if 'GoldenPopcorn' in torrent and torrent['GoldenPopcorn']: |
|||
torrentdesc += ' HQ' |
|||
if 'Scene' in torrent and torrent['Scene']: |
|||
torrentdesc += ' Scene' |
|||
if 'RemasterTitle' in torrent and torrent['RemasterTitle']: |
|||
# eliminate odd characters... |
|||
torrentdesc += self.htmlToASCII(' %s' % torrent['RemasterTitle']) |
|||
|
|||
torrentdesc += ' (%s)' % quality_id |
|||
torrent_name = re.sub('[^A-Za-z0-9\-_ \(\).]+', '', '%s (%s) - %s' % (movie_title, ptpmovie['Year'], torrentdesc)) |
|||
|
|||
def extra_check(item): |
|||
return self.torrentMeetsQualitySpec(item, type) |
|||
|
|||
def extra_score(item): |
|||
return 50 if torrent['GoldenPopcorn'] else 0 |
|||
|
|||
new = { |
|||
'id': torrent_id, |
|||
'type': 'torrent', |
|||
'provider': self.getName(), |
|||
'name': torrent_name, |
|||
'description': '', |
|||
'url': '%s?action=download&id=%d&authkey=%s&torrent_pass=%s' % (self.urls['torrent'], torrent_id, authkey, passkey), |
|||
'detail_url': self.urls['detail'] % torrent_id, |
|||
'date': tryInt(time.mktime(parse(torrent['UploadTime']).timetuple())), |
|||
'size': tryInt(torrent['Size']) / (1024 * 1024), |
|||
'provider': self.getName(), |
|||
'seeders': tryInt(torrent['Seeders']), |
|||
'leechers': tryInt(torrent['Leechers']), |
|||
'extra_score': extra_score, |
|||
'extra_check': extra_check, |
|||
'download': self.loginDownload, |
|||
} |
|||
|
|||
new['score'] = fireEvent('score.calculate', new, movie, single = True) |
|||
|
|||
if fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality): |
|||
results.append(new) |
|||
self.found(new) |
|||
|
|||
return results |
|||
except: |
|||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc())) |
|||
|
|||
return [] |
|||
|
|||
def login(self): |
|||
|
|||
cookieprocessor = urllib2.HTTPCookieProcessor(cookielib.CookieJar()) |
|||
opener = urllib2.build_opener(cookieprocessor, PassThePopcorn.PTPHTTPRedirectHandler()) |
|||
opener.addheaders = [ |
|||
('User-Agent', 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.75 Safari/537.1'), |
|||
('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'), |
|||
('Accept-Language', 'en-gb,en;q=0.5'), |
|||
('Accept-Charset', 'ISO-8859-1,utf-8;q=0.7,*;q=0.7'), |
|||
('Keep-Alive', '115'), |
|||
('Connection', 'keep-alive'), |
|||
('Cache-Control', 'max-age=0'), |
|||
] |
|||
|
|||
try: |
|||
response = opener.open(self.urls['login'], self.getLoginParams()) |
|||
except urllib2.URLError as e: |
|||
log.error('Login to PassThePopcorn failed: %s' % e) |
|||
return False |
|||
|
|||
if response.getcode() == 200: |
|||
log.debug('Login HTTP status 200; seems successful') |
|||
self.login_opener = opener |
|||
return True |
|||
else: |
|||
log.error('Login to PassThePopcorn failed: returned code %d' % response.getcode()) |
|||
return False |
|||
|
|||
def torrentMeetsQualitySpec(self, torrent, quality): |
|||
|
|||
if not quality in self.post_search_filters: |
|||
return True |
|||
|
|||
for field, specs in self.post_search_filters[quality].items(): |
|||
matches_one = False |
|||
seen_one = False |
|||
|
|||
if not field in torrent: |
|||
log.debug('Torrent with ID %s has no field "%s"; cannot apply post-search-filter for quality "%s"' % (torrent['Id'], field, quality)) |
|||
continue |
|||
|
|||
for spec in specs: |
|||
if len(spec) > 0 and spec[0] == '!': |
|||
# a negative rule; if the field matches, return False |
|||
if torrent[field] == spec[1:]: |
|||
return False |
|||
else: |
|||
# a positive rule; if any of the possible positive values match the field, return True |
|||
seen_one = True |
|||
if torrent[field] == spec: |
|||
matches_one = True |
|||
|
|||
if seen_one and not matches_one: |
|||
return False |
|||
|
|||
return True |
|||
|
|||
def htmlToUnicode(self, text): |
|||
def fixup(m): |
|||
text = m.group(0) |
|||
if text[:2] == "&#": |
|||
# character reference |
|||
try: |
|||
if text[:3] == "&#x": |
|||
return unichr(int(text[3:-1], 16)) |
|||
else: |
|||
return unichr(int(text[2:-1])) |
|||
except ValueError: |
|||
pass |
|||
else: |
|||
# named entity |
|||
try: |
|||
text = unichr(htmlentitydefs.name2codepoint[text[1:-1]]) |
|||
except KeyError: |
|||
pass |
|||
return text # leave as is |
|||
return re.sub("&#?\w+;", fixup, u'%s' % text) |
|||
|
|||
def unicodeToASCII(self, text): |
|||
import unicodedata |
|||
return ''.join(c for c in unicodedata.normalize('NFKD', text) if unicodedata.category(c) != 'Mn') |
|||
|
|||
def htmlToASCII(self, text): |
|||
return self.unicodeToASCII(self.htmlToUnicode(text)) |
|||
|
|||
def getLoginParams(self): |
|||
return tryUrlencode({ |
|||
'username': self.conf('username'), |
|||
'password': self.conf('password'), |
|||
'keeplogged': '1', |
|||
'login': 'Login' |
|||
}) |
Loading…
Reference in new issue