diff --git a/couchpotato/__init__.py b/couchpotato/__init__.py index 0d0ddd8..712224a 100644 --- a/couchpotato/__init__.py +++ b/couchpotato/__init__.py @@ -26,6 +26,8 @@ def get_session(engine = None): def get_engine(): return create_engine(Env.get('db_path'), echo = False) +def addView(route, func, static = False): + web.add_url_rule(route + ('' if static else '/'), endpoint = route if route else 'index', view_func = func) """ Web view """ @web.route('/') diff --git a/couchpotato/cli.py b/couchpotato/cli.py index a4c6dc9..e3fb946 100644 --- a/couchpotato/cli.py +++ b/couchpotato/cli.py @@ -90,7 +90,7 @@ def cmd_couchpotato(base_path, args): # Load configs & plugins loader = Env.get('loader') loader.preload(root = base_path) - loader.addModule('core', 'couchpotato.core', 'core') + loader.addModule(0, 'core', 'couchpotato.core', 'core') loader.run() diff --git a/couchpotato/core/event.py b/couchpotato/core/event.py index 84276b3..bfcd65a 100644 --- a/couchpotato/core/event.py +++ b/couchpotato/core/event.py @@ -20,7 +20,7 @@ def removeEvent(name, handler): e -= handler def fireEvent(name, *args, **kwargs): - log.debug('Firing "%s": %s, %s' % (name, args, kwargs)) + #log.debug('Firing "%s": %s, %s' % (name, args, kwargs)) try: # Return single handler @@ -64,7 +64,7 @@ def fireEvent(name, *args, **kwargs): log.error('%s: %s' % (name, e)) def fireEventAsync(name, *args, **kwargs): - log.debug('Async "%s": %s, %s' % (name, args, kwargs)) + #log.debug('Async "%s": %s, %s' % (name, args, kwargs)) try: e = events[name] e.asynchronous = True diff --git a/couchpotato/core/loader.py b/couchpotato/core/loader.py index a9f75d7..551b3f4 100644 --- a/couchpotato/core/loader.py +++ b/couchpotato/core/loader.py @@ -18,47 +18,48 @@ class Loader: providers = os.path.join(root, 'couchpotato', 'core', 'providers') self.paths = { - 'plugin' : ('couchpotato.core.plugins', os.path.join(core, 'plugins')), - 'notifications' : ('couchpotato.core.notifications', os.path.join(core, 'notifications')), - 'downloaders' : ('couchpotato.core.downloaders', os.path.join(root, 'couchpotato', 'core', 'downloaders')), - 'movie_provider' : ('couchpotato.core.providers.movie', os.path.join(providers, 'movie')), - 'nzb_provider' : ('couchpotato.core.providers.nzb', os.path.join(providers, 'nzb')), - 'torrent_provider' : ('couchpotato.core.providers.torrent', os.path.join(providers, 'torrent')), - 'trailer_provider' : ('couchpotato.core.providers.trailer', os.path.join(providers, 'trailer')), - 'subtitle_provider' : ('couchpotato.core.providers.subtitle', os.path.join(providers, 'subtitle')), + 'plugin' : (0, 'couchpotato.core.plugins', os.path.join(core, 'plugins')), + 'notifications' : (20, 'couchpotato.core.notifications', os.path.join(core, 'notifications')), + 'downloaders' : (20, 'couchpotato.core.downloaders', os.path.join(core, 'downloaders')), + 'movie_provider' : (20, 'couchpotato.core.providers.movie', os.path.join(providers, 'movie')), + 'nzb_provider' : (20, 'couchpotato.core.providers.nzb', os.path.join(providers, 'nzb')), + 'torrent_provider' : (20, 'couchpotato.core.providers.torrent', os.path.join(providers, 'torrent')), + 'trailer_provider' : (20, 'couchpotato.core.providers.trailer', os.path.join(providers, 'trailer')), + 'subtitle_provider' : (20, 'couchpotato.core.providers.subtitle', os.path.join(providers, 'subtitle')), } for type, tuple in self.paths.iteritems(): - self.addFromDir(type, tuple[0], tuple[1]) + priority, module, dir = tuple + self.addFromDir(type, priority, module, dir) def run(self): did_save = 0 - for module_name, plugin in sorted(self.modules.iteritems()): + for priority in self.modules: + for module_name, plugin in sorted(self.modules[priority].iteritems()): + # Load module + try: + m = getattr(self.loadModule(module_name), plugin.get('name')) - # Load module - try: - m = getattr(self.loadModule(module_name), plugin.get('name')) + log.info("Loading %s: %s" % (plugin['type'], plugin['name'])) - log.info("Loading %s: %s" % (plugin['type'], plugin['name'])) + # Save default settings for plugin/provider + did_save += self.loadSettings(m, module_name, save = False) - # Save default settings for plugin/provider - did_save += self.loadSettings(m, module_name, save = False) - - self.loadPlugins(m, plugin.get('name')) - except Exception, e: - log.error(e) + self.loadPlugins(m, plugin.get('name')) + except Exception, e: + log.error('Can\'t import %s: %s' % (plugin.get('name'), e)) if did_save: fireEvent('settings.save') - def addFromDir(self, type, module, dir): + def addFromDir(self, type, priority, module, dir): for file in glob.glob(os.path.join(dir, '*')): name = os.path.basename(file) if os.path.isdir(os.path.join(dir, name)): module_name = '%s.%s' % (module, name) - self.addModule(type, module_name, name) + self.addModule(priority, type, module_name, name) def loadSettings(self, module, name, save = True): try: @@ -82,8 +83,14 @@ class Loader: log.error("Failed loading plugin '%s': %s" % (name, e)) return False - def addModule(self, type, module, name): - self.modules[module] = { + def addModule(self, priority, type, module, name): + + if not self.modules.get(priority): + self.modules[priority] = {} + + self.modules[priority][module] = { + 'priority': priority, + 'module': module, 'type': type, 'name': name, } diff --git a/couchpotato/core/notifications/core/__init__.py b/couchpotato/core/notifications/core/__init__.py new file mode 100644 index 0000000..6e923da --- /dev/null +++ b/couchpotato/core/notifications/core/__init__.py @@ -0,0 +1,6 @@ +from .main import CoreNotifier + +def start(): + return CoreNotifier() + +config = [] diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py new file mode 100644 index 0000000..d7d260b --- /dev/null +++ b/couchpotato/core/notifications/core/main.py @@ -0,0 +1,49 @@ +from couchpotato.api import addApiView +from couchpotato.core.event import addEvent, fireEvent +from couchpotato.core.helpers.request import jsonified +from couchpotato.core.logger import CPLog +from couchpotato.core.plugins.base import Plugin +import time + +log = CPLog(__name__) + + +class CoreNotifier(Plugin): + + messages = [] + + def __init__(self): + addEvent('notify', self.notify) + addEvent('notify.core_notifier', self.notify) + addEvent('core_notifier.frontend', self.frontend) + + addApiView('core_notifier.listener', self.listener) + + static = self.registerStatic(__file__) + fireEvent('register_script', static + 'notification.js') + + def notify(self, message = '', data = {}): + self.add(data = { + 'message': message, + 'raw': data, + }) + + def frontend(self, type = 'notification', data = {}): + + self.messages.append({ + 'time': time.time(), + 'type': type, + 'data': data, + }) + + def listener(self): + + for message in self.messages: + #delete message older then 15s + if message['time'] < (time.time() - 15): + del message + + return jsonified({ + 'success': True, + 'result': self.messages, + }) diff --git a/couchpotato/core/notifications/core/static/notification.js b/couchpotato/core/notifications/core/static/notification.js new file mode 100644 index 0000000..caa2f22 --- /dev/null +++ b/couchpotato/core/notifications/core/static/notification.js @@ -0,0 +1,42 @@ +var NotificationBase = new Class({ + + Extends: BlockBase, + Implements: [Options, Events], + + initialize: function(options){ + var self = this; + self.setOptions(options); + + //App.addEvent('load', self.request.bind(self)); + + self.addEvent('notification', self.notify.bind(self)) + + }, + + request: function(){ + var self = this; + + Api.request('core_notifier.listener', { + 'initialDelay': 100, + 'delay': 3000, + 'onComplete': self.processData.bind(self) + }).startTimer() + + }, + + notify: function(data){ + var self = this; + + }, + + processData: function(json){ + var self = this; + + Array.each(json.result, function(result){ + self.fireEvent(result.type, result.data) + }) + } + +}); + +window.Notification = new NotificationBase(); diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index 3c2bfe6..5021296 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -1,7 +1,8 @@ -from couchpotato.api import addApiView +from couchpotato import addView from couchpotato.environment import Env from flask.helpers import send_from_directory import os.path +import re class Plugin(): @@ -12,20 +13,23 @@ class Plugin(): def getName(self): return self.__class__.__name__ - def registerStatic(self, file_path): + def registerStatic(self, plugin_file): - class_name = self.__class__.__name__.lower() - self.plugin_file = file_path - path = class_name + '.static/' + # Register plugin path + self.plugin_path = os.path.dirname(plugin_file) - addApiView(path + '', self.showStatic, static = True) + # Get plugin_name from PluginName + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', self.__class__.__name__) + class_name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + + path = 'static/' + class_name + '/' + addView(path + '', self.showStatic, static = True) return path def showStatic(self, file = ''): - plugin_dir = os.path.dirname(self.plugin_file) - dir = os.path.join(plugin_dir, 'static') + dir = os.path.join(self.plugin_path, 'static') return send_from_directory(dir, file) diff --git a/couchpotato/core/plugins/wizard/__init__.py b/couchpotato/core/plugins/wizard/__init__.py new file mode 100644 index 0000000..807c32c --- /dev/null +++ b/couchpotato/core/plugins/wizard/__init__.py @@ -0,0 +1,22 @@ +from .main import Wizard + +def start(): + return Wizard() + +config = [{ + 'name': 'global', + 'groups': [ + { + 'tab': 'general', + 'name': 'advanced', + 'options': [ + { + 'name': 'show_wizard', + 'label': 'Run the wizard', + 'default': True, + 'type': 'bool', + }, + ], + }, + ], +}] diff --git a/couchpotato/core/plugins/wizard/main.py b/couchpotato/core/plugins/wizard/main.py new file mode 100644 index 0000000..66f4294 --- /dev/null +++ b/couchpotato/core/plugins/wizard/main.py @@ -0,0 +1,13 @@ +from couchpotato.core.event import fireEvent +from couchpotato.core.logger import CPLog +from couchpotato.core.plugins.base import Plugin + +log = CPLog(__name__) + + +class Wizard(Plugin): + + def __init__(self): + path = self.registerStatic(__file__) + fireEvent('register_script', path + 'spotlight.js') + fireEvent('register_script', path + 'wizard.js') diff --git a/couchpotato/core/plugins/wizard/static/spotlight.js b/couchpotato/core/plugins/wizard/static/spotlight.js new file mode 100644 index 0000000..0f249e2 --- /dev/null +++ b/couchpotato/core/plugins/wizard/static/spotlight.js @@ -0,0 +1,306 @@ +/* +--- +description: Fill the empty space around elements, creating a spotlight effect. + +license: GPL v3.0 + +authors: +- Ruud Burger + +requires: +- core/1.3: [Class.Extras, Element.Dimensions] + +provides: [Spotlight] + +... +*/ + +var Spotlight = new Class({ + + Implements: [Options], + + options: { + 'fillClass': 'spotlight_fill', + 'fillColor': [255,255,255], + 'fillOpacity': 1, + 'parent': null, + 'inject': null, + 'soften': 10 + }, + + initialize: function(elements, options){ + var self = this; + self.setOptions(options); + + self.setElements(elements); + self.clean(); + + }, + + clean: function(){ + var self = this; + + self.range = []; self.fills = []; self.edges = []; + + self.vert = []; + self.vert_el = []; + + self.top = []; self.left = []; + self.width = []; self.height = []; + }, + + setElements: function(elements){ + this.elements = elements; + }, + + addElement: function(element){ + this.elements.include(element); + }, + + create: function(){ + var self = this; + + self.destroy(); + + var page_c = $(self.options.parent || window).getScrollSize(); + var soften = self.options.soften; + + // Get the top and bottom of all the elements + self.elements.each(function(el, nr){ + var c = el.getCoordinates(); + + if(c.top > 0 && nr == 0){ + self.vert.append([0]); + self.vert_el.append([null]); + } + + // Top + self.vert.append([c.top-soften]); + self.vert_el.append([el]); + + // Bottom + self.vert.append([c.top+c.height+soften]); + self.vert_el.append([el]); + + // Add it to range, for later calculation from left to right + self.range.append([{ + 'el': el, + 'top': c.top-soften, + 'bottom': c.top+c.height+soften, + 'left': c.left-soften, + 'right': c.left+c.width+soften + }]) + + // Create soft edge around element + self.soften(el); + + }); + + if(self.elements.length == 0){ + self.vert.append([0]); + self.vert_el.append([null]); + } + + // Reorder + var vert = self.vert.clone().sort(self.numberSort) // Use custom sort function because apparently 100 is less then 20.. + vert_el_new = [], vert_new = []; + vert.each(function(v){ + var old_nr = self.vert.indexOf(v); + vert_el_new.append([self.vert_el[old_nr]]); + vert_new.append([v]); + + }); + self.vert = vert_new; + self.vert_el = vert_el_new; + + // Shorten vars + var vert = self.vert, + vert_el = self.vert_el; + var t, h, l, w, left, width, + row_el, cursor = 0; + + // Loop over all vertical lines + vert.each(function(v, nr){ + + // Use defaults if el == null (for first fillblock) + var c = vert_el[nr] ? vert_el[nr].getCoordinates() : { + 'left': 0, + 'top': 0, + 'width': page_c.x, + 'height': 0 + }; + + // Loop till cursor gets to parent_element.width + var fail_safe = 0; + while (cursor < page_c.x && fail_safe < 10){ + + t = vert[nr]; // Top is the same for every element in a row + h = (nr == vert.length-1) ? (page_c.y - t) : vert[nr+1] - vert[nr]; // So is hight + + // First element get special treatment + if(nr == 0){ + l = 0; + w = c.width+(2*soften); + cursor += w; + } + else { + + row_el = self.firstFromLeft(cursor, t) // First next element + left = row_el.el ? row_el.left : c.left-soften; + width = row_el.el ? row_el.left - cursor : c.left-soften; + + if(t == c.bottom+soften && !row_el.el) + width = page_c.x; + + l = cursor; + if(cursor < left){ + w = width; + cursor += w+(row_el.right - row_el.left); + } + else { + w = page_c.x-l; + cursor += w; + } + + } + + // Add it to the pile! + if(h > 0 && w > 0){ + self.top.append([t]); self.left.append([l]); + self.width.append([w]); self.height.append([h]); + } + + fail_safe++; + + } + + cursor = 0; // New line, reset cursor position + fail_safe = 0; + + }); + + // Create the fill blocks + self.top.each(self.createFillItem.bind(self)); + + }, + + createFillItem: function(top, nr){ + var self = this; + + var fill = new Element('div', { + 'class': self.options.fillClass, + 'styles': { + 'position': 'absolute', + 'background-color': 'rgba('+self.options.fillColor.join(',')+', '+self.options.fillOpacity+')', + 'display': 'block', + 'z-index': 2, + 'top': self.top[nr], + 'left': self.left[nr], + 'height': self.height[nr], + 'width': self.width[nr] + } + }).inject(self.options.inject || document.body); + + self.fills.include(fill); + }, + + // Find the first element after x,y coordinates + firstFromLeft: function(x, y){ + var self = this; + + var lowest_left = null; + var return_data = {}; + + self.range.each(function(range){ + var is_within_height_range = range.top <= y && range.bottom > y, + is_within_width_range = range.left >= x, + more_left_then_previous = range.left < lowest_left || lowest_left == null; + + if(is_within_height_range && is_within_width_range && more_left_then_previous){ + lowest_left = range.left; + return_data = range; + } + }) + + return return_data + + }, + + soften: function(el){ + var self = this; + var soften = self.options.soften; + + var c = el.getCoordinates(); + var from_color = 'rgba('+self.options.fillColor.join(',')+', '+self.options.fillOpacity+')'; + var to_color = 'rgba('+self.options.fillColor.join(',')+', 0)'; + + // Top + self.createEdge({ + 'top': c.top-soften, + 'left': c.left-soften, + 'width': c.width+(2*soften), + 'background': '-webkit-gradient(linear, left top, left bottom, from('+from_color+'), to('+to_color+'))', + 'background': '-moz-linear-gradient(top, '+from_color+', '+to_color+')' + }) + + // Right + self.createEdge({ + 'top': c.top-soften, + 'left': c.right, + 'height': c.height+(2*soften), + 'background': '-webkit-gradient(linear, left, right, from('+from_color+'), to('+to_color+'))', + 'background': '-moz-linear-gradient(right, '+from_color+', '+to_color+')' + }) + + // Bottom + self.createEdge({ + 'top': c.bottom, + 'left': c.left-soften, + 'width': c.width+(2*soften), + 'background': '-webkit-gradient(linear, left bottom, left top, from('+from_color+'), to('+to_color+'))', + 'background': '-moz-linear-gradient(bottom, '+from_color+', '+to_color+')' + }) + + // Left + self.createEdge({ + 'top': c.top-soften, + 'left': c.left-soften, + 'height': c.height+(2*soften), + 'background': '-webkit-gradient(linear, right, left, from('+from_color+'), to('+to_color+'))', + 'background': '-moz-linear-gradient(left, '+from_color+', '+to_color+')' + }) + + }, + + createEdge: function(style){ + var self = this; + + var soften = self.options.soften; + var edge = new Element('div', { + 'styles': Object.merge({ + 'position': 'absolute', + 'width': soften, + 'height': soften, + }, style) + }).inject(self.options.inject || document.body) + + self.edges.include(edge); + + }, + + destroy: function(){ + var self = this; + self.fills.each(function(fill){ + fill.destroy(); + }) + self.edges.each(function(edge){ + edge.destroy(); + }) + self.clean(); + }, + + numberSort: function (a, b) { + return a - b; + } + +}); \ No newline at end of file diff --git a/couchpotato/core/plugins/wizard/static/wizard.js b/couchpotato/core/plugins/wizard/static/wizard.js new file mode 100644 index 0000000..2c1d48c --- /dev/null +++ b/couchpotato/core/plugins/wizard/static/wizard.js @@ -0,0 +1,76 @@ +var WizardBase = new Class({ + + Implements: [Options, Events], + + initialize: function(steps){ + var self = this; + + self.steps = steps; + self.start(); + + }, + + start: function(){ + + + + }, + + nextStep: function(){ + + }, + + previousStep: function(){ + + } + +}); + +WizardBase.Screen = new Class({ + + initialize: function(data){ + var self = this; + + self.data = data; + self.create() + + }, + + create: function(){ + var self = this; + + self.el = new Element('div.') + + + }, + + destroy: function(){ + this.el.destroy(); + + return this + } + +}) + +window.Wizard = new WizardBase([ + { + 'title': 'Fill in your username and password', + 'Description': 'Outside blabla', + 'tab': 'general', + 'fields': ['username', 'password'] + }, + { + 'title': 'What do you use to download your movies', + 'answers': [ + {'name': 'nzb', 'label': 'Usenet'}, + {'name': 'torrent', 'label': 'Torrents'} + ] + }, + { + 'title': 'Do you have a login for any of the following sites', + 'tab': 'providers', + 'needs': function(){ + return self.config_nzb || self.config_torrent + } + } +]) diff --git a/couchpotato/core/providers/base.py b/couchpotato/core/providers/base.py index 7367bf2..a677a33 100644 --- a/couchpotato/core/providers/base.py +++ b/couchpotato/core/providers/base.py @@ -20,6 +20,10 @@ class NZBProvider(Provider): time_between_searches = 10 # Seconds + def isEnabled(self): + return True # nzb_downloaded is enabled check + + class TorrentProvider(Provider): type = 'torrent' diff --git a/couchpotato/core/providers/nzb/newzbin/main.py b/couchpotato/core/providers/nzb/newzbin/main.py index 3fe3a29..babf91d 100644 --- a/couchpotato/core/providers/nzb/newzbin/main.py +++ b/couchpotato/core/providers/nzb/newzbin/main.py @@ -1,8 +1,138 @@ +from couchpotato.core.event import addEvent from couchpotato.core.logger import CPLog from couchpotato.core.providers.base import NZBProvider +from dateutil.parser import parse +from urllib import urlencode +from urllib2 import URLError +import time log = CPLog(__name__) class Newzbin(NZBProvider): - pass + searchUrl = 'https://www.newzbin.com/search/' + + formatIds = { + 2: ['scr'], + 1: ['cam'], + 4: ['tc'], + 8: ['ts'], + 1024: ['r5'], + } + cat_ids = [ + ([2097152], ['1080p']), + ([524288], ['720p']), + ([262144], ['brrip']), + ([2], ['dvdr']), + ] + cat_backup_id = -1 + + def __init__(self): + addEvent('provider.nzb.search', self.search) + + def search(self, movie, quality): + + self.cleanCache(); + + results = [] + if not self.enabled() or not self.isAvailable(self.searchUrl): + return results + + formatId = self.getFormatId(type) + catId = self.getCatId(type) + + arguments = urlencode({ + 'searchaction': 'Search', + 'u_url_posts_only': '0', + 'u_show_passworded': '0', + 'q_url': 'imdb.com/title/' + movie.imdb, + 'sort': 'ps_totalsize', + 'order': 'asc', + 'u_post_results_amt': '100', + 'feed': 'rss', + 'category': '6', + 'ps_rb_video_format': str(catId), + 'ps_rb_source': str(formatId), + }) + + url = "%s?%s" % (self.searchUrl, arguments) + cacheId = str('%s %s %s' % (movie.imdb, str(formatId), str(catId))) + singleCat = True + + try: + cached = False + if(self.cache.get(cacheId)): + data = True + cached = True + log.info('Getting RSS from cache: %s.' % cacheId) + else: + log.info('Searching: %s' % url) + data = self.urlopen(url, username = self.conf('username'), password = self.conf('password')) + self.cache[cacheId] = { + 'time': time.time() + } + + except (IOError, URLError): + log.error('Failed to open %s.' % url) + return results + + if data: + try: + try: + if cached: + xml = self.cache[cacheId]['xml'] + else: + xml = self.getItems(data) + self.cache[cacheId]['xml'] = xml + except: + log.debug('No valid xml or to many requests.. You never know with %s.' % self.name) + return results + + for item in xml: + + title = self.gettextelement(item, "title") + if 'error' in title.lower(): continue + + REPORT_NS = 'http://www.newzbin.com/DTD/2007/feeds/report/'; + + # Add attributes to name + for attr in item.find('{%s}attributes' % REPORT_NS): + title += ' ' + attr.text + + id = int(self.gettextelement(item, '{%s}id' % REPORT_NS)) + size = str(int(self.gettextelement(item, '{%s}size' % REPORT_NS)) / 1024 / 1024) + ' mb' + date = str(self.gettextelement(item, '{%s}postdate' % REPORT_NS)) + + new = self.feedItem() + new.id = id + new.type = 'nzb' + new.name = title + new.date = int(time.mktime(parse(date).timetuple())) + new.size = self.parseSize(size) + new.url = str(self.gettextelement(item, '{%s}nzb' % REPORT_NS)) + new.detailUrl = str(self.gettextelement(item, 'link')) + new.content = self.gettextelement(item, "description") + new.score = self.calcScore(new, movie) + new.addbyid = True + new.checkNZB = False + + if new.date > time.time() - (int(self.config.get('NZB', 'retention')) * 24 * 60 * 60) and self.isCorrectMovie(new, movie, type, imdbResults = True, singleCategory = singleCat): + results.append(new) + log.info('Found: %s' % new.name) + + return results + except SyntaxError: + log.error('Failed to parse XML response from newzbin.com') + + return results + + def getFormatId(self, format): + for id, quality in self.formatIds.iteritems(): + for q in quality: + if q == format: + return id + + return self.catBackupId + + def isEnabled(self): + return NZBProvider.isEnabled(self) and self.conf('enabled') and self.conf('username') and self.conf('password') diff --git a/couchpotato/core/providers/nzb/newznab/__init__.py b/couchpotato/core/providers/nzb/newznab/__init__.py index 6587764..7674c97 100644 --- a/couchpotato/core/providers/nzb/newznab/__init__.py +++ b/couchpotato/core/providers/nzb/newznab/__init__.py @@ -17,6 +17,7 @@ config = [{ { 'name': 'host', 'default': 'http://nzb.su', + 'description': 'The hostname of your newznab provider, like http://nzb.su' }, { 'name': 'api_key', diff --git a/couchpotato/core/providers/nzb/newznab/main.py b/couchpotato/core/providers/nzb/newznab/main.py index 0e2f30d..c8ffbe4 100644 --- a/couchpotato/core/providers/nzb/newznab/main.py +++ b/couchpotato/core/providers/nzb/newznab/main.py @@ -1,8 +1,126 @@ +from couchpotato.core.event import addEvent +from couchpotato.core.helpers.variable import cleanHost from couchpotato.core.logger import CPLog from couchpotato.core.providers.base import NZBProvider +from dateutil.parser import parse +from urllib import urlencode +from urllib2 import URLError +import time log = CPLog(__name__) class Newznab(NZBProvider): - pass + + urls = { + 'download': 'get&id=%s%s', + 'detail': 'details&id=%s', + } + + cat_ids = [ + ([2000], ['brrip']), + ([2010], ['dvdr']), + ([2030], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']), + ([2040], ['720p', '1080p']), + ] + cat_backup_id = 2000 + + time_between_searches = 1 # Seconds + + def __init__(self): + addEvent('provider.nzb.search', self.search) + + def getUrl(self, type): + return cleanHost(self.conf('host')) + 'api?t=' + type + + def search(self, movie, quality): + + self.cleanCache(); + + results = [] + if not self.enabled() or not self.isAvailable(self.getUrl(self.searchUrl)): + return results + + catId = self.getCatId(type) + arguments = urlencode({ + 'imdbid': movie.imdb.replace('tt', ''), + 'cat': catId, + 'apikey': self.conf('apikey'), + 't': self.searchUrl, + 'extended': 1 + }) + url = "%s&%s" % (self.getUrl(self.searchUrl), arguments) + cacheId = str(movie.imdb) + '-' + str(catId) + singleCat = (len(self.catIds.get(catId)) == 1 and catId != self.catBackupId) + + try: + cached = False + if(self.cache.get(cacheId)): + data = True + cached = True + log.info('Getting RSS from cache: %s.' % cacheId) + else: + log.info('Searching: %s' % url) + data = self.urlopen(url) + self.cache[cacheId] = { + 'time': time.time() + } + + except (IOError, URLError): + log.error('Failed to open %s.' % url) + return results + + if data: + try: + try: + if cached: + xml = self.cache[cacheId]['xml'] + else: + xml = self.getItems(data) + self.cache[cacheId]['xml'] = xml + except: + log.debug('No valid xml or to many requests.' % self.name) + return results + + results = [] + for nzb in xml: + + for item in nzb: + if item.attrib.get('name') == 'size': + size = item.attrib.get('value') + elif item.attrib.get('name') == 'usenetdate': + date = item.attrib.get('value') + + new = self.feedItem() + new.id = self.gettextelement(nzb, "guid").split('/')[-1:].pop() + new.type = 'nzb' + new.name = self.gettextelement(nzb, "title") + new.date = int(time.mktime(parse(date).timetuple())) + new.size = int(size) / 1024 / 1024 + new.url = self.downloadLink(new.id) + new.detailUrl = self.detailLink(new.id) + new.content = self.gettextelement(nzb, "description") + new.score = self.calcScore(new, movie) + + if new.date > time.time() - (int(self.config.get('NZB', 'retention')) * 24 * 60 * 60) and self.isCorrectMovie(new, movie, type, imdbResults = True, singleCategory = singleCat): + results.append(new) + log.info('Found: %s' % new.name) + + return results + except SyntaxError: + log.error('Failed to parse XML response from Newznab') + return False + + return results + + def isEnabled(self): + return NZBProvider.isEnabled(self) and self.conf('enabled') and self.conf('host') and self.conf('apikey') + + def getApiExt(self): + return '&apikey=%s' % self.conf('apikey') + + def downloadLink(self, id): + return self.getUrl(self.downloadUrl) % (id, self.getApiExt()) + + def detailLink(self, id): + return self.getUrl(self.detailUrl) % id diff --git a/couchpotato/core/providers/nzb/nzbmatrix/__init__.py b/couchpotato/core/providers/nzb/nzbmatrix/__init__.py index 201cfcc..376c125 100644 --- a/couchpotato/core/providers/nzb/nzbmatrix/__init__.py +++ b/couchpotato/core/providers/nzb/nzbmatrix/__init__.py @@ -20,7 +20,7 @@ config = [{ }, { 'name': 'api_key', - 'default': '9b939aee0aaafc12a65bf448e4af9543', + 'default': '', 'label': 'Api Key', }, ], diff --git a/couchpotato/core/providers/nzb/nzbmatrix/main.py b/couchpotato/core/providers/nzb/nzbmatrix/main.py index fe7d80a..95b97d3 100644 --- a/couchpotato/core/providers/nzb/nzbmatrix/main.py +++ b/couchpotato/core/providers/nzb/nzbmatrix/main.py @@ -1,5 +1,10 @@ +from couchpotato.core.event import addEvent from couchpotato.core.logger import CPLog from couchpotato.core.providers.base import NZBProvider +from dateutil.parser import parse +from urllib import urlencode +from urllib2 import URLError +import time log = CPLog(__name__) @@ -20,12 +25,10 @@ class NZBMatrix(NZBProvider): ] cat_backup_id = 2 - def __init__(self, config): - log.info('Using NZBMatrix provider') + def __init__(self): + addEvent('provider.nzb.search', self.search) - self.config = config - - def find(self, movie, quality, type, retry = False): + def search(self, movie, quality): self.cleanCache(); @@ -113,4 +116,4 @@ class NZBMatrix(NZBProvider): return '&username=%s&apikey=%s' % (self.conf('username'), self.conf('apikey')) def isEnabled(self): - return self.conf('enabled') and self.conf('username') and self.conf('apikey') + return NZBProvider.isEnabled(self) and self.conf('enabled') and self.conf('username') and self.conf('apikey') diff --git a/couchpotato/core/providers/nzb/nzbmatrix/nzbmatrix.py b/couchpotato/core/providers/nzb/nzbmatrix/nzbmatrix.py deleted file mode 100644 index 905a00d..0000000 --- a/couchpotato/core/providers/nzb/nzbmatrix/nzbmatrix.py +++ /dev/null @@ -1,124 +0,0 @@ -from app.config.cplog import CPLog -from app.lib.provider.yarr.base import nzbBase -from dateutil.parser import parse -from urllib import urlencode -from urllib2 import URLError -import time - -log = CPLog(__name__) - -class nzbMatrix(nzbBase): - """Api for NZBMatrix""" - - name = 'NZBMatrix' - downloadUrl = 'https://api.nzbmatrix.com/v1.1/download.php?id=%s%s' - detailUrl = 'https://nzbmatrix.com/nzb-details.php?id=%s&hit=1' - searchUrl = 'http://rss.nzbmatrix.com/rss.php' - - catIds = { - 42: ['720p', '1080p'], - 2: ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr'], - 54: ['brrip'], - 1: ['dvdr'] - } - catBackupId = 2 - - timeBetween = 10 # Seconds - - def __init__(self, config): - log.info('Using NZBMatrix provider') - - self.config = config - - def conf(self, option): - return self.config.get('NZBMatrix', option) - - def enabled(self): - return self.conf('enabled') and self.config.get('NZB', 'enabled') and self.conf('username') and self.conf('apikey') - - def find(self, movie, quality, type, retry = False): - - self.cleanCache(); - - results = [] - if not self.enabled() or not self.isAvailable(self.searchUrl): - return results - - catId = self.getCatId(type) - arguments = urlencode({ - 'term': movie.imdb, - 'subcat': catId, - 'username': self.conf('username'), - 'apikey': self.conf('apikey'), - 'searchin': 'weblink', - 'english': 1 if self.conf('english') else 0, - }) - url = "%s?%s" % (self.searchUrl, arguments) - cacheId = str(movie.imdb) + '-' + str(catId) - singleCat = (len(self.catIds.get(catId)) == 1 and catId != self.catBackupId) - - try: - cached = False - if(self.cache.get(cacheId)): - data = True - cached = True - log.info('Getting RSS from cache: %s.' % cacheId) - else: - log.info('Searching: %s' % url) - data = self.urlopen(url) - self.cache[cacheId] = { - 'time': time.time() - } - - except (IOError, URLError): - log.error('Failed to open %s.' % url) - return results - - if data: - try: - try: - if cached: - xml = self.cache[cacheId]['xml'] - else: - xml = self.getItems(data) - self.cache[cacheId]['xml'] = xml - except: - log.debug('No valid xml or to many requests.. You never know with %s.' % self.name) - return results - - for nzb in xml: - - title = self.gettextelement(nzb, "title") - if 'error' in title.lower(): continue - - id = int(self.gettextelement(nzb, "link").split('&')[0].partition('id=')[2]) - size = self.gettextelement(nzb, "description").split('
')[2].split('> ')[1] - date = str(self.gettextelement(nzb, "description").split('
')[3].partition('Added: ')[2]) - - new = self.feedItem() - new.id = id - new.type = 'nzb' - new.name = title - new.date = int(time.mktime(parse(date).timetuple())) - new.size = self.parseSize(size) - new.url = self.downloadLink(id) - new.detailUrl = self.detailLink(id) - new.content = self.gettextelement(nzb, "description") - new.score = self.calcScore(new, movie) - new.checkNZB = True - - if new.date > time.time() - (int(self.config.get('NZB', 'retention')) * 24 * 60 * 60): - if self.isCorrectMovie(new, movie, type, imdbResults = True, singleCategory = singleCat): - results.append(new) - log.info('Found: %s' % new.name) - else: - log.info('Found outside retention: %s' % new.name) - - return results - except SyntaxError: - log.error('Failed to parse XML response from NZBMatrix.com') - - return results - - def getApiExt(self): - return '&username=%s&apikey=%s' % (self.conf('username'), self.conf('apikey')) diff --git a/couchpotato/core/providers/nzb/nzbs/main.py b/couchpotato/core/providers/nzb/nzbs/main.py index fb4742c..e328cda 100644 --- a/couchpotato/core/providers/nzb/nzbs/main.py +++ b/couchpotato/core/providers/nzb/nzbs/main.py @@ -1,8 +1,122 @@ +from couchpotato.core.event import addEvent from couchpotato.core.logger import CPLog from couchpotato.core.providers.base import NZBProvider +from dateutil.parser import parse +from urllib import urlencode +from urllib2 import URLError +import time log = CPLog(__name__) class Nzbs(NZBProvider): - pass + + urls = { + 'download': 'http://nzbs.org/index.php?action=getnzb&nzbid=%s%s', + 'nfo': 'http://nzbs.org/index.php?action=view&nzbid=%s&nfo=1', + 'detail': 'http://nzbs.org/index.php?action=view&nzbid=%s', + 'api': 'http://nzbs.org/rss.php', + } + + cat_ids = [ + ([4], ['720p', '1080p']), + ([2], ['cam', 'ts', 'dvdrip', 'tc', 'brrip', 'r5', 'scr']), + ([9], ['dvdr']), + ] + cat_backup_id = 't2' + + time_between_searches = 3 # Seconds + + def __init__(self): + addEvent('provider.nzb.search', self.search) + + def search(self, movie, quality): + + self.cleanCache(); + + results = [] + if not self.enabled() or not self.isAvailable(self.apiUrl + '?test' + self.getApiExt()): + return results + + catId = self.getCatId(type) + arguments = urlencode({ + 'action':'search', + 'q': self.toSearchString(movie.name), + 'catid': catId, + 'i': self.conf('id'), + 'h': self.conf('key'), + 'age': self.config.get('NZB', 'retention') + }) + url = "%s?%s" % (self.apiUrl, arguments) + cacheId = str(movie.imdb) + '-' + str(catId) + singleCat = (len(self.catIds.get(catId)) == 1 and catId != self.catBackupId) + + try: + cached = False + if(self.cache.get(cacheId)): + data = True + cached = True + log.info('Getting RSS from cache: %s.' % cacheId) + else: + log.info('Searching: %s' % url) + data = self.urlopen(url) + self.cache[cacheId] = { + 'time': time.time() + } + except (IOError, URLError): + log.error('Failed to open %s.' % url) + return results + + if data: + log.debug('Parsing NZBs.org RSS.') + try: + try: + if cached: + xml = self.cache[cacheId]['xml'] + else: + xml = self.getItems(data) + self.cache[cacheId]['xml'] = xml + except: + retry = False + if retry == False: + log.error('No valid xml, to many requests? Try again in 15sec.') + time.sleep(15) + return self.find(movie, quality, type, retry = True) + else: + log.error('Failed again.. disable %s for 15min.' % self.name) + self.available = False + return results + + for nzb in xml: + + id = int(self.gettextelement(nzb, "link").partition('nzbid=')[2]) + + size = self.gettextelement(nzb, "description").split('
')[1].split('">')[1] + + new = self.feedItem() + new.id = id + new.type = 'nzb' + new.name = self.gettextelement(nzb, "title") + new.date = int(time.mktime(parse(self.gettextelement(nzb, "pubDate")).timetuple())) + new.size = self.parseSize(size) + new.url = self.downloadLink(id) + new.detailUrl = self.detailLink(id) + new.content = self.gettextelement(nzb, "description") + new.score = self.calcScore(new, movie) + + if self.isCorrectMovie(new, movie, type, singleCategory = singleCat): + results.append(new) + log.info('Found: %s' % new.name) + + return results + except SyntaxError: + log.error('Failed to parse XML response from NZBs.org') + return False + + return results + + def isEnabled(self): + return NZBProvider.isEnabled(self) and self.conf('enabled') and self.conf('id') and self.conf('key') + + def getApiExt(self): + return '&i=%s&h=%s' % (self.conf('id'), self.conf('key')) diff --git a/couchpotato/core/providers/torrent/thepiratebay/main.py b/couchpotato/core/providers/torrent/thepiratebay/main.py index 2c2ce6f..c2e5636 100644 --- a/couchpotato/core/providers/torrent/thepiratebay/main.py +++ b/couchpotato/core/providers/torrent/thepiratebay/main.py @@ -5,4 +5,144 @@ log = CPLog(__name__) class ThePirateBay(TorrentProvider): - pass + + urls = { + 'download': 'http://torrents.thepiratebay.org/%s/%s.torrent', + 'nfo': 'http://thepiratebay.org/torrent/%s', + 'detail': 'http://thepiratebay.org/torrent/%s', + 'search': 'http://thepiratebay.org/search/%s/0/7/%d', + } + + cat_ids = [ + ([207], ['720p', '1080p']), + ([200], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr', 'brrip']), + ([202], ['dvdr']) + ] + cat_backup_id = 200 + + ignore_string = { + '720p': ' -brrip -bdrip', + '1080p': ' -brrip -bdrip' + } + + def __init__(self): + pass + + def find(self, movie, quality, type): + + results = [] + if not self.enabled() or not self.isAvailable(self.apiUrl): + return results + + url = self.apiUrl % (quote_plus(self.toSearchString(movie.name + ' ' + quality) + self.makeIgnoreString(type)), self.getCatId(type)) + + log.info('Searching: %s' % url) + + try: + data = urllib2.urlopen(url, timeout = self.timeout).read() + except (IOError, URLError): + log.error('Failed to open %s.' % url) + return results + + try: + tables = SoupStrainer('table') + html = BeautifulSoup(data, parseOnlyThese = tables) + resultTable = html.find('table', attrs = {'id':'searchResult'}) + for result in resultTable.findAll('tr'): + details = result.find('a', attrs = {'class':'detLink'}) + if details: + href = re.search('/(?P\d+)/', details['href']) + id = href.group('id') + name = self.toSaveString(details.contents[0]) + desc = result.find('font', attrs = {'class':'detDesc'}).contents[0].split(',') + date = '' + size = 0 + for item in desc: + # Weird date stuff + if 'uploaded' in item.lower(): + date = item.replace('Uploaded', '') + date = date.replace('Today', '') + + # Do something with yesterday + yesterdayMinus = 0 + if 'Y-day' in date: + date = date.replace('Y-day', '') + yesterdayMinus = 86400 + + datestring = date.replace(' ', ' ').strip() + date = int(time.mktime(parse(datestring).timetuple())) - yesterdayMinus + # size + elif 'size' in item.lower(): + size = item.replace('Size', '') + + seedleech = [] + for td in result.findAll('td'): + try: + seedleech.append(int(td.contents[0])) + except ValueError: + pass + + seeders = 0 + leechers = 0 + if len(seedleech) == 2 and seedleech[0] > 0 and seedleech[1] > 0: + seeders = seedleech[0] + leechers = seedleech[1] + + # to item + new = self.feedItem() + new.id = id + new.type = 'torrent' + new.name = name + new.date = date + new.size = self.parseSize(size) + new.seeders = seeders + new.leechers = leechers + new.url = self.downloadLink(id, name) + new.score = self.calcScore(new, movie) + self.uploader(result) + (seeders / 10) + + if seeders > 0 and (new.date + (int(self.conf('wait')) * 60 * 60) < time.time()) and Qualities.types.get(type).get('minSize') <= new.size: + new.detailUrl = self.detailLink(id) + new.content = self.getInfo(new.detailUrl) + if self.isCorrectMovie(new, movie, type): + results.append(new) + log.info('Found: %s' % new.name) + + return results + + except AttributeError: + log.debug('No search results found.') + + return [] + + def makeIgnoreString(self, type): + ignore = self.ignoreString.get(type) + return ignore if ignore else '' + + def uploader(self, html): + score = 0 + if html.find('img', attr = {'alt':'VIP'}): + score += 3 + if html.find('img', attr = {'alt':'Trusted'}): + score += 1 + return score + + + def getInfo(self, url): + log.debug('Getting info: %s' % url) + try: + data = urllib2.urlopen(url, timeout = self.timeout).read() + pass + except IOError: + log.error('Failed to open %s.' % url) + return '' + + div = SoupStrainer('div') + html = BeautifulSoup(data, parseOnlyThese = div) + html = html.find('div', attrs = {'class':'nfo'}) + return str(html).decode("utf-8", "replace") + + def downloadLink(self, id, name): + return self.downloadUrl % (id, quote_plus(name)) + + def isEnabled(self): + return self.conf('enabled') and TorrentProvider.isEnabled(self) diff --git a/couchpotato/core/settings/__init__.py b/couchpotato/core/settings/__init__.py index 2c44702..0d0b866 100644 --- a/couchpotato/core/settings/__init__.py +++ b/couchpotato/core/settings/__init__.py @@ -45,7 +45,7 @@ class Settings(): for option, value in options.iteritems(): self.setDefault(section_name, option, value) - self.log.debug('Defaults for "%s": %s' % (section_name, options)) + #self.log.debug('Defaults for "%s": %s' % (section_name, options)) if save: self.save(self) diff --git a/couchpotato/static/scripts/library/mootools_more.js b/couchpotato/static/scripts/library/mootools_more.js index 58767dc..6a49319 100644 --- a/couchpotato/static/scripts/library/mootools_more.js +++ b/couchpotato/static/scripts/library/mootools_more.js @@ -1,6 +1,6 @@ // MooTools: the javascript framework. -// Load this file's selection again by visiting: http://mootools.net/more/2b832e45b9bf2f9e5fdbdafc9b16febf -// Or build this file again with packager using: packager build More/Element.Forms More/Element.Delegation More/Element.Shortcuts More/Fx.Slide More/Sortables More/Request.JSONP More/Spinner +// Load this file's selection again by visiting: http://mootools.net/more/1e3edb90c5e02d9b9013b54e6ab001ea +// Or build this file again with packager using: packager build More/Element.Forms More/Element.Delegation More/Element.Shortcuts More/Fx.Slide More/Sortables More/Request.JSONP More/Request.Periodical More/Spinner /* --- @@ -1757,6 +1757,59 @@ Request.JSONP.request_map = {}; /* --- +script: Request.Periodical.js + +name: Request.Periodical + +description: Requests the same URL to pull data from a server but increases the intervals if no data is returned to reduce the load + +license: MIT-style license + +authors: + - Christoph Pojer + +requires: + - Core/Request + - /MooTools.More + +provides: [Request.Periodical] + +... +*/ + +Request.implement({ + + options: { + initialDelay: 5000, + delay: 5000, + limit: 60000 + }, + + startTimer: function(data){ + var fn = function(){ + if (!this.running) this.send({data: data}); + }; + this.lastDelay = this.options.initialDelay; + this.timer = fn.delay(this.lastDelay, this); + this.completeCheck = function(response){ + clearTimeout(this.timer); + this.lastDelay = (response) ? this.options.delay : (this.lastDelay + this.options.delay).min(this.options.limit); + this.timer = fn.delay(this.lastDelay, this); + }; + return this.addEvent('complete', this.completeCheck); + }, + + stopTimer: function(){ + clearTimeout(this.timer); + return this.removeEvent('complete', this.completeCheck); + } + +}); + + +/* +--- + script: Class.Refactor.js name: Class.Refactor diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js index 0759133..9a16ec3 100644 --- a/couchpotato/static/scripts/page/settings.js +++ b/couchpotato/static/scripts/page/settings.js @@ -9,7 +9,8 @@ Page.Settings = new Class({ 'general': {}, 'providers': {}, 'downloaders': {}, - 'notifications': {} + 'notifications': {}, + 'renamer': {} }, open: function(action, params){ @@ -114,15 +115,16 @@ Page.Settings = new Class({ section.groups.sortBy('order').each(function(group){ // Create the group - var group_el = self.createGroup(group).inject(self.tabs[group.tab].content); - - self.tabs[group.tab].groups[group.name] = group_el + if(!self.tabs[group.tab].groups[group.name]){ + var group_el = self.createGroup(group).inject(self.tabs[group.tab].content); + self.tabs[group.tab].groups[group.name] = group_el + } // Add options to group group.options.sortBy('order').each(function(option){ var class_name = (option.type || 'string').capitalize(); var input = new Option[class_name](self, section_name, option.name, option); - input.inject(group_el); + input.inject(self.tabs[group.tab].groups[group.name]); input.fireEvent('injected') }); @@ -142,7 +144,7 @@ Page.Settings = new Class({ if(self.tabs[tab_name] && self.tabs[tab_name].tab) return self.tabs[tab_name].tab - var label = (tab.label || tab.name).capitalize() + var label = (tab.label || tab.name || tab_name).capitalize() var tab_el = new Element('li').adopt( new Element('a', { 'href': '/'+self.name+'/'+tab_name+'/', diff --git a/couchpotato/templates/_desktop.html b/couchpotato/templates/_desktop.html index 1f9e802..f5c8c48 100644 --- a/couchpotato/templates/_desktop.html +++ b/couchpotato/templates/_desktop.html @@ -27,9 +27,9 @@ {% for url in fireEvent('clientscript.get_scripts', as_html = True, single = True) %} - {% endfor %} + {% endfor %} {% for url in fireEvent('clientscript.get_styles', as_html = True, single = True) %} - {% endfor %} + {% endfor %}