Permalink
Browse files

Changes the BTN provider to support backlog searches

The new provider uses BTN's API to search for torrents in stead of using
the RSS. The regular "RSS" searches are emulated by an API request for
the torrents added since the last successful request.

Because the BTN API is accessed using a JSON-RPC interface, the library
jsonrpclib is added. The source of this is
https://github.com/joshmarshall/jsonrpclib, I copied it with only some
minor changes to fix import errors without having to change the library
paths used in Sick Beard. The version used is from 20th of August 2011.
I have also added the jsonrpclib to the list of libraries as listed in the
readme.md.

Some changes had to be made to the way whole seasons are treated because
torrents cannot be split like NZB's can. This has been solved by checking
if the provider of the season file is BTN, if so the whole season is
added as a multi-ep result.

Because BTN often uses the name "tvrip" to identify SDTV (non-scene) rips,
I have added that to the name checking regex.
  • Loading branch information...
1 parent b3a7afe commit 55ab9d37e8b953b86b2c289af4aaf972f1acecb4 @DTMDTM DTMDTM committed May 13, 2012
View
22 data/interfaces/default/config_providers.tmpl
@@ -198,26 +198,8 @@ var show_nzb_providers = #if $sickbeard.USE_NZBS then "true" else "false"#;
<div class="providerDiv" id="btnDiv">
<div class="field-pair">
<label class="clearfix">
- <span class="component-title">BTN User ID:</span>
- <input class="component-desc" type="text" name="btn_user_id" value="$sickbeard.BTN_USER_ID" />
- </label>
- </div>
- <div class="field-pair">
- <label class="clearfix">
- <span class="component-title">BTN Auth Token:</span>
- <input class="component-desc" type="text" name="btn_auth_token" value="$sickbeard.BTN_AUTH_TOKEN" size="32" />
- </label>
- </div>
- <div class="field-pair">
- <label class="clearfix">
- <span class="component-title">BTN Passkey:</span>
- <input class="component-desc" type="text" name="btn_passkey" value="$sickbeard.BTN_PASSKEY" size="32" />
- </label>
- </div>
- <div class="field-pair">
- <label class="clearfix">
- <span class="component-title">BTN Authkey:</span>
- <input class="component-desc" type="text" name="btn_authkey" value="$sickbeard.BTN_AUTHKEY" size="32" />
+ <span class="component-title">BTN API KEY:</span>
+ <input class="component-desc" type="text" name="btn_api_key" value="$sickbeard.BTN_API_KEY" />
</label>
</div>
</div>
View
229 lib/jsonrpclib/SimpleJSONRPCServer.py
@@ -0,0 +1,229 @@
+import lib.jsonrpclib
+from lib.jsonrpclib import Fault
+from lib.jsonrpclib.jsonrpc import USE_UNIX_SOCKETS
+import SimpleXMLRPCServer
+import SocketServer
+import socket
+import logging
+import os
+import types
+import traceback
+import sys
+try:
+ import fcntl
+except ImportError:
+ # For Windows
+ fcntl = None
+
+def get_version(request):
+ # must be a dict
+ if 'jsonrpc' in request.keys():
+ return 2.0
+ if 'id' in request.keys():
+ return 1.0
+ return None
+
+def validate_request(request):
+ if type(request) is not types.DictType:
+ fault = Fault(
+ -32600, 'Request must be {}, not %s.' % type(request)
+ )
+ return fault
+ rpcid = request.get('id', None)
+ version = get_version(request)
+ if not version:
+ fault = Fault(-32600, 'Request %s invalid.' % request, rpcid=rpcid)
+ return fault
+ request.setdefault('params', [])
+ method = request.get('method', None)
+ params = request.get('params')
+ param_types = (types.ListType, types.DictType, types.TupleType)
+ if not method or type(method) not in types.StringTypes or \
+ type(params) not in param_types:
+ fault = Fault(
+ -32600, 'Invalid request parameters or method.', rpcid=rpcid
+ )
+ return fault
+ return True
+
+class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
+
+ def __init__(self, encoding=None):
+ SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self,
+ allow_none=True,
+ encoding=encoding)
+
+ def _marshaled_dispatch(self, data, dispatch_method = None):
+ response = None
+ try:
+ request = jsonrpclib.loads(data)
+ except Exception, e:
+ fault = Fault(-32700, 'Request %s invalid. (%s)' % (data, e))
+ response = fault.response()
+ return response
+ if not request:
+ fault = Fault(-32600, 'Request invalid -- no request data.')
+ return fault.response()
+ if type(request) is types.ListType:
+ # This SHOULD be a batch, by spec
+ responses = []
+ for req_entry in request:
+ result = validate_request(req_entry)
+ if type(result) is Fault:
+ responses.append(result.response())
+ continue
+ resp_entry = self._marshaled_single_dispatch(req_entry)
+ if resp_entry is not None:
+ responses.append(resp_entry)
+ if len(responses) > 0:
+ response = '[%s]' % ','.join(responses)
+ else:
+ response = ''
+ else:
+ result = validate_request(request)
+ if type(result) is Fault:
+ return result.response()
+ response = self._marshaled_single_dispatch(request)
+ return response
+
+ def _marshaled_single_dispatch(self, request):
+ # TODO - Use the multiprocessing and skip the response if
+ # it is a notification
+ # Put in support for custom dispatcher here
+ # (See SimpleXMLRPCServer._marshaled_dispatch)
+ method = request.get('method')
+ params = request.get('params')
+ try:
+ response = self._dispatch(method, params)
+ except:
+ exc_type, exc_value, exc_tb = sys.exc_info()
+ fault = Fault(-32603, '%s:%s' % (exc_type, exc_value))
+ return fault.response()
+ if 'id' not in request.keys() or request['id'] == None:
+ # It's a notification
+ return None
+ try:
+ response = jsonrpclib.dumps(response,
+ methodresponse=True,
+ rpcid=request['id']
+ )
+ return response
+ except:
+ exc_type, exc_value, exc_tb = sys.exc_info()
+ fault = Fault(-32603, '%s:%s' % (exc_type, exc_value))
+ return fault.response()
+
+ def _dispatch(self, method, params):
+ func = None
+ try:
+ func = self.funcs[method]
+ except KeyError:
+ if self.instance is not None:
+ if hasattr(self.instance, '_dispatch'):
+ return self.instance._dispatch(method, params)
+ else:
+ try:
+ func = SimpleXMLRPCServer.resolve_dotted_attribute(
+ self.instance,
+ method,
+ True
+ )
+ except AttributeError:
+ pass
+ if func is not None:
+ try:
+ if type(params) is types.ListType:
+ response = func(*params)
+ else:
+ response = func(**params)
+ return response
+ except TypeError:
+ return Fault(-32602, 'Invalid parameters.')
+ except:
+ err_lines = traceback.format_exc().splitlines()
+ trace_string = '%s | %s' % (err_lines[-3], err_lines[-1])
+ fault = jsonrpclib.Fault(-32603, 'Server error: %s' %
+ trace_string)
+ return fault
+ else:
+ return Fault(-32601, 'Method %s not supported.' % method)
+
+class SimpleJSONRPCRequestHandler(
+ SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
+
+ def do_POST(self):
+ if not self.is_rpc_path_valid():
+ self.report_404()
+ return
+ try:
+ max_chunk_size = 10*1024*1024
+ size_remaining = int(self.headers["content-length"])
+ L = []
+ while size_remaining:
+ chunk_size = min(size_remaining, max_chunk_size)
+ L.append(self.rfile.read(chunk_size))
+ size_remaining -= len(L[-1])
+ data = ''.join(L)
+ response = self.server._marshaled_dispatch(data)
+ self.send_response(200)
+ except Exception, e:
+ self.send_response(500)
+ err_lines = traceback.format_exc().splitlines()
+ trace_string = '%s | %s' % (err_lines[-3], err_lines[-1])
+ fault = jsonrpclib.Fault(-32603, 'Server error: %s' % trace_string)
+ response = fault.response()
+ if response == None:
+ response = ''
+ self.send_header("Content-type", "application/json-rpc")
+ self.send_header("Content-length", str(len(response)))
+ self.end_headers()
+ self.wfile.write(response)
+ self.wfile.flush()
+ self.connection.shutdown(1)
+
+class SimpleJSONRPCServer(SocketServer.TCPServer, SimpleJSONRPCDispatcher):
+
+ allow_reuse_address = True
+
+ def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler,
+ logRequests=True, encoding=None, bind_and_activate=True,
+ address_family=socket.AF_INET):
+ self.logRequests = logRequests
+ SimpleJSONRPCDispatcher.__init__(self, encoding)
+ # TCPServer.__init__ has an extra parameter on 2.6+, so
+ # check Python version and decide on how to call it
+ vi = sys.version_info
+ self.address_family = address_family
+ if USE_UNIX_SOCKETS and address_family == socket.AF_UNIX:
+ # Unix sockets can't be bound if they already exist in the
+ # filesystem. The convention of e.g. X11 is to unlink
+ # before binding again.
+ if os.path.exists(addr):
+ try:
+ os.unlink(addr)
+ except OSError:
+ logging.warning("Could not unlink socket %s", addr)
+ # if python 2.5 and lower
+ if vi[0] < 3 and vi[1] < 6:
+ SocketServer.TCPServer.__init__(self, addr, requestHandler)
+ else:
+ SocketServer.TCPServer.__init__(self, addr, requestHandler,
+ bind_and_activate)
+ if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'):
+ flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD)
+ flags |= fcntl.FD_CLOEXEC
+ fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)
+
+class CGIJSONRPCRequestHandler(SimpleJSONRPCDispatcher):
+
+ def __init__(self, encoding=None):
+ SimpleJSONRPCDispatcher.__init__(self, encoding)
+
+ def handle_jsonrpc(self, request_text):
+ response = self._marshaled_dispatch(request_text)
+ print 'Content-Type: application/json-rpc'
+ print 'Content-Length: %d' % len(response)
+ print
+ sys.stdout.write(response)
+
+ handle_xmlrpc = handle_jsonrpc
View
6 lib/jsonrpclib/__init__.py
@@ -0,0 +1,6 @@
+from config import Config
+config = Config.instance()
+from history import History
+history = History.instance()
+from jsonrpc import Server, MultiCall, Fault
+from jsonrpc import ProtocolError, loads, dumps
View
38 lib/jsonrpclib/config.py
@@ -0,0 +1,38 @@
+import sys
+
+class LocalClasses(dict):
+ def add(self, cls):
+ self[cls.__name__] = cls
+
+class Config(object):
+ """
+ This is pretty much used exclusively for the 'jsonclass'
+ functionality... set use_jsonclass to False to turn it off.
+ You can change serialize_method and ignore_attribute, or use
+ the local_classes.add(class) to include "local" classes.
+ """
+ use_jsonclass = True
+ # Change to False to keep __jsonclass__ entries raw.
+ serialize_method = '_serialize'
+ # The serialize_method should be a string that references the
+ # method on a custom class object which is responsible for
+ # returning a tuple of the constructor arguments and a dict of
+ # attributes.
+ ignore_attribute = '_ignore'
+ # The ignore attribute should be a string that references the
+ # attribute on a custom class object which holds strings and / or
+ # references of the attributes the class translator should ignore.
+ classes = LocalClasses()
+ # The list of classes to use for jsonclass translation.
+ version = 2.0
+ # Version of the JSON-RPC spec to support
+ user_agent = 'jsonrpclib/0.1 (Python %s)' % \
+ '.'.join([str(ver) for ver in sys.version_info[0:3]])
+ # User agent to use for calls.
+ _instance = None
+
+ @classmethod
+ def instance(cls):
+ if not cls._instance:
+ cls._instance = cls()
+ return cls._instance
View
40 lib/jsonrpclib/history.py
@@ -0,0 +1,40 @@
+class History(object):
+ """
+ This holds all the response and request objects for a
+ session. A server using this should call "clear" after
+ each request cycle in order to keep it from clogging
+ memory.
+ """
+ requests = []
+ responses = []
+ _instance = None
+
+ @classmethod
+ def instance(cls):
+ if not cls._instance:
+ cls._instance = cls()
+ return cls._instance
+
+ def add_response(self, response_obj):
+ self.responses.append(response_obj)
+
+ def add_request(self, request_obj):
+ self.requests.append(request_obj)
+
+ @property
+ def request(self):
+ if len(self.requests) == 0:
+ return None
+ else:
+ return self.requests[-1]
+
+ @property
+ def response(self):
+ if len(self.responses) == 0:
+ return None
+ else:
+ return self.responses[-1]
+
+ def clear(self):
+ del self.requests[:]
+ del self.responses[:]
View
145 lib/jsonrpclib/jsonclass.py
@@ -0,0 +1,145 @@
+import types
+import inspect
+import re
+import traceback
+
+from lib.jsonrpclib import config
+
+iter_types = [
+ types.DictType,
+ types.ListType,
+ types.TupleType
+]
+
+string_types = [
+ types.StringType,
+ types.UnicodeType
+]
+
+numeric_types = [
+ types.IntType,
+ types.LongType,
+ types.FloatType
+]
+
+value_types = [
+ types.BooleanType,
+ types.NoneType
+]
+
+supported_types = iter_types+string_types+numeric_types+value_types
+invalid_module_chars = r'[^a-zA-Z0-9\_\.]'
+
+class TranslationError(Exception):
+ pass
+
+def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]):
+ if not serialize_method:
+ serialize_method = config.serialize_method
+ if not ignore_attribute:
+ ignore_attribute = config.ignore_attribute
+ obj_type = type(obj)
+ # Parse / return default "types"...
+ if obj_type in numeric_types+string_types+value_types:
+ return obj
+ if obj_type in iter_types:
+ if obj_type in (types.ListType, types.TupleType):
+ new_obj = []
+ for item in obj:
+ new_obj.append(dump(item, serialize_method,
+ ignore_attribute, ignore))
+ if obj_type is types.TupleType:
+ new_obj = tuple(new_obj)
+ return new_obj
+ # It's a dict...
+ else:
+ new_obj = {}
+ for key, value in obj.iteritems():
+ new_obj[key] = dump(value, serialize_method,
+ ignore_attribute, ignore)
+ return new_obj
+ # It's not a standard type, so it needs __jsonclass__
+ module_name = inspect.getmodule(obj).__name__
+ class_name = obj.__class__.__name__
+ json_class = class_name
+ if module_name not in ['', '__main__']:
+ json_class = '%s.%s' % (module_name, json_class)
+ return_obj = {"__jsonclass__":[json_class,]}
+ # If a serialization method is defined..
+ if serialize_method in dir(obj):
+ # Params can be a dict (keyword) or list (positional)
+ # Attrs MUST be a dict.
+ serialize = getattr(obj, serialize_method)
+ params, attrs = serialize()
+ return_obj['__jsonclass__'].append(params)
+ return_obj.update(attrs)
+ return return_obj
+ # Otherwise, try to figure it out
+ # Obviously, we can't assume to know anything about the
+ # parameters passed to __init__
+ return_obj['__jsonclass__'].append([])
+ attrs = {}
+ ignore_list = getattr(obj, ignore_attribute, [])+ignore
+ for attr_name, attr_value in obj.__dict__.iteritems():
+ if type(attr_value) in supported_types and \
+ attr_name not in ignore_list and \
+ attr_value not in ignore_list:
+ attrs[attr_name] = dump(attr_value, serialize_method,
+ ignore_attribute, ignore)
+ return_obj.update(attrs)
+ return return_obj
+
+def load(obj):
+ if type(obj) in string_types+numeric_types+value_types:
+ return obj
+ if type(obj) is types.ListType:
+ return_list = []
+ for entry in obj:
+ return_list.append(load(entry))
+ return return_list
+ # Othewise, it's a dict type
+ if '__jsonclass__' not in obj.keys():
+ return_dict = {}
+ for key, value in obj.iteritems():
+ new_value = load(value)
+ return_dict[key] = new_value
+ return return_dict
+ # It's a dict, and it's a __jsonclass__
+ orig_module_name = obj['__jsonclass__'][0]
+ params = obj['__jsonclass__'][1]
+ if orig_module_name == '':
+ raise TranslationError('Module name empty.')
+ json_module_clean = re.sub(invalid_module_chars, '', orig_module_name)
+ if json_module_clean != orig_module_name:
+ raise TranslationError('Module name %s has invalid characters.' %
+ orig_module_name)
+ json_module_parts = json_module_clean.split('.')
+ json_class = None
+ if len(json_module_parts) == 1:
+ # Local class name -- probably means it won't work
+ if json_module_parts[0] not in config.classes.keys():
+ raise TranslationError('Unknown class or module %s.' %
+ json_module_parts[0])
+ json_class = config.classes[json_module_parts[0]]
+ else:
+ json_class_name = json_module_parts.pop()
+ json_module_tree = '.'.join(json_module_parts)
+ try:
+ temp_module = __import__(json_module_tree)
+ except ImportError:
+ raise TranslationError('Could not import %s from module %s.' %
+ (json_class_name, json_module_tree))
+ json_class = getattr(temp_module, json_class_name)
+ # Creating the object...
+ new_obj = None
+ if type(params) is types.ListType:
+ new_obj = json_class(*params)
+ elif type(params) is types.DictType:
+ new_obj = json_class(**params)
+ else:
+ raise TranslationError('Constructor args must be a dict or list.')
+ for key, value in obj.iteritems():
+ if key == '__jsonclass__':
+ continue
+ setattr(new_obj, key, value)
+ return new_obj
View
556 lib/jsonrpclib/jsonrpc.py
@@ -0,0 +1,556 @@
+"""
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+============================
+JSONRPC Library (jsonrpclib)
+============================
+
+This library is a JSON-RPC v.2 (proposed) implementation which
+follows the xmlrpclib API for portability between clients. It
+uses the same Server / ServerProxy, loads, dumps, etc. syntax,
+while providing features not present in XML-RPC like:
+
+* Keyword arguments
+* Notifications
+* Versioning
+* Batches and batch notifications
+
+Eventually, I'll add a SimpleXMLRPCServer compatible library,
+and other things to tie the thing off nicely. :)
+
+For a quick-start, just open a console and type the following,
+replacing the server address, method, and parameters
+appropriately.
+>>> import jsonrpclib
+>>> server = jsonrpclib.Server('http://localhost:8181')
+>>> server.add(5, 6)
+11
+>>> server._notify.add(5, 6)
+>>> batch = jsonrpclib.MultiCall(server)
+>>> batch.add(3, 50)
+>>> batch.add(2, 3)
+>>> batch._notify.add(3, 5)
+>>> batch()
+[53, 5]
+
+See http://code.google.com/p/jsonrpclib/ for more info.
+"""
+
+import types
+import sys
+from xmlrpclib import Transport as XMLTransport
+from xmlrpclib import SafeTransport as XMLSafeTransport
+from xmlrpclib import ServerProxy as XMLServerProxy
+from xmlrpclib import _Method as XML_Method
+import time
+import string
+import random
+
+# Library includes
+import lib.jsonrpclib
+from lib.jsonrpclib import config
+from lib.jsonrpclib import history
+
+# JSON library importing
+cjson = None
+json = None
+try:
+ import cjson
+except ImportError:
+ try:
+ import json
+ except ImportError:
+ try:
+ import lib.simplejson as json
+ except ImportError:
+ raise ImportError(
+ 'You must have the cjson, json, or simplejson ' +
+ 'module(s) available.'
+ )
+
+IDCHARS = string.ascii_lowercase+string.digits
+
+class UnixSocketMissing(Exception):
+ """
+ Just a properly named Exception if Unix Sockets usage is
+ attempted on a platform that doesn't support them (Windows)
+ """
+ pass
+
+#JSON Abstractions
+
+def jdumps(obj, encoding='utf-8'):
+ # Do 'serialize' test at some point for other classes
+ global cjson
+ if cjson:
+ return cjson.encode(obj)
+ else:
+ return json.dumps(obj, encoding=encoding)
+
+def jloads(json_string):
+ global cjson
+ if cjson:
+ return cjson.decode(json_string)
+ else:
+ return json.loads(json_string)
+
+
+# XMLRPClib re-implementations
+
+class ProtocolError(Exception):
+ pass
+
+class TransportMixIn(object):
+ """ Just extends the XMLRPC transport where necessary. """
+ user_agent = config.user_agent
+ # for Python 2.7 support
+ _connection = None
+
+ def send_content(self, connection, request_body):
+ connection.putheader("Content-Type", "application/json-rpc")
+ connection.putheader("Content-Length", str(len(request_body)))
+ connection.endheaders()
+ if request_body:
+ connection.send(request_body)
+
+ def getparser(self):
+ target = JSONTarget()
+ return JSONParser(target), target
+
+class JSONParser(object):
+ def __init__(self, target):
+ self.target = target
+
+ def feed(self, data):
+ self.target.feed(data)
+
+ def close(self):
+ pass
+
+class JSONTarget(object):
+ def __init__(self):
+ self.data = []
+
+ def feed(self, data):
+ self.data.append(data)
+
+ def close(self):
+ return ''.join(self.data)
+
+class Transport(TransportMixIn, XMLTransport):
+ pass
+
+class SafeTransport(TransportMixIn, XMLSafeTransport):
+ pass
+from httplib import HTTP, HTTPConnection
+from socket import socket
+
+USE_UNIX_SOCKETS = False
+
+try:
+ from socket import AF_UNIX, SOCK_STREAM
+ USE_UNIX_SOCKETS = True
+except ImportError:
+ pass
+
+if (USE_UNIX_SOCKETS):
+
+ class UnixHTTPConnection(HTTPConnection):
+ def connect(self):
+ self.sock = socket(AF_UNIX, SOCK_STREAM)
+ self.sock.connect(self.host)
+
+ class UnixHTTP(HTTP):
+ _connection_class = UnixHTTPConnection
+
+ class UnixTransport(TransportMixIn, XMLTransport):
+ def make_connection(self, host):
+ import httplib
+ host, extra_headers, x509 = self.get_host_info(host)
+ return UnixHTTP(host)
+
+
+class ServerProxy(XMLServerProxy):
+ """
+ Unfortunately, much more of this class has to be copied since
+ so much of it does the serialization.
+ """
+
+ def __init__(self, uri, transport=None, encoding=None,
+ verbose=0, version=None):
+ import urllib
+ if not version:
+ version = config.version
+ self.__version = version
+ schema, uri = urllib.splittype(uri)
+ if schema not in ('http', 'https', 'unix'):
+ raise IOError('Unsupported JSON-RPC protocol.')
+ if schema == 'unix':
+ if not USE_UNIX_SOCKETS:
+ # Don't like the "generic" Exception...
+ raise UnixSocketMissing("Unix sockets not available.")
+ self.__host = uri
+ self.__handler = '/'
+ else:
+ self.__host, self.__handler = urllib.splithost(uri)
+ if not self.__handler:
+ # Not sure if this is in the JSON spec?
+ #self.__handler = '/'
+ self.__handler == '/'
+ if transport is None:
+ if schema == 'unix':
+ transport = UnixTransport()
+ elif schema == 'https':
+ transport = SafeTransport()
+ else:
+ transport = Transport()
+ self.__transport = transport
+ self.__encoding = encoding
+ self.__verbose = verbose
+
+ def _request(self, methodname, params, rpcid=None):
+ request = dumps(params, methodname, encoding=self.__encoding,
+ rpcid=rpcid, version=self.__version)
+ response = self._run_request(request)
+ check_for_errors(response)
+ return response['result']
+
+ def _request_notify(self, methodname, params, rpcid=None):
+ request = dumps(params, methodname, encoding=self.__encoding,
+ rpcid=rpcid, version=self.__version, notify=True)
+ response = self._run_request(request, notify=True)
+ check_for_errors(response)
+ return
+
+ def _run_request(self, request, notify=None):
+ history.add_request(request)
+
+ response = self.__transport.request(
+ self.__host,
+ self.__handler,
+ request,
+ verbose=self.__verbose
+ )
+
+ # Here, the XMLRPC library translates a single list
+ # response to the single value -- should we do the
+ # same, and require a tuple / list to be passed to
+ # the response object, or expect the Server to be
+ # outputting the response appropriately?
+
+ history.add_response(response)
+ if not response:
+ return None
+ return_obj = loads(response)
+ return return_obj
+
+ def __getattr__(self, name):
+ # Same as original, just with new _Method reference
+ return _Method(self._request, name)
+
+ @property
+ def _notify(self):
+ # Just like __getattr__, but with notify namespace.
+ return _Notify(self._request_notify)
+
+
+class _Method(XML_Method):
+
+ def __call__(self, *args, **kwargs):
+ if len(args) > 0 and len(kwargs) > 0:
+ raise ProtocolError('Cannot use both positional ' +
+ 'and keyword arguments (according to JSON-RPC spec.)')
+ if len(args) > 0:
+ return self.__send(self.__name, args)
+ else:
+ return self.__send(self.__name, kwargs)
+
+ def __getattr__(self, name):
+ self.__name = '%s.%s' % (self.__name, name)
+ return self
+ # The old method returned a new instance, but this seemed wasteful.
+ # The only thing that changes is the name.
+ #return _Method(self.__send, "%s.%s" % (self.__name, name))
+
+class _Notify(object):
+ def __init__(self, request):
+ self._request = request
+
+ def __getattr__(self, name):
+ return _Method(self._request, name)
+
+# Batch implementation
+
+class MultiCallMethod(object):
+
+ def __init__(self, method, notify=False):
+ self.method = method
+ self.params = []
+ self.notify = notify
+
+ def __call__(self, *args, **kwargs):
+ if len(kwargs) > 0 and len(args) > 0:
+ raise ProtocolError('JSON-RPC does not support both ' +
+ 'positional and keyword arguments.')
+ if len(kwargs) > 0:
+ self.params = kwargs
+ else:
+ self.params = args
+
+ def request(self, encoding=None, rpcid=None):
+ return dumps(self.params, self.method, version=2.0,
+ encoding=encoding, rpcid=rpcid, notify=self.notify)
+
+ def __repr__(self):
+ return '%s' % self.request()
+
+ def __getattr__(self, method):
+ new_method = '%s.%s' % (self.method, method)
+ self.method = new_method
+ return self
+
+class MultiCallNotify(object):
+
+ def __init__(self, multicall):
+ self.multicall = multicall
+
+ def __getattr__(self, name):
+ new_job = MultiCallMethod(name, notify=True)
+ self.multicall._job_list.append(new_job)
+ return new_job
+
+class MultiCallIterator(object):
+
+ def __init__(self, results):
+ self.results = results
+
+ def __iter__(self):
+ for i in range(0, len(self.results)):
+ yield self[i]
+ raise StopIteration
+
+ def __getitem__(self, i):
+ item = self.results[i]
+ check_for_errors(item)
+ return item['result']
+
+ def __len__(self):
+ return len(self.results)
+
+class MultiCall(object):
+
+ def __init__(self, server):
+ self._server = server
+ self._job_list = []
+
+ def _request(self):
+ if len(self._job_list) < 1:
+ # Should we alert? This /is/ pretty obvious.
+ return
+ request_body = '[ %s ]' % ','.join([job.request() for
+ job in self._job_list])
+ responses = self._server._run_request(request_body)
+ del self._job_list[:]
+ if not responses:
+ responses = []
+ return MultiCallIterator(responses)
+
+ @property
+ def _notify(self):
+ return MultiCallNotify(self)
+
+ def __getattr__(self, name):
+ new_job = MultiCallMethod(name)
+ self._job_list.append(new_job)
+ return new_job
+
+ __call__ = _request
+
+# These lines conform to xmlrpclib's "compatibility" line.
+# Not really sure if we should include these, but oh well.
+Server = ServerProxy
+
+class Fault(object):
+ # JSON-RPC error class
+ def __init__(self, code=-32000, message='Server error', rpcid=None):
+ self.faultCode = code
+ self.faultString = message
+ self.rpcid = rpcid
+
+ def error(self):
+ return {'code':self.faultCode, 'message':self.faultString}
+
+ def response(self, rpcid=None, version=None):
+ if not version:
+ version = config.version
+ if rpcid:
+ self.rpcid = rpcid
+ return dumps(
+ self, methodresponse=True, rpcid=self.rpcid, version=version
+ )
+
+ def __repr__(self):
+ return '<Fault %s: %s>' % (self.faultCode, self.faultString)
+
+def random_id(length=8):
+ return_id = ''
+ for i in range(length):
+ return_id += random.choice(IDCHARS)
+ return return_id
+
+class Payload(dict):
+ def __init__(self, rpcid=None, version=None):
+ if not version:
+ version = config.version
+ self.id = rpcid
+ self.version = float(version)
+
+ def request(self, method, params=[]):
+ if type(method) not in types.StringTypes:
+ raise ValueError('Method name must be a string.')
+ if not self.id:
+ self.id = random_id()
+ request = { 'id':self.id, 'method':method }
+ if params:
+ request['params'] = params
+ if self.version >= 2:
+ request['jsonrpc'] = str(self.version)
+ return request
+
+ def notify(self, method, params=[]):
+ request = self.request(method, params)
+ if self.version >= 2:
+ del request['id']
+ else:
+ request['id'] = None
+ return request
+
+ def response(self, result=None):
+ response = {'result':result, 'id':self.id}
+ if self.version >= 2:
+ response['jsonrpc'] = str(self.version)
+ else:
+ response['error'] = None
+ return response
+
+ def error(self, code=-32000, message='Server error.'):
+ error = self.response()
+ if self.version >= 2:
+ del error['result']
+ else:
+ error['result'] = None
+ error['error'] = {'code':code, 'message':message}
+ return error
+
+def dumps(params=[], methodname=None, methodresponse=None,
+ encoding=None, rpcid=None, version=None, notify=None):
+ """
+ This differs from the Python implementation in that it implements
+ the rpcid argument since the 2.0 spec requires it for responses.
+ """
+ if not version:
+ version = config.version
+ valid_params = (types.TupleType, types.ListType, types.DictType)
+ if methodname in types.StringTypes and \
+ type(params) not in valid_params and \
+ not isinstance(params, Fault):
+ """
+ If a method, and params are not in a listish or a Fault,
+ error out.
+ """
+ raise TypeError('Params must be a dict, list, tuple or Fault ' +
+ 'instance.')
+ # Begin parsing object
+ payload = Payload(rpcid=rpcid, version=version)
+ if not encoding:
+ encoding = 'utf-8'
+ if type(params) is Fault:
+ response = payload.error(params.faultCode, params.faultString)
+ return jdumps(response, encoding=encoding)
+ if type(methodname) not in types.StringTypes and methodresponse != True:
+ raise ValueError('Method name must be a string, or methodresponse '+
+ 'must be set to True.')
+ if config.use_jsonclass == True:
+ from lib.jsonrpclib import jsonclass
+ params = jsonclass.dump(params)
+ if methodresponse is True:
+ if rpcid is None:
+ raise ValueError('A method response must have an rpcid.')
+ response = payload.response(params)
+ return jdumps(response, encoding=encoding)
+ request = None
+ if notify == True:
+ request = payload.notify(methodname, params)
+ else:
+ request = payload.request(methodname, params)
+ return jdumps(request, encoding=encoding)
+
+def loads(data):
+ """
+ This differs from the Python implementation, in that it returns
+ the request structure in Dict format instead of the method, params.
+ It will return a list in the case of a batch request / response.
+ """
+ if data == '':
+ # notification
+ return None
+ result = jloads(data)
+ # if the above raises an error, the implementing server code
+ # should return something like the following:
+ # { 'jsonrpc':'2.0', 'error': fault.error(), id: None }
+ if config.use_jsonclass == True:
+ from lib.jsonrpclib import jsonclass
+ result = jsonclass.load(result)
+ return result
+
+def check_for_errors(result):
+ if not result:
+ # Notification
+ return result
+ if type(result) is not types.DictType:
+ raise TypeError('Response is not a dict.')
+ if 'jsonrpc' in result.keys() and float(result['jsonrpc']) > 2.0:
+ raise NotImplementedError('JSON-RPC version not yet supported.')
+ if 'result' not in result.keys() and 'error' not in result.keys():
+ raise ValueError('Response does not have a result or error key.')
+ if 'error' in result.keys() and result['error'] != None:
+ code = result['error']['code']
+ message = result['error']['message']
+ raise ProtocolError((code, message))
+ return result
+
+def isbatch(result):
+ if type(result) not in (types.ListType, types.TupleType):
+ return False
+ if len(result) < 1:
+ return False
+ if type(result[0]) is not types.DictType:
+ return False
+ if 'jsonrpc' not in result[0].keys():
+ return False
+ try:
+ version = float(result[0]['jsonrpc'])
+ except ValueError:
+ raise ProtocolError('"jsonrpc" key must be a float(able) value.')
+ if version < 2:
+ return False
+ return True
+
+def isnotification(request):
+ if 'id' not in request.keys():
+ # 2.0 notification
+ return True
+ if request['id'] == None:
+ # 1.0 notification
+ return True
+ return False
View
4 readme.md
@@ -30,6 +30,7 @@ Sick Beard makes use of the following projects:
* [Python GNTP][pythongntp]
* [SocksiPy][socks]
* [python-dateutil][dateutil]
+* [jsonrpclib][jsonrpclib]
## Dependencies
@@ -51,4 +52,5 @@ If you find a bug please report it or it'll never get fixed. Verify that it hasn
[dateutil]: http://labix.org/python-dateutil
[googledownloads]: http://code.google.com/p/sickbeard/downloads/list
[googleissues]: http://code.google.com/p/sickbeard/issues/list
-[googlenewissue]: http://code.google.com/p/sickbeard/issues/entry
+[googlenewissue]: http://code.google.com/p/sickbeard/issues/entry
+[jsonrpclib]: https://github.com/joshmarshall/jsonrpclib
View
17 sickbeard/__init__.py
@@ -157,10 +157,7 @@
TVTORRENTS_HASH = None
BTN = False
-BTN_USER_ID = None
-BTN_AUTH_TOKEN = None
-BTN_PASSKEY = None
-BTN_AUTHKEY = None
+BTN_API_KEY = None
TORRENT_DIR = None
@@ -393,7 +390,7 @@ def initialize(consoleLogging=True):
USE_PLEX, PLEX_NOTIFY_ONSNATCH, PLEX_NOTIFY_ONDOWNLOAD, PLEX_UPDATE_LIBRARY, \
PLEX_SERVER_HOST, PLEX_HOST, PLEX_USERNAME, PLEX_PASSWORD, \
showUpdateScheduler, __INITIALIZED__, LAUNCH_BROWSER, showList, loadingShowList, \
- NZBS, NZBS_UID, NZBS_HASH, EZRSS, TVTORRENTS, TVTORRENTS_DIGEST, TVTORRENTS_HASH, BTN, BTN_USER_ID, BTN_AUTH_TOKEN, BTN_PASSKEY, BTN_AUTHKEY, TORRENT_DIR, USENET_RETENTION, SOCKET_TIMEOUT, \
+ NZBS, NZBS_UID, NZBS_HASH, EZRSS, TVTORRENTS, TVTORRENTS_DIGEST, TVTORRENTS_HASH, BTN, BTN_API_KEY, TORRENT_DIR, USENET_RETENTION, SOCKET_TIMEOUT, \
SEARCH_FREQUENCY, DEFAULT_SEARCH_FREQUENCY, BACKLOG_SEARCH_FREQUENCY, \
QUALITY_DEFAULT, SEASON_FOLDERS_FORMAT, SEASON_FOLDERS_DEFAULT, STATUS_DEFAULT, \
GROWL_NOTIFY_ONSNATCH, GROWL_NOTIFY_ONDOWNLOAD, TWITTER_NOTIFY_ONSNATCH, TWITTER_NOTIFY_ONDOWNLOAD, \
@@ -553,10 +550,7 @@ def initialize(consoleLogging=True):
TVTORRENTS_HASH = check_setting_str(CFG, 'TVTORRENTS', 'tvtorrents_hash', '')
BTN = bool(check_setting_int(CFG, 'BTN', 'btn', 0))
- BTN_USER_ID = check_setting_str(CFG, 'BTN', 'btn_user_id', '')
- BTN_AUTH_TOKEN = check_setting_str(CFG, 'BTN', 'btn_auth_token', '')
- BTN_AUTHKEY = check_setting_str(CFG, 'BTN', 'btn_authkey', '')
- BTN_PASSKEY = check_setting_str(CFG, 'BTN', 'btn_passkey', '')
+ BTN_API_KEY = check_setting_str(CFG, 'BTN', 'btn_api_key', '')
NZBS = bool(check_setting_int(CFG, 'NZBs', 'nzbs', 0))
NZBS_UID = check_setting_str(CFG, 'NZBs', 'nzbs_uid', '')
@@ -1079,10 +1073,7 @@ def save_config():
new_config['BTN'] = {}
new_config['BTN']['btn'] = int(BTN)
- new_config['BTN']['btn_user_id'] = BTN_USER_ID
- new_config['BTN']['btn_auth_token'] = BTN_AUTH_TOKEN
- new_config['BTN']['btn_authkey'] = BTN_AUTHKEY
- new_config['BTN']['btn_passkey'] = BTN_PASSKEY
+ new_config['BTN']['btn_api_key'] = BTN_API_KEY
new_config['NZBs'] = {}
new_config['NZBs']['nzbs'] = int(NZBS)
View
2 sickbeard/common.py
@@ -124,7 +124,7 @@ def nameQuality(name):
checkName = lambda list, func: func([re.search(x, name, re.I) for x in list])
- if checkName(["(pdtv|hdtv|dsr).(xvid|x264)"], all) and not checkName(["(720|1080)[pi]"], all):
+ if checkName(["(pdtv|hdtv|dsr|tvrip).(xvid|x264)"], all) and not checkName(["(720|1080)[pi]"], all):
return Quality.SDTV
elif checkName(["(dvdrip|bdrip)(.ws)?.(xvid|divx|x264)"], any) and not checkName(["(720|1080)[pi]"], all):
return Quality.SDDVD
View
405 sickbeard/providers/btn.py
@@ -1,72 +1,333 @@
-# Author: Nic Wolfe <nic@wolfeden.ca>
-# URL: http://code.google.com/p/sickbeard/
-#
-# This file is part of Sick Beard.
-#
-# Sick Beard is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# Sick Beard is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>.
-
-import sickbeard
-import generic
-
-from sickbeard import logger
-from sickbeard import tvcache
-
-class BTNProvider(generic.TorrentProvider):
-
- def __init__(self):
-
- generic.TorrentProvider.__init__(self, "BTN")
-
- self.supportsBacklog = False
-
- self.cache = BTNCache(self)
-
- self.url = 'http://broadcasthe.net/'
-
- def isEnabled(self):
- return sickbeard.BTN
-
- def imageName(self):
- return 'btn.gif'
-
-class BTNCache(tvcache.TVCache):
-
- def __init__(self, provider):
-
- tvcache.TVCache.__init__(self, provider)
-
- # only poll BTN every 15 minutes max
- self.minTime = 15
-
- def _getRSSData(self):
- url = 'https://broadcasthe.net/feeds.php?feed=torrents_all&user='+ sickbeard.BTN_USER_ID +'&auth='+ sickbeard.BTN_AUTH_TOKEN +'&passkey='+ sickbeard.BTN_PASSKEY +'&authkey='+ sickbeard.BTN_AUTHKEY
- logger.log(u"BTN cache update URL: "+ url, logger.DEBUG)
-
- data = self.provider.getURL(url)
-
- return data
-
- def _parseItem(self, item):
-
- (title, url) = self.provider._get_title_and_url(item)
-
- if not title or not url:
- logger.log(u"The XML returned from the BTN RSS feed is incomplete, this result is unusable", logger.ERROR)
- return
-
- logger.log(u"Adding item from RSS to cache: "+title, logger.DEBUG)
-
- self._addCacheEntry(title, url)
-
-provider = BTNProvider()
+# coding=utf-8
+# Author: Daniël Heimans
+# URL: http://code.google.com/p/sickbeard
+#
+# This file is part of Sick Beard.
+#
+# Sick Beard is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Sick Beard is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>.
+
+import sickbeard
+import generic
+
+from sickbeard import scene_exceptions
+from sickbeard import logger
+from sickbeard import tvcache
+from sickbeard.helpers import sanitizeSceneName
+from sickbeard.common import Quality
+from sickbeard.exceptions import ex
+
+from lib import jsonrpclib
+import datetime
+import time
+import socket
+import math
+import pprint
+
+class BTNProvider(generic.TorrentProvider):
+
+ def __init__(self):
+
+ generic.TorrentProvider.__init__(self, "BTN")
+
+ self.supportsBacklog = True
+ self.cache = BTNCache(self)
+
+ self.url = "http://broadcasthe.net"
+
+ def isEnabled(self):
+ return sickbeard.BTN
+
+ def imageName(self):
+ return 'btn.gif'
+
+ def checkAuthFromData(self, data):
+ result = True
+ if 'api-error' in data:
+ logger.log("Error in sickbeard data retrieval: " + data['api-error'], logger.ERROR)
+ result = False
+
+ return result
+
+ def _doSearch(self, search_params, show=None):
+ params = {}
+ apikey = sickbeard.BTN_API_KEY
+
+ if search_params:
+ params.update(search_params)
+
+ search_results = self._api_call(apikey, params)
+
+ if not search_results:
+ return []
+
+ if 'torrents' in search_results:
+ found_torrents = search_results['torrents']
+ else:
+ found_torrents = {}
+
+ # We got something, we know the API sends max 1000 results at a time.
+ # See if there are more than 1000 results for our query, if not we
+ # keep requesting until we've got everything.
+ # max 150 requests per minute so limit at that
+ max_pages = 150
+ results_per_page = 1000.0
+
+ if 'results' in search_results and search_results['results'] >= results_per_page:
+ pages_needed = int(math.ceil(int(search_results['results']) / results_per_page))
+ if pages_needed > max_pages:
+ pages_needed = max_pages
+
+ # +1 because range(1,4) = 1, 2, 3
+ for page in range(1,pages_needed+1):
+ search_results = self._api_call(apikey, params, results_per_page, page * results_per_page)
+ # Note that this these are individual requests and might time out individually. This would result in 'gaps'
+ # in the results. There is no way to fix this though.
+ if 'torrents' in search_results:
+ found_torrents.update(search_results['torrents'])
+
+ results = []
+
+ for torrentid, torrent_info in found_torrents.iteritems():
+ (title, url) = self._get_title_and_url(torrent_info)
+
+ if not title or not url:
+ logger.log(u"The BTN provider did not return both a valid title and URL for search parameters: " + str(params) + " but returned " + str(torrentinfo), logger.WARNING)
+ results.append(torrent_info)
+
+# Disabled this because it overspammed the debug log a bit too much
+# logger.log(u'BTN provider returning the following results for search parameters: ' + str(params), logger.DEBUG)
+# for result in results:
+# (title, result) = self._get_title_and_url(result)
+# logger.log(title, logger.DEBUG)
+
+ return results
+
+ def _api_call(self, apikey, params={}, results_per_page=1000, offset=0):
+ server = jsonrpclib.Server('http://api.btnapps.net')
+
+ search_results ={}
+ try:
+ search_results = server.getTorrentsSearch(apikey, params, int(results_per_page), int(offset))
+ except jsonrpclib.jsonrpc.ProtocolError, error:
+ logger.log(u"JSON-RPC protocol error while accessing BTN API: " + ex(error), logger.ERROR)
+ search_results = {'api-error': ex(error)}
+ return search_results
+ except socket.timeout:
+ logger.log(u"Timeout while accessing BTN API", logger.WARNING)
+ except socket.error, error:
+ # Note that sometimes timeouts are thrown as socket errors
+ logger.log(u"Socket error while accessing BTN API: " + error[1], logger.ERROR)
+ except Exception, error:
+ errorstring = str(error)
+ if(errorstring.startswith('<') and errorstring.endswith('>')):
+ errorstring = errorstring[1:-1]
+ logger.log(u"Unknown error while accessing BTN API: " + errorstring, logger.ERROR)
+
+ return search_results
+
+ def _get_title_and_url(self, search_result):
+
+ # The BTN API gives a lot of information in response,
+ # however Sick Beard is built mostly around Scene or
+ # release names, which is why we are using them here.
+ if 'ReleaseName' in search_result and search_result['ReleaseName']:
+ title = search_result['ReleaseName']
+ else:
+ # If we don't have a release name we need to get creative
+ title = u''
+ if 'Series' in search_result:
+ title += search_result['Series']
+ if 'GroupName' in search_result:
+ title += '.' + search_result['GroupName'] if title else search_result['GroupName']
+ if 'Resolution' in search_result:
+ title += '.' + search_result['Resolution'] if title else search_result['Resolution']
+ if 'Source' in search_result:
+ title += '.' + search_result['Source'] if title else search_result['Source']
+ if 'Codec' in search_result:
+ title += '.' + search_result['Codec'] if title else search_result['Codec']
+
+ if 'DownloadURL' in search_result:
+ url = search_result['DownloadURL']
+ else:
+ url = None
+
+ return (title, url)
+
+ def _get_season_search_strings(self, show, season=None):
+ if not show:
+ return [{}]
+
+ search_params = []
+
+ name_exceptions = scene_exceptions.get_scene_exceptions(show.tvdbid) + [show.name]
+ for name in name_exceptions:
+
+ current_params = {}
+
+ if show.tvdbid:
+ current_params['tvdb'] = show.tvdbid
+ elif show.tvrid:
+ current_params['tvrage'] = show.tvrid
+ else:
+ # Search by name if we don't have tvdb or tvrage id
+ current_params['series'] = sanitizeSceneName(name)
+
+ if season != None:
+ whole_season_params = current_params.copy()
+ partial_season_params = current_params.copy()
+ # Search for entire seasons: no need to do special things for air by date shows
+ whole_season_params['category'] = 'Season'
+ whole_season_params['name'] = 'Season ' + str(season)
+
+ search_params.append(whole_season_params)
+
+ # Search for episodes in the season
+ partial_season_params['category'] = 'Episode'
+
+ if show.air_by_date:
+ # Search for the year of the air by date show
+ partial_season_params['name'] = str(season.split('-')[0])
+ else:
+ # Search for any result which has Sxx in the name
+ partial_season_params['name'] = 'S%02d' % int(season)
+
+ search_params.append(partial_season_params)
+
+ else:
+ search_params.append(current_params)
+
+ return search_params
+
+ def _get_episode_search_strings(self, ep_obj):
+
+ if not ep_obj:
+ return [{}]
+
+ search_params = {'category':'Episode'}
+
+ if ep_obj.show.tvdbid:
+ search_params['tvdb'] = ep_obj.show.tvdbid
+ elif ep_obj.show.tvrid:
+ search_params['tvrage'] = ep_obj.show.rid
+ else:
+ search_params['series'] = sanitizeSceneName(ep_obj.show_name)
+
+ if ep_obj.show.air_by_date:
+ date_str = str(ep_obj.airdate)
+
+ # BTN uses dots in dates, we just search for the date since that
+ # combined with the series identifier should result in just one episode
+ search_params['name'] = date_str.replace('-','.')
+
+ else:
+ # Do a general name search for the episode, formatted like SXXEYY
+ search_params['name'] = "S%02dE%02d" % (ep_obj.season,ep_obj.episode)
+
+ to_return = [search_params]
+
+ # only do scene exceptions if we are searching by name
+ if 'series' in search_params:
+
+ # add new query string for every exception
+ name_exceptions = scene_exceptions.get_scene_exceptions(ep_obj.show.tvdbid)
+ for cur_exception in name_exceptions:
+
+ # don't add duplicates
+ if cur_exception == ep_obj.show.name:
+ continue
+
+ # copy all other parameters before setting the show name for this exception
+ cur_return = search_params.copy()
+ cur_return['series'] = sanitizeSceneName(cur_exception)
+ to_return.append(cur_return)
+
+ return to_return
+
+ def getQuality(self, item):
+ quality = None
+ (title,url) = self._get_title_and_url(item)
+ quality = Quality.nameQuality(title)
+
+ return quality
+
+ def _doGeneralSearch(self, search_string):
+ # 'search' looks as broad is it can find. Can contain episode overview and title for example,
+ # use with caution!
+ return self._doSearch({'search': search_string})
+
+class BTNCache(tvcache.TVCache):
+
+ def __init__(self, provider):
+ tvcache.TVCache.__init__(self, provider)
+
+ # At least 15 minutes between queries
+ self.minTime = 15
+
+ def updateCache(self):
+ if not self.shouldUpdate():
+ return
+
+ data = self._getRSSData()
+
+ # As long as we got something from the provider we count it as an update
+ if data:
+ self.setLastUpdate()
+ else:
+ return []
+
+ logger.log(u"Clearing "+self.provider.name+" cache and updating with new information")
+ self._clearCache()
+
+ if not self._checkAuth(data):
+ raise exceptions.AuthException("Your authentication info for "+self.provider.name+" is incorrect, check your config")
+
+ # By now we know we've got data and no auth errors, all we need to do is put it in the database
+ for item in data:
+ self._parseItem(item)
+
+ def _getRSSData(self):
+ # Get the torrents uploaded since last check.
+ seconds_since_last_update = math.ceil(time.time() - time.mktime(self._getLastUpdate().timetuple()))
+
+
+ # default to 15 minutes
+ if seconds_since_last_update < 15*60:
+ seconds_since_last_update = 15*60
+
+ # Set maximum to 24 hours of "RSS" data search, older things will need to be done through backlog
+ if seconds_since_last_update > 24*60*60:
+ logger.log(u"The last known successful \"RSS\" update on the BTN API was more than 24 hours ago (%i hours to be precise), only trying to fetch the last 24 hours!" %(int(seconds_since_last_update)//(60*60)), logger.WARNING)
+ seconds_since_last_update = 24*60*60
+
+ age_string = "<=%i" % seconds_since_last_update
+ search_params={'age': age_string}
+
+ data = self.provider._doSearch(search_params)
+
+ return data
+
+ def _parseItem(self, item):
+ (title, url) = self.provider._get_title_and_url(item)
+
+ if not title or not url:
+ logger.log(u"The result returned from the BTN regular search is incomplete, this result is unusable", logger.ERROR)
+ return
+ logger.log(u"Adding item from regular BTN search to cache: " + title, logger.DEBUG)
+
+ self._addCacheEntry(title, url)
+
+ def _checkAuth(self, data):
+ return self.provider.checkAuthFromData(data)
+
+provider = BTNProvider()
View
48 sickbeard/search.py
@@ -394,25 +394,39 @@ def findSeason(show, season):
logger.log(u"No eps from this season are wanted at this quality, ignoring the result of "+bestSeasonNZB.name, logger.DEBUG)
else:
-
- logger.log(u"Breaking apart the NZB and adding the individual ones to our results", logger.DEBUG)
-
- # if not, break it apart and add them as the lowest priority results
- individualResults = nzbSplitter.splitResult(bestSeasonNZB)
-
- individualResults = filter(lambda x: show_name_helpers.filterBadReleases(x.name) and show_name_helpers.isGoodResult(x.name, show), individualResults)
-
- for curResult in individualResults:
- if len(curResult.episodes) == 1:
- epNum = curResult.episodes[0].episode
- elif len(curResult.episodes) > 1:
- epNum = MULTI_EP_RESULT
-
+
+ # Check if the provider of this NZB is BTN, if so it's not a NZB but a torrent so all we can do is leach the entire torrent, user will have to select which eps not do download in his torrent client
+ if not 'BTN' in bestSeasonNZB.provider.name:
+ logger.log(u"Breaking apart the NZB and adding the individual ones to our results", logger.DEBUG)
+
+ # if not, break it apart and add them as the lowest priority results
+ individualResults = nzbSplitter.splitResult(bestSeasonNZB)
+
+ individualResults = filter(lambda x: show_name_helpers.filterBadReleases(x.name) and show_name_helpers.isGoodResult(x.name, show), individualResults)
+
+ for curResult in individualResults:
+ if len(curResult.episodes) == 1:
+ epNum = curResult.episodes[0].episode
+ elif len(curResult.episodes) > 1:
+ epNum = MULTI_EP_RESULT
+
+ if epNum in foundResults:
+ foundResults[epNum].append(curResult)
+ else:
+ foundResults[epNum] = [curResult]
+ else:
+ # Season result from BTN must be a full-season torrent, creating multi-ep result for it.
+ logger.log(u"Adding multi-ep result for full-season torrent. Set the episodes you don't want to 'don't download' in your torrent client if desired!")
+ epObjs = []
+ for curEpNum in allEps:
+ epObjs.append(show.getEpisode(season, curEpNum))
+ bestSeasonNZB.episodes = epObjs
+
+ epNum = MULTI_EP_RESULT
if epNum in foundResults:
- foundResults[epNum].append(curResult)
+ foundResults[epNum].append(bestSeasonNZB)
else:
- foundResults[epNum] = [curResult]
-
+ foundResults[epNum] = [bestSeasonNZB]
# go through multi-ep results and see if we really want them or not, get rid of the rest
multiResults = {}
View
7 sickbeard/webserve.py
@@ -1092,7 +1092,7 @@ def deleteNewznabProvider(self, id):
def saveProviders(self, nzbmatrix_username=None, nzbmatrix_apikey=None,
nzbs_r_us_uid=None, nzbs_r_us_hash=None, newznab_string=None,
tvtorrents_digest=None, tvtorrents_hash=None,
- btn_user_id=None, btn_auth_token=None, btn_passkey=None, btn_authkey=None,
+ btn_api_key=None,
newzbin_username=None, newzbin_password=None,
provider_order=None):
@@ -1165,10 +1165,7 @@ def saveProviders(self, nzbmatrix_username=None, nzbmatrix_apikey=None,
sickbeard.TVTORRENTS_DIGEST = tvtorrents_digest.strip()
sickbeard.TVTORRENTS_HASH = tvtorrents_hash.strip()
- sickbeard.BTN_USER_ID = btn_user_id.strip()
- sickbeard.BTN_AUTH_TOKEN = btn_auth_token.strip()
- sickbeard.BTN_PASSKEY = btn_passkey.strip()
- sickbeard.BTN_AUTHKEY = btn_authkey.strip()
+ sickbeard.BTN_API_KEY = btn_api_key.strip()
sickbeard.NZBSRUS_UID = nzbs_r_us_uid.strip()
sickbeard.NZBSRUS_HASH = nzbs_r_us_hash.strip()

0 comments on commit 55ab9d3

Please sign in to comment.