Error:
+ #
+ #
+ response = self.urlopen(server, headers = headers, timeout = 3, show_error = False)
+
+ if 'OK' in response:
+ log.debug('Returned from non-JSON-type request %s: %s', (host, response))
+ # manually fake expected response array
+ return [{'result': 'OK'}]
+ else:
+ log.error('Returned from non-JSON-type request %s: %s', (host, response))
+ # manually fake expected response array
+ return [{'result': 'Error'}]
+
+ except (MaxRetryError, Timeout, ConnectionError):
+ log.info2('Couldn\'t send request to Kodi, assuming it\'s turned off')
+ return [{'result': 'Error'}]
+ except:
+ log.error('Failed sending non-JSON-type request to Kodi: %s', traceback.format_exc())
+ return [{'result': 'Error'}]
+
+ def request(self, host, do_requests):
+ server = 'http://%s/jsonrpc' % host
+
+ data = []
+ for req in do_requests:
+ method, id, kwargs = req
+
+ data.append({
+ 'method': method,
+ 'params': kwargs,
+ 'jsonrpc': '2.0',
+ 'id': id if id else method,
+ })
+ data = json.dumps(data)
+
+ headers = {
+ 'Content-Type': 'application/json',
+ }
+
+ if self.conf('password'):
+ base64string = base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password'))).replace('\n', '')
+ headers['Authorization'] = 'Basic %s' % base64string
+
+ try:
+ log.debug('Sending request to %s: %s', (host, data))
+ response = self.getJsonData(server, headers = headers, data = data, timeout = 3, show_error = False)
+ log.debug('Returned from request %s: %s', (host, response))
+
+ return response
+ except (MaxRetryError, Timeout, ConnectionError):
+ log.info2('Couldn\'t send request to Kodi, assuming it\'s turned off')
+ return []
+ except:
+ log.error('Failed sending request to Kodi: %s', traceback.format_exc())
+ return []
+
+
+config = [{
+ 'name': 'xbmc',
+ 'groups': [
+ {
+ 'tab': 'notifications',
+ 'list': 'notification_providers',
+ 'name': 'xbmc',
+ 'label': 'Kodi',
+ 'description': 'v14 (Helix), v15 (Isengard)',
+ 'options': [
+ {
+ 'name': 'enabled',
+ 'default': 0,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'host',
+ 'default': 'localhost:8080',
+ },
+ {
+ 'name': 'username',
+ 'default': 'xbmc',
+ },
+ {
+ 'name': 'password',
+ 'default': '',
+ 'type': 'password',
+ },
+ {
+ 'name': 'only_first',
+ 'default': 0,
+ 'type': 'bool',
+ 'advanced': True,
+ 'description': 'Only update the first host when movie snatched, useful for synced Kodi',
+ },
+ {
+ 'name': 'remote_dir_scan',
+ 'label': 'Remote Folder Scan',
+ 'default': 0,
+ 'type': 'bool',
+ 'advanced': True,
+ 'description': ('Only scan new movie folder at remote Kodi servers.', 'Useful if the Kodi path is different from the path CPS uses.'),
+ },
+ {
+ 'name': 'force_full_scan',
+ 'label': 'Always do a full scan',
+ 'default': 0,
+ 'type': 'bool',
+ 'advanced': True,
+ 'description': ('Do a full scan instead of only the new movie.', 'Useful if the Kodi path is different from the path CPS uses.'),
+ },
+ {
+ 'name': 'on_snatch',
+ 'default': False,
+ 'type': 'bool',
+ 'advanced': True,
+ 'description': 'Also send message when movie is snatched.',
+ },
+ ],
+ }
+ ],
+}]
diff --git a/couchpotato/core/notifications/xbmc/__init__.py b/couchpotato/core/notifications/xbmc/__init__.py
deleted file mode 100644
index 0753c82aa5..0000000000
--- a/couchpotato/core/notifications/xbmc/__init__.py
+++ /dev/null
@@ -1,37 +0,0 @@
-from .main import XBMC
-
-def start():
- return XBMC()
-
-config = [{
- 'name': 'xbmc',
- 'groups': [
- {
- 'tab': 'notifications',
- 'list': 'notification_providers',
- 'name': 'xbmc',
- 'label': 'XBMC',
- 'description': 'v11 (Eden) and v12 (Frodo)',
- 'options': [
- {
- 'name': 'enabled',
- 'default': 0,
- 'type': 'enabler',
- },
- {
- 'name': 'host',
- 'default': 'localhost:8080',
- },
- {
- 'name': 'username',
- 'default': 'xbmc',
- },
- {
- 'name': 'password',
- 'default': '',
- 'type': 'password',
- },
- ],
- }
- ],
-}]
diff --git a/couchpotato/core/notifications/xbmc/main.py b/couchpotato/core/notifications/xbmc/main.py
deleted file mode 100755
index a1987bfa13..0000000000
--- a/couchpotato/core/notifications/xbmc/main.py
+++ /dev/null
@@ -1,189 +0,0 @@
-from couchpotato.core.helpers.variable import splitString
-from couchpotato.core.logger import CPLog
-from couchpotato.core.notifications.base import Notification
-from flask.helpers import json
-import base64
-import traceback
-import urllib
-
-log = CPLog(__name__)
-
-
-class XBMC(Notification):
-
- listen_to = ['renamer.after']
- use_json_notifications = {}
- couch_logo_url = 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/xbmc-notify.png'
-
- def notify(self, message = '', data = {}, listener = None):
- if self.isDisabled(): return
-
- hosts = splitString(self.conf('host'))
-
- successful = 0
- for host in hosts:
-
- if self.use_json_notifications.get(host) is None:
- self.getXBMCJSONversion(host, message = message)
-
- if self.use_json_notifications.get(host):
- response = self.request(host, [
- ('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.couch_logo_url}),
- ('VideoLibrary.Scan', {}),
- ])
- else:
- response = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message})
- response += self.request(host, [('VideoLibrary.Scan', {})])
-
- try:
- for result in response:
- if (result.get('result') and result['result'] == 'OK'):
- successful += 1
- elif (result.get('error')):
- log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
-
- except:
- log.error('Failed parsing results: %s', traceback.format_exc())
-
- return successful == len(hosts) * 2
-
- def getXBMCJSONversion(self, host, message = ''):
-
- success = False
-
- # XBMC JSON-RPC version request
- response = self.request(host, [
- ('JSONRPC.Version', {})
- ])
- for result in response:
- if (result.get('result') and type(result['result']['version']).__name__ == 'int'):
- # only v2 and v4 return an int object
- # v6 (as of XBMC v12(Frodo)) is required to send notifications
- xbmc_rpc_version = str(result['result']['version'])
-
- log.debug('XBMC JSON-RPC Version: %s ; Notifications by JSON-RPC only supported for v6 [as of XBMC v12(Frodo)]', xbmc_rpc_version)
-
- # disable JSON use
- self.use_json_notifications[host] = False
-
- # send the text message
- resp = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message})
- for result in resp:
- if (result.get('result') and result['result'] == 'OK'):
- log.debug('Message delivered successfully!')
- success = True
- break
- elif (result.get('error')):
- log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
- break
-
- elif (result.get('result') and type(result['result']['version']).__name__ == 'dict'):
- # XBMC JSON-RPC v6 returns an array object containing
- # major, minor and patch number
- xbmc_rpc_version = str(result['result']['version']['major'])
- xbmc_rpc_version += '.' + str(result['result']['version']['minor'])
- xbmc_rpc_version += '.' + str(result['result']['version']['patch'])
-
- log.debug('XBMC JSON-RPC Version: %s', xbmc_rpc_version)
-
- # ok, XBMC version is supported
- self.use_json_notifications[host] = True
-
- # send the text message
- resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message, 'image':self.couch_logo_url})])
- for result in resp:
- if (result.get('result') and result['result'] == 'OK'):
- log.debug('Message delivered successfully!')
- success = True
- break
- elif (result.get('error')):
- log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
- break
-
- # error getting version info (we do have contact with XBMC though)
- elif (result.get('error')):
- log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
-
- log.debug('Use JSON notifications: %s ', self.use_json_notifications)
-
- return success
-
- def notifyXBMCnoJSON(self, host, data):
-
- server = 'http://%s/xbmcCmds/' % host
-
- # Notification(title, message [, timeout , image])
- cmd = "xbmcHttp?command=ExecBuiltIn(Notification(%s,%s,'',%s))" % (urllib.quote(data['title']), urllib.quote(data['message']), urllib.quote(self.couch_logo_url))
- server += cmd
-
- # I have no idea what to set to, just tried text/plain and seems to be working :)
- headers = {
- 'Content-Type': 'text/plain',
- }
-
- # authentication support
- if self.conf('password'):
- base64string = base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password'))).replace('\n', '')
- headers['Authorization'] = 'Basic %s' % base64string
-
- try:
- log.debug('Sending non-JSON-type request to %s: %s', (host, data))
-
- # response wil either be 'OK':
- #
- #
OK
- #
- #
- # or 'Error':
- #
- #
Error:
- #
- #
- response = self.urlopen(server, headers = headers)
-
- if 'OK' in response:
- log.debug('Returned from non-JSON-type request %s: %s', (host, response))
- # manually fake expected response array
- return [{'result': 'OK'}]
- else:
- log.error('Returned from non-JSON-type request %s: %s', (host, response))
- # manually fake expected response array
- return [{'result': 'Error'}]
-
- except:
- log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc())
- return [{'result': 'Error'}]
-
- def request(self, host, requests):
- server = 'http://%s/jsonrpc' % host
-
- data = []
- for req in requests:
- method, kwargs = req
- data.append({
- 'method': method,
- 'params': kwargs,
- 'jsonrpc': '2.0',
- 'id': method,
- })
- data = json.dumps(data)
-
- headers = {
- 'Content-Type': 'application/json',
- }
-
- if self.conf('password'):
- base64string = base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password'))).replace('\n', '')
- headers['Authorization'] = 'Basic %s' % base64string
-
- try:
- log.debug('Sending request to %s: %s', (host, data))
- rdata = self.urlopen(server, headers = headers, params = data, multipart = True)
- response = json.loads(rdata)
- log.debug('Returned from request %s: %s', (host, response))
-
- return response
- except:
- log.error('Failed sending request to XBMC: %s', traceback.format_exc())
- return []
-
diff --git a/couchpotato/core/notifications/xmpp_.py b/couchpotato/core/notifications/xmpp_.py
new file mode 100644
index 0000000000..f9916cd020
--- /dev/null
+++ b/couchpotato/core/notifications/xmpp_.py
@@ -0,0 +1,96 @@
+from time import sleep
+import traceback
+
+from couchpotato.core.logger import CPLog
+from couchpotato.core.notifications.base import Notification
+import xmpp
+
+
+log = CPLog(__name__)
+
+autoload = 'Xmpp'
+
+
+class Xmpp(Notification):
+
+ def notify(self, message = '', data = None, listener = None):
+ if not data: data = {}
+
+ try:
+ jid = xmpp.protocol.JID(self.conf('username'))
+ client = xmpp.Client(jid.getDomain(), debug = [])
+
+ # Connect
+ if not client.connect(server = (self.conf('hostname'), self.conf('port'))):
+ log.error('XMPP failed: Connection to server failed.')
+ return False
+
+ # Authenticate
+ if not client.auth(jid.getNode(), self.conf('password'), resource = jid.getResource()):
+ log.error('XMPP failed: Failed to authenticate.')
+ return False
+
+ # Send message
+ client.send(xmpp.protocol.Message(to = self.conf('to'), body = message, typ = 'chat'))
+
+ # Disconnect
+ # some older servers will not send the message if you disconnect immediately after sending
+ sleep(1)
+ client.disconnect()
+
+ log.info('XMPP notifications sent.')
+ return True
+
+ except:
+ log.error('XMPP failed: %s', traceback.format_exc())
+
+ return False
+
+
+config = [{
+ 'name': 'xmpp',
+ 'groups': [
+ {
+ 'tab': 'notifications',
+ 'list': 'notification_providers',
+ 'name': 'xmpp',
+ 'label': 'XMPP',
+ 'description`': 'for Jabber, Hangouts (Google Talk), AIM...',
+ 'options': [
+ {
+ 'name': 'enabled',
+ 'default': 0,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'username',
+ 'description': 'User sending the message. For Hangouts, e-mail of a single-step authentication Google account.',
+ },
+ {
+ 'name': 'password',
+ 'type': 'Password',
+ },
+ {
+ 'name': 'hostname',
+ 'default': 'talk.google.com',
+ },
+ {
+ 'name': 'to',
+ 'description': 'Username (or e-mail for Hangouts) of the person to send the messages to.',
+ },
+ {
+ 'name': 'port',
+ 'type': 'int',
+ 'default': 5222,
+ },
+ {
+ 'name': 'on_snatch',
+ 'default': 0,
+ 'type': 'bool',
+ 'advanced': True,
+ 'description': 'Also send message when movie is snatched.',
+ },
+ ],
+ }
+ ],
+}]
diff --git a/couchpotato/core/plugins/automation.py b/couchpotato/core/plugins/automation.py
new file mode 100644
index 0000000000..e98a00a619
--- /dev/null
+++ b/couchpotato/core/plugins/automation.py
@@ -0,0 +1,105 @@
+from couchpotato.core.event import addEvent, fireEvent
+from couchpotato.core.logger import CPLog
+from couchpotato.core.plugins.base import Plugin
+from couchpotato.environment import Env
+
+log = CPLog(__name__)
+
+autoload = 'Automation'
+
+
+class Automation(Plugin):
+
+ def __init__(self):
+
+ addEvent('app.load', self.setCrons)
+
+ if not Env.get('dev'):
+ addEvent('app.load', self.addMovies)
+
+ addEvent('setting.save.automation.hour.after', self.setCrons)
+
+ def setCrons(self):
+ fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12))
+
+ def addMovies(self):
+
+ movies = fireEvent('automation.get_movies', merge = True)
+ movie_ids = []
+
+ for imdb_id in movies:
+
+ if self.shuttingDown():
+ break
+
+ prop_name = 'automation.added.%s' % imdb_id
+ added = Env.prop(prop_name, default = False)
+ if not added:
+ added_movie = fireEvent('movie.add', params = {'identifier': imdb_id}, force_readd = False, search_after = False, update_after = True, single = True)
+ if added_movie:
+ movie_ids.append(added_movie['_id'])
+ Env.prop(prop_name, True)
+
+ for movie_id in movie_ids:
+
+ if self.shuttingDown():
+ break
+
+ movie_dict = fireEvent('media.get', movie_id, single = True)
+ if movie_dict:
+ fireEvent('movie.searcher.single', movie_dict)
+
+ return True
+
+
+config = [{
+ 'name': 'automation',
+ 'order': 101,
+ 'groups': [
+ {
+ 'tab': 'automation',
+ 'name': 'automation',
+ 'label': 'Minimal movie requirements',
+ 'options': [
+ {
+ 'name': 'year',
+ 'default': 2011,
+ 'type': 'int',
+ },
+ {
+ 'name': 'votes',
+ 'default': 1000,
+ 'type': 'int',
+ },
+ {
+ 'name': 'rating',
+ 'default': 7.0,
+ 'type': 'float',
+ },
+ {
+ 'name': 'hour',
+ 'advanced': True,
+ 'default': 12,
+ 'label': 'Check every',
+ 'type': 'int',
+ 'unit': 'hours',
+ 'description': 'hours',
+ },
+ {
+ 'name': 'required_genres',
+ 'label': 'Required Genres',
+ 'default': '',
+ 'placeholder': 'Example: Action, Crime & Drama',
+ 'description': ('Ignore movies that don\'t contain at least one set of genres.', 'Sets are separated by "," and each word within a set must be separated with "&"')
+ },
+ {
+ 'name': 'ignored_genres',
+ 'label': 'Ignored Genres',
+ 'default': '',
+ 'placeholder': 'Example: Horror, Comedy & Drama & Romance',
+ 'description': 'Ignore movies that contain at least one set of genres. Sets work the same as above.'
+ },
+ ],
+ },
+ ],
+}]
diff --git a/couchpotato/core/plugins/automation/__init__.py b/couchpotato/core/plugins/automation/__init__.py
deleted file mode 100644
index b7b1ab2869..0000000000
--- a/couchpotato/core/plugins/automation/__init__.py
+++ /dev/null
@@ -1,42 +0,0 @@
-from .main import Automation
-
-def start():
- return Automation()
-
-config = [{
- 'name': 'automation',
- 'order': 101,
- 'groups': [
- {
- 'tab': 'automation',
- 'name': 'automation',
- 'label': 'Minimal movie requirements',
- 'options': [
- {
- 'name': 'year',
- 'default': 2011,
- 'type': 'int',
- },
- {
- 'name': 'votes',
- 'default': 1000,
- 'type': 'int',
- },
- {
- 'name': 'rating',
- 'default': 7.0,
- 'type': 'float',
- },
- {
- 'name': 'hour',
- 'advanced': True,
- 'default': 12,
- 'label': 'Check every',
- 'type': 'int',
- 'unit': 'hours',
- 'description': 'hours',
- },
- ],
- },
- ],
-}]
diff --git a/couchpotato/core/plugins/automation/main.py b/couchpotato/core/plugins/automation/main.py
deleted file mode 100644
index f4ede40dd4..0000000000
--- a/couchpotato/core/plugins/automation/main.py
+++ /dev/null
@@ -1,34 +0,0 @@
-from couchpotato.core.event import addEvent, fireEvent
-from couchpotato.core.logger import CPLog
-from couchpotato.core.plugins.base import Plugin
-from couchpotato.environment import Env
-
-log = CPLog(__name__)
-
-
-class Automation(Plugin):
-
- def __init__(self):
-
- fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12))
-
- if not Env.get('dev'):
- addEvent('app.load', self.addMovies)
-
- def addMovies(self):
-
- movies = fireEvent('automation.get_movies', merge = True)
- movie_ids = []
-
- for imdb_id in movies:
- prop_name = 'automation.added.%s' % imdb_id
- added = Env.prop(prop_name, default = False)
- if not added:
- added_movie = fireEvent('movie.add', params = {'identifier': imdb_id}, force_readd = False, search_after = False, update_library = True, single = True)
- if added_movie:
- movie_ids.append(added_movie['id'])
- Env.prop(prop_name, True)
-
- for movie_id in movie_ids:
- movie_dict = fireEvent('movie.get', movie_id, single = True)
- fireEvent('searcher.single', movie_dict)
diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py
index 9330631de9..abfa5fd5e6 100644
--- a/couchpotato/core/plugins/base.py
+++ b/couchpotato/core/plugins/base.py
@@ -1,174 +1,245 @@
-from StringIO import StringIO
-from couchpotato import addView
-from couchpotato.core.event import fireEvent, addEvent
-from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString
-from couchpotato.core.helpers.variable import getExt, md5
-from couchpotato.core.logger import CPLog
-from couchpotato.environment import Env
-from flask.templating import render_template_string
-from multipartpost import MultipartPostHandler
+import threading
+from urllib import quote, getproxies
from urlparse import urlparse
-import cookielib
-import glob
-import gzip
-import math
import os.path
-import re
import time
import traceback
-import urllib2
+
+from couchpotato.core.event import fireEvent, addEvent
+from couchpotato.core.helpers.encoding import ss, toSafeString, \
+ toUnicode, sp
+from couchpotato.core.helpers.variable import md5, isLocalIP, scanForPassword, tryInt, getIdentifier, \
+ randomString
+from couchpotato.core.logger import CPLog
+from couchpotato.environment import Env
+import requests
+from requests.packages.urllib3 import Timeout
+from requests.packages.urllib3.exceptions import MaxRetryError
+from tornado import template
log = CPLog(__name__)
class Plugin(object):
+ _class_name = None
+ _database = None
+ plugin_path = None
+
enabled_option = 'enabled'
- auto_register_static = True
_needs_shutdown = False
+ _running = None
+ _locks = {}
+
+ user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:40.0) Gecko/20100101 Firefox/40.0'
http_last_use = {}
+ http_last_use_queue = {}
http_time_between_calls = 0
http_failed_request = {}
http_failed_disabled = {}
+ def __new__(cls, *args, **kwargs):
+ new_plugin = super(Plugin, cls).__new__(cls)
+ new_plugin.registerPlugin()
+
+ return new_plugin
+
def registerPlugin(self):
addEvent('app.do_shutdown', self.doShutdown)
addEvent('plugin.running', self.isRunning)
+ self._running = []
- def conf(self, attr, value = None, default = None):
- return Env.setting(attr, self.getName().lower(), value = value, default = default)
-
- def getName(self):
- return self.__class__.__name__
+ # Setup database
+ if self._database:
+ addEvent('database.setup', self.databaseSetup)
- def renderTemplate(self, parent_file, template, **params):
+ def databaseSetup(self):
- template = open(os.path.join(os.path.dirname(parent_file), template), 'r').read()
- return render_template_string(template, **params)
+ for index_name in self._database:
+ klass = self._database[index_name]
- def registerStatic(self, plugin_file, add_to_head = True):
+ fireEvent('database.setup_index', index_name, klass)
- # Register plugin path
- self.plugin_path = os.path.dirname(plugin_file)
+ def conf(self, attr, value = None, default = None, section = None):
+ class_name = self.getName().lower().split(':')[0].lower()
+ return Env.setting(attr, section = section if section else class_name, value = value, default = default)
- # 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()
+ def deleteConf(self, attr):
+ return Env._settings.delete(attr, section = self.getName().lower().split(':')[0].lower())
- path = 'api/%s/static/%s/' % (Env.setting('api_key'), class_name)
- addView(path + '', self.showStatic, static = True)
+ def getName(self):
+ return self._class_name or self.__class__.__name__
- if add_to_head:
- for f in glob.glob(os.path.join(self.plugin_path, 'static', '*')):
- ext = getExt(f)
- if ext in ['js', 'css']:
- fireEvent('register_%s' % ('script' if ext in 'js' else 'style'), path + os.path.basename(f), f)
+ def setName(self, name):
+ self._class_name = name
- def showStatic(self, filename):
- d = os.path.join(self.plugin_path, 'static')
+ def renderTemplate(self, parent_file, templ, **params):
- from flask.helpers import send_from_directory
- return send_from_directory(d, filename)
+ t = template.Template(open(os.path.join(os.path.dirname(parent_file), templ), 'r').read())
+ return t.generate(**params)
def createFile(self, path, content, binary = False):
- path = ss(path)
+ path = sp(path)
self.makeDir(os.path.dirname(path))
- try:
- f = open(path, 'w+' if not binary else 'w+b')
- f.write(content)
- f.close()
- os.chmod(path, Env.getPermission('file'))
- except Exception, e:
- log.error('Unable writing to file "%s": %s', (path, e))
+ if os.path.exists(path):
+ log.debug('%s already exists, overwriting file with new version', path)
+
+ write_type = 'w+' if not binary else 'w+b'
+
+ # Stream file using response object
+ if isinstance(content, requests.models.Response):
+
+ # Write file to temp
+ with open('%s.tmp' % path, write_type) as f:
+ for chunk in content.iter_content(chunk_size = 1048576):
+ if chunk: # filter out keep-alive new chunks
+ f.write(chunk)
+ f.flush()
+
+ # Rename to destination
+ os.rename('%s.tmp' % path, path)
+
+ else:
+ try:
+ f = open(path, write_type)
+ f.write(content)
+ f.close()
+ os.chmod(path, Env.getPermission('file'))
+ except:
+ log.error('Unable to write file "%s": %s', (path, traceback.format_exc()))
+ if os.path.isfile(path):
+ os.remove(path)
def makeDir(self, path):
- path = ss(path)
+ path = sp(path)
try:
if not os.path.isdir(path):
os.makedirs(path, Env.getPermission('folder'))
return True
- except Exception, e:
+ except Exception as e:
log.error('Unable to create folder "%s": %s', (path, e))
return False
+ def deleteEmptyFolder(self, folder, show_error = True, only_clean = None):
+ folder = sp(folder)
+
+ for item in os.listdir(folder):
+ full_folder = sp(os.path.join(folder, item))
+
+ if not only_clean or (item in only_clean and os.path.isdir(full_folder)):
+
+ for subfolder, dirs, files in os.walk(full_folder, topdown = False):
+
+ try:
+ os.rmdir(subfolder)
+ except:
+ if show_error:
+ log.info2('Couldn\'t remove directory %s: %s', (subfolder, traceback.format_exc()))
+
+ try:
+ os.rmdir(folder)
+ except:
+ if show_error:
+ log.error('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc()))
+
# http request
- def urlopen(self, url, timeout = 30, params = None, headers = None, opener = None, multipart = False, show_error = True):
- url = ss(url)
+ def urlopen(self, url, timeout = 30, data = None, headers = None, files = None, show_error = True, stream = False):
+ url = quote(ss(url), safe = "%/:=&?~#+!$,;'@()*[]")
if not headers: headers = {}
- if not params: params = {}
+ if not data: data = {}
# Fill in some headers
- headers['Referer'] = headers.get('Referer', urlparse(url).hostname)
- headers['Host'] = headers.get('Host', urlparse(url).hostname)
- headers['User-Agent'] = headers.get('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:10.0.2) Gecko/20100101 Firefox/10.0.2')
+ parsed_url = urlparse(url)
+ host = '%s%s' % (parsed_url.hostname, (':' + str(parsed_url.port) if parsed_url.port else ''))
+
+ headers['Referer'] = headers.get('Referer', '%s://%s' % (parsed_url.scheme, host))
+ headers['Host'] = headers.get('Host', None)
+ headers['User-Agent'] = headers.get('User-Agent', self.user_agent)
headers['Accept-encoding'] = headers.get('Accept-encoding', 'gzip')
+ headers['Connection'] = headers.get('Connection', 'keep-alive')
+ headers['Cache-Control'] = headers.get('Cache-Control', 'max-age=0')
+
+ use_proxy = Env.setting('use_proxy')
+ proxy_url = None
+
+ if use_proxy:
+ proxy_server = Env.setting('proxy_server')
+ proxy_username = Env.setting('proxy_username')
+ proxy_password = Env.setting('proxy_password')
+
+ if proxy_server:
+ loc = "{0}:{1}@{2}".format(proxy_username, proxy_password, proxy_server) if proxy_username else proxy_server
+ proxy_url = {
+ "http": "http://"+loc,
+ "https": "https://"+loc,
+ }
+ else:
+ proxy_url = getproxies()
- host = urlparse(url).hostname
+ r = Env.get('http_opener')
# Don't try for failed requests
if self.http_failed_disabled.get(host, 0) > 0:
if self.http_failed_disabled[host] > (time.time() - 900):
log.info2('Disabled calls to %s for 15 minutes because so many failed requests.', host)
if not show_error:
- raise
+ raise Exception('Disabled calls to %s for 15 minutes because so many failed requests' % host)
else:
return ''
else:
del self.http_failed_request[host]
del self.http_failed_disabled[host]
- self.wait(host)
+ self.wait(host, url)
+ status_code = None
try:
- if multipart:
- log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
- request = urllib2.Request(url, params, headers)
-
- if opener:
- opener.add_handler(MultipartPostHandler())
- else:
- cookies = cookielib.CookieJar()
- opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), MultipartPostHandler)
-
- response = opener.open(request, timeout = timeout)
+ kwargs = {
+ 'headers': headers,
+ 'data': data if len(data) > 0 else None,
+ 'timeout': timeout,
+ 'files': files,
+ 'verify': False, #verify_ssl, Disable for now as to many wrongly implemented certificates..
+ 'stream': stream,
+ 'proxies': proxy_url,
+ }
+ method = 'post' if len(data) > 0 or files else 'get'
+
+ log.info('Opening url: %s %s, data: %s', (method, url, [x for x in data.keys()] if isinstance(data, dict) else 'with data'))
+ response = r.request(method, url, **kwargs)
+
+ status_code = response.status_code
+ if response.status_code == requests.codes.ok:
+ data = response if stream else response.content
else:
- log.info('Opening url: %s, params: %s', (url, [x for x in params.iterkeys()]))
- data = tryUrlencode(params) if len(params) > 0 else None
- request = urllib2.Request(url, data, headers)
-
- if opener:
- response = opener.open(request, timeout = timeout)
- else:
- response = urllib2.urlopen(request, timeout = timeout)
-
- # unzip if needed
- if response.info().get('Content-Encoding') == 'gzip':
- buf = StringIO(response.read())
- f = gzip.GzipFile(fileobj = buf)
- data = f.read()
- else:
- data = response.read()
+ response.raise_for_status()
self.http_failed_request[host] = 0
- except IOError:
+ except (IOError, MaxRetryError, Timeout):
if show_error:
- log.error('Failed opening url in %s: %s %s', (self.getName(), url, traceback.format_exc(1)))
+ log.error('Failed opening url in %s: %s %s', (self.getName(), url, traceback.format_exc(0)))
# Save failed requests by hosts
try:
+
+ # To many requests
+ if status_code in [429]:
+ self.http_failed_request[host] = 1
+ self.http_failed_disabled[host] = time.time()
+
if not self.http_failed_request.get(host):
self.http_failed_request[host] = 1
else:
self.http_failed_request[host] += 1
# Disable temporarily
- if self.http_failed_request[host] > 5:
+ if self.http_failed_request[host] > 5 and not isLocalIP(host):
self.http_failed_disabled[host] = time.time()
except:
@@ -180,16 +251,34 @@ def urlopen(self, url, timeout = 30, params = None, headers = None, opener = Non
return data
- def wait(self, host = ''):
- now = time.time()
+ def wait(self, host = '', url = ''):
+ if self.http_time_between_calls == 0:
+ return
- last_use = self.http_last_use.get(host, 0)
+ try:
+ if host not in self.http_last_use_queue:
+ self.http_last_use_queue[host] = []
- wait = math.ceil(last_use - now + self.http_time_between_calls)
+ self.http_last_use_queue[host].append(url)
+
+ while True and not self.shuttingDown():
+ wait = (self.http_last_use.get(host, 0) - time.time()) + self.http_time_between_calls
+
+ if self.http_last_use_queue[host][0] != url:
+ time.sleep(.1)
+ continue
+
+ if wait > 0:
+ log.debug('Waiting for %s, %d seconds', (self.getName(), max(1, wait)))
+ time.sleep(min(wait, 30))
+ else:
+ self.http_last_use_queue[host] = self.http_last_use_queue[host][1:]
+ self.http_last_use[host] = time.time()
+ break
+ except:
+ log.error('Failed handling waiting call: %s', traceback.format_exc())
+ time.sleep(self.http_time_between_calls)
- if wait > 0:
- log.debug('Waiting for %s, %d seconds', (self.getName(), wait))
- time.sleep(last_use - now + self.http_time_between_calls)
def beforeCall(self, handler):
self.isRunning('%s.%s' % (self.getName(), handler.__name__))
@@ -197,7 +286,7 @@ def beforeCall(self, handler):
def afterCall(self, handler):
self.isRunning('%s.%s' % (self.getName(), handler.__name__), False)
- def doShutdown(self):
+ def doShutdown(self, *args, **kwargs):
self.shuttingDown(True)
return True
@@ -209,9 +298,6 @@ def shuttingDown(self, value = None):
def isRunning(self, value = None, boolean = True):
- if not hasattr(self, '_running'):
- self._running = []
-
if value is None:
return self._running
@@ -223,55 +309,134 @@ def isRunning(self, value = None, boolean = True):
except:
log.error("Something went wrong when finishing the plugin function. Could not find the 'is_running' key")
-
def getCache(self, cache_key, url = None, **kwargs):
- cache_key = md5(ss(cache_key))
- cache = Env.get('cache').get(cache_key)
- if cache:
- if not Env.get('dev'): log.debug('Getting cache %s', cache_key)
- return cache
+
+ use_cache = not len(kwargs.get('data', {})) > 0 and not kwargs.get('files')
+
+ if use_cache:
+ cache_key_md5 = md5(cache_key)
+ cache = Env.get('cache').get(cache_key_md5)
+ if cache:
+ if not Env.get('dev'): log.debug('Getting cache %s', cache_key)
+ return cache
if url:
try:
cache_timeout = 300
- if kwargs.get('cache_timeout'):
+ if 'cache_timeout' in kwargs:
cache_timeout = kwargs.get('cache_timeout')
del kwargs['cache_timeout']
data = self.urlopen(url, **kwargs)
- if data:
+ if data and cache_timeout > 0 and use_cache:
self.setCache(cache_key, data, timeout = cache_timeout)
return data
except:
if not kwargs.get('show_error', True):
raise
+ log.debug('Failed getting cache: %s', (traceback.format_exc(0)))
return ''
def setCache(self, cache_key, value, timeout = 300):
+ cache_key_md5 = md5(cache_key)
log.debug('Setting cache %s', cache_key)
- Env.get('cache').set(cache_key, value, timeout)
+ Env.get('cache').set(cache_key_md5, value, timeout)
return value
- def createNzbName(self, data, movie):
- tag = self.cpTag(movie)
- return '%s%s' % (toSafeString(data.get('name')[:127 - len(tag)]), tag)
+ def createNzbName(self, data, media, unique_tag = False):
+ release_name = data.get('name')
+ tag = self.cpTag(media, unique_tag = unique_tag)
+
+ # Check if password is filename
+ name_password = scanForPassword(data.get('name'))
+ if name_password:
+ release_name, password = name_password
+ tag += '{{%s}}' % password
+ elif data.get('password'):
+ tag += '{{%s}}' % data.get('password')
- def createFileName(self, data, filedata, movie):
- name = os.path.join(self.createNzbName(data, movie))
- if data.get('type') == 'nzb' and 'DOCTYPE nzb' not in filedata and '' not in filedata:
+ max_length = 127 - len(tag) # Some filesystems don't support 128+ long filenames
+ return '%s%s' % (toSafeString(toUnicode(release_name)[:max_length]), tag)
+
+ def createFileName(self, data, filedata, media, unique_tag = False):
+ name = self.createNzbName(data, media, unique_tag = unique_tag)
+ if data.get('protocol') == 'nzb' and 'DOCTYPE nzb' not in filedata and '' not in filedata:
return '%s.%s' % (name, 'rar')
- return '%s.%s' % (name, data.get('type'))
+ return '%s.%s' % (name, data.get('protocol'))
+
+ def cpTag(self, media, unique_tag = False):
- def cpTag(self, movie):
- if Env.setting('enabled', 'renamer'):
- return '.cp(' + movie['library'].get('identifier') + ')' if movie['library'].get('identifier') else ''
+ tag = ''
+ if Env.setting('enabled', 'renamer') or unique_tag:
+ identifier = getIdentifier(media) or ''
+ unique_tag = ', ' + randomString() if unique_tag else ''
- return ''
+ tag = '.cp('
+ tag += identifier
+ tag += ', ' if unique_tag and identifier else ''
+ tag += randomString() if unique_tag else ''
+ tag += ')'
+
+ return tag if len(tag) > 7 else ''
+
+ def checkFilesChanged(self, files, unchanged_for = 60):
+ now = time.time()
+ file_too_new = False
+
+ file_time = []
+ for cur_file in files:
+
+ # File got removed while checking
+ if not os.path.isfile(cur_file):
+ file_too_new = now
+ break
+
+ # File has changed in last 60 seconds
+ file_time = self.getFileTimes(cur_file)
+ for t in file_time:
+ if t > now - unchanged_for:
+ file_too_new = tryInt(time.time() - t)
+ break
+
+ if file_too_new:
+ break
+
+ if file_too_new:
+ try:
+ time_string = time.ctime(file_time[0])
+ except:
+ try:
+ time_string = time.ctime(file_time[1])
+ except:
+ time_string = 'unknown'
+
+ return file_too_new, time_string
+
+ return False, None
+
+ def getFileTimes(self, file_path):
+ return [os.path.getmtime(file_path), os.path.getctime(file_path) if os.name != 'posix' else 0]
def isDisabled(self):
return not self.isEnabled()
def isEnabled(self):
- return self.conf(self.enabled_option) or self.conf(self.enabled_option) == None
+ return self.conf(self.enabled_option) or self.conf(self.enabled_option) is None
+
+ def acquireLock(self, key):
+
+ lock = self._locks.get(key)
+ if not lock:
+ self._locks[key] = threading.RLock()
+
+ log.debug('Acquiring lock: %s', key)
+ self._locks.get(key).acquire()
+
+ def releaseLock(self, key):
+
+ lock = self._locks.get(key)
+ if lock:
+ log.debug('Releasing lock: %s', key)
+ self._locks.get(key).release()
diff --git a/couchpotato/core/plugins/browser.py b/couchpotato/core/plugins/browser.py
new file mode 100644
index 0000000000..660070a2e5
--- /dev/null
+++ b/couchpotato/core/plugins/browser.py
@@ -0,0 +1,125 @@
+import ctypes
+import os
+import string
+import traceback
+import time
+
+from couchpotato import CPLog
+from couchpotato.api import addApiView
+from couchpotato.core.event import addEvent
+from couchpotato.core.helpers.encoding import sp, ss, toUnicode
+from couchpotato.core.helpers.variable import getUserDir
+from couchpotato.core.plugins.base import Plugin
+
+
+log = CPLog(__name__)
+
+
+if os.name == 'nt':
+ import imp
+ try:
+ imp.find_module('win32file')
+ except:
+ # todo:: subclass ImportError for missing dependencies, vs. broken plugins?
+ raise ImportError("Missing the win32file module, which is a part of the prerequisite \
+ pywin32 package. You can get it from http://sourceforge.net/projects/pywin32/files/pywin32/")
+ else:
+ # noinspection PyUnresolvedReferences
+ import win32file
+
+autoload = 'FileBrowser'
+
+
+class FileBrowser(Plugin):
+
+ def __init__(self):
+ addApiView('directory.list', self.view, docs = {
+ 'desc': 'Return the directory list of a given directory',
+ 'params': {
+ 'path': {'desc': 'The directory to scan'},
+ 'show_hidden': {'desc': 'Also show hidden files'}
+ },
+ 'return': {'type': 'object', 'example': """{
+ 'is_root': bool, //is top most folder
+ 'parent': string, //parent folder of requested path
+ 'home': string, //user home folder
+ 'empty': bool, //directory is empty
+ 'dirs': array, //directory names
+}"""}
+ })
+
+ def getDirectories(self, path = '/', show_hidden = True):
+
+ # Return driveletters or root if path is empty
+ if path == '/' or not path or path == '\\':
+ if os.name == 'nt':
+ return self.getDriveLetters()
+ path = '/'
+
+ dirs = []
+ path = sp(path)
+ for f in os.listdir(path):
+ p = sp(os.path.join(path, f))
+ if os.path.isdir(p) and ((self.is_hidden(p) and bool(int(show_hidden))) or not self.is_hidden(p)):
+ dirs.append(toUnicode('%s%s' % (p, os.path.sep)))
+
+ return sorted(dirs)
+
+ def getFiles(self):
+ pass
+
+ def getDriveLetters(self):
+
+ driveletters = []
+ for drive in string.ascii_uppercase:
+ if win32file.GetDriveType(drive + ':') in [win32file.DRIVE_FIXED, win32file.DRIVE_REMOTE, win32file.DRIVE_RAMDISK, win32file.DRIVE_REMOVABLE]:
+ driveletters.append(drive + ':\\')
+
+ return driveletters
+
+ def view(self, path = '/', show_hidden = True, **kwargs):
+
+ home = getUserDir()
+
+ if not path:
+ path = home
+
+ try:
+ dirs = self.getDirectories(path = path, show_hidden = show_hidden)
+ except:
+ log.error('Failed getting directory "%s" : %s', (path, traceback.format_exc()))
+ dirs = []
+
+ parent = os.path.dirname(path.rstrip(os.path.sep))
+ if parent == path.rstrip(os.path.sep):
+ parent = '/'
+ elif parent != '/' and parent[-2:] != ':\\':
+ parent += os.path.sep
+
+ return {
+ 'is_root': path == '/',
+ 'empty': len(dirs) == 0,
+ 'parent': parent,
+ 'home': home + os.path.sep,
+ 'platform': os.name,
+ 'dirs': dirs,
+ }
+
+
+ def is_hidden(self, filepath):
+ name = ss(os.path.basename(os.path.abspath(filepath)))
+ return name.startswith('.') or self.has_hidden_attribute(filepath)
+
+ def has_hidden_attribute(self, filepath):
+
+ result = False
+ try:
+ attrs = ctypes.windll.kernel32.GetFileAttributesW(sp(filepath)) #@UndefinedVariable
+ assert attrs != -1
+ result = bool(attrs & 2)
+ except (AttributeError, AssertionError):
+ pass
+ except:
+ log.error('Failed getting hidden attribute: %s', traceback.format_exc())
+
+ return result
diff --git a/couchpotato/core/plugins/browser/__init__.py b/couchpotato/core/plugins/browser/__init__.py
deleted file mode 100644
index 976fcd102c..0000000000
--- a/couchpotato/core/plugins/browser/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from .main import FileBrowser
-
-def start():
- return FileBrowser()
-
-config = []
diff --git a/couchpotato/core/plugins/browser/main.py b/couchpotato/core/plugins/browser/main.py
deleted file mode 100644
index 3eee85bb91..0000000000
--- a/couchpotato/core/plugins/browser/main.py
+++ /dev/null
@@ -1,106 +0,0 @@
-from couchpotato.api import addApiView
-from couchpotato.core.helpers.request import getParam, jsonified
-from couchpotato.core.helpers.variable import getUserDir
-from couchpotato.core.plugins.base import Plugin
-import ctypes
-import os
-import string
-
-if os.name == 'nt':
- import imp
- try:
- imp.find_module('win32file')
- except:
- # todo:: subclass ImportError for missing dependencies, vs. broken plugins?
- raise ImportError("Missing the win32file module, which is a part of the prerequisite \
- pywin32 package. You can get it from http://sourceforge.net/projects/pywin32/files/pywin32/");
- else:
- import win32file #@UnresolvedImport
-
-class FileBrowser(Plugin):
-
- def __init__(self):
- addApiView('directory.list', self.view, docs = {
- 'desc': 'Return the directory list of a given directory',
- 'params': {
- 'path': {'desc': 'The directory to scan'},
- 'show_hidden': {'desc': 'Also show hidden files'}
- },
- 'return': {'type': 'object', 'example': """{
- 'is_root': bool, //is top most folder
- 'parent': string, //parent folder of requested path
- 'home': string, //user home folder
- 'empty': bool, //directory is empty
- 'dirs': array, //directory names
-}"""}
- })
-
- def getDirectories(self, path = '/', show_hidden = True):
-
- # Return driveletters or root if path is empty
- if path == '/' or not path or path == '\\':
- if os.name == 'nt':
- return self.getDriveLetters()
- path = '/'
-
- dirs = []
- for f in os.listdir(path):
- p = os.path.join(path, f)
- if os.path.isdir(p) and ((self.is_hidden(p) and bool(int(show_hidden))) or not self.is_hidden(p)):
- dirs.append(p + os.path.sep)
-
- return sorted(dirs)
-
- def getFiles(self):
- pass
-
- def getDriveLetters(self):
-
- driveletters = []
- for drive in string.ascii_uppercase:
- if win32file.GetDriveType(drive + ":") in [win32file.DRIVE_FIXED, win32file.DRIVE_REMOTE, win32file.DRIVE_RAMDISK, win32file.DRIVE_REMOVABLE]:
- driveletters.append(drive + ":\\")
-
- return driveletters
-
- def view(self):
-
- path = getParam('path', '/')
- home = getUserDir()
-
- if not path:
- path = home
-
- try:
- dirs = self.getDirectories(path = path, show_hidden = getParam('show_hidden', True))
- except:
- dirs = []
-
- parent = os.path.dirname(path.rstrip(os.path.sep))
- if parent == path.rstrip(os.path.sep):
- parent = '/'
- elif parent != '/' and parent[-2:] != ':\\':
- parent += os.path.sep
-
- return jsonified({
- 'is_root': path == '/',
- 'empty': len(dirs) == 0,
- 'parent': parent,
- 'home': home + os.path.sep,
- 'platform': os.name,
- 'dirs': dirs,
- })
-
-
- def is_hidden(self, filepath):
- name = os.path.basename(os.path.abspath(filepath))
- return name.startswith('.') or self.has_hidden_attribute(filepath)
-
- def has_hidden_attribute(self, filepath):
- try:
- attrs = ctypes.windll.kernel32.GetFileAttributesW(unicode(filepath)) #@UndefinedVariable
- assert attrs != -1
- result = bool(attrs & 2)
- except (AttributeError, AssertionError):
- result = False
- return result
diff --git a/couchpotato/core/plugins/category/__init__.py b/couchpotato/core/plugins/category/__init__.py
new file mode 100644
index 0000000000..d147092f3d
--- /dev/null
+++ b/couchpotato/core/plugins/category/__init__.py
@@ -0,0 +1,5 @@
+from .main import CategoryPlugin
+
+
+def autoload():
+ return CategoryPlugin()
diff --git a/couchpotato/core/plugins/category/index.py b/couchpotato/core/plugins/category/index.py
new file mode 100644
index 0000000000..6445de3ca8
--- /dev/null
+++ b/couchpotato/core/plugins/category/index.py
@@ -0,0 +1,31 @@
+from CodernityDB.tree_index import TreeBasedIndex
+
+
+class CategoryIndex(TreeBasedIndex):
+ _version = 1
+
+ def __init__(self, *args, **kwargs):
+ kwargs['key_format'] = 'i'
+ super(CategoryIndex, self).__init__(*args, **kwargs)
+
+ def make_key(self, key):
+ return key
+
+ def make_key_value(self, data):
+ if data.get('_t') == 'category':
+ return data.get('order', -99), None
+
+
+class CategoryMediaIndex(TreeBasedIndex):
+ _version = 1
+
+ def __init__(self, *args, **kwargs):
+ kwargs['key_format'] = '32s'
+ super(CategoryMediaIndex, self).__init__(*args, **kwargs)
+
+ def make_key(self, key):
+ return str(key)
+
+ def make_key_value(self, data):
+ if data.get('_t') == 'media' and data.get('category_id'):
+ return str(data.get('category_id')), None
diff --git a/couchpotato/core/plugins/category/main.py b/couchpotato/core/plugins/category/main.py
new file mode 100644
index 0000000000..4abc94c03c
--- /dev/null
+++ b/couchpotato/core/plugins/category/main.py
@@ -0,0 +1,150 @@
+import traceback
+
+from couchpotato import get_db
+from couchpotato.api import addApiView
+from couchpotato.core.event import addEvent
+from couchpotato.core.helpers.encoding import toUnicode
+from couchpotato.core.logger import CPLog
+from couchpotato.core.plugins.base import Plugin
+from .index import CategoryIndex, CategoryMediaIndex
+
+
+log = CPLog(__name__)
+
+
+class CategoryPlugin(Plugin):
+
+ _database = {
+ 'category': CategoryIndex,
+ 'category_media': CategoryMediaIndex,
+ }
+
+ def __init__(self):
+ addApiView('category.save', self.save)
+ addApiView('category.save_order', self.saveOrder)
+ addApiView('category.delete', self.delete)
+ addApiView('category.list', self.allView, docs = {
+ 'desc': 'List all available categories',
+ 'return': {'type': 'object', 'example': """{
+ 'success': True,
+ 'categories': array, categories
+}"""}
+ })
+
+ addEvent('category.all', self.all)
+
+ def allView(self, **kwargs):
+
+ return {
+ 'success': True,
+ 'categories': self.all()
+ }
+
+ def all(self):
+
+ db = get_db()
+ categories = db.all('category', with_doc = True)
+
+ return [x['doc'] for x in categories]
+
+ def save(self, **kwargs):
+
+ try:
+ db = get_db()
+
+ category = {
+ '_t': 'category',
+ 'order': kwargs.get('order', 999),
+ 'label': toUnicode(kwargs.get('label', '')),
+ 'ignored': toUnicode(kwargs.get('ignored', '')),
+ 'preferred': toUnicode(kwargs.get('preferred', '')),
+ 'required': toUnicode(kwargs.get('required', '')),
+ 'destination': toUnicode(kwargs.get('destination', '')),
+ }
+
+ try:
+ c = db.get('id', kwargs.get('id'))
+ category['order'] = c.get('order', category['order'])
+ c.update(category)
+
+ db.update(c)
+ except:
+ c = db.insert(category)
+ c.update(category)
+
+ return {
+ 'success': True,
+ 'category': c
+ }
+ except:
+ log.error('Failed: %s', traceback.format_exc())
+
+ return {
+ 'success': False,
+ 'category': None
+ }
+
+ def saveOrder(self, **kwargs):
+
+ try:
+ db = get_db()
+
+ order = 0
+ for category_id in kwargs.get('ids', []):
+ c = db.get('id', category_id)
+ c['order'] = order
+ db.update(c)
+
+ order += 1
+
+ return {
+ 'success': True
+ }
+ except:
+ log.error('Failed: %s', traceback.format_exc())
+
+ return {
+ 'success': False
+ }
+
+ def delete(self, id = None, **kwargs):
+
+ try:
+ db = get_db()
+
+ success = False
+ message = ''
+ try:
+ c = db.get('id', id)
+ db.delete(c)
+
+ # Force defaults on all empty category movies
+ self.removeFromMovie(id)
+
+ success = True
+ except:
+ message = log.error('Failed deleting category: %s', traceback.format_exc())
+
+ return {
+ 'success': success,
+ 'message': message
+ }
+ except:
+ log.error('Failed: %s', traceback.format_exc())
+
+ return {
+ 'success': False
+ }
+
+ def removeFromMovie(self, category_id):
+
+ try:
+ db = get_db()
+ movies = [x['doc'] for x in db.get_many('category_media', category_id, with_doc = True)]
+
+ if len(movies) > 0:
+ for movie in movies:
+ movie['category_id'] = None
+ db.update(movie)
+ except:
+ log.error('Failed: %s', traceback.format_exc())
diff --git a/couchpotato/core/plugins/category/static/category.js b/couchpotato/core/plugins/category/static/category.js
new file mode 100644
index 0000000000..916110224c
--- /dev/null
+++ b/couchpotato/core/plugins/category/static/category.js
@@ -0,0 +1,328 @@
+var CategoryListBase = new Class({
+
+ initialize: function(){
+ var self = this;
+
+ App.addEvent('loadSettings', self.addSettings.bind(self));
+ },
+
+ setup: function(categories){
+ var self = this;
+
+ self.categories = [];
+ Array.each(categories, self.createCategory.bind(self));
+
+ },
+
+ addSettings: function(){
+ var self = this;
+
+ self.settings = App.getPage('Settings');
+ self.settings.addEvent('create', function(){
+ var tab = self.settings.createSubTab('category', {
+ 'label': 'Categories',
+ 'name': 'category',
+ 'subtab_label': 'Category & filtering'
+ }, self.settings.tabs.searcher ,'searcher');
+
+ self.tab = tab.tab;
+ self.content = tab.content;
+
+ self.createList();
+ self.createOrdering();
+
+ });
+
+ // Add categories in renamer
+ self.settings.addEvent('create', function(){
+ var renamer_group = self.settings.tabs.renamer.groups.renamer;
+
+ self.categories.each(function(category){
+
+ var input = new Option.Directory('section_name', 'option.name', category.get('destination'), {
+ 'name': category.get('label')
+ });
+ input.inject(renamer_group.getElement('.renamer_to'));
+ input.fireEvent('injected');
+
+ input.save = function(){
+ category.data.destination = input.getValue();
+ category.save();
+ };
+
+ });
+
+ });
+
+ },
+
+ createList: function(){
+ var self = this;
+
+ var count = self.categories.length;
+
+ self.settings.createGroup({
+ 'label': 'Categories',
+ 'description': 'Create categories, each one extending global filters. (Needs refresh \'' +(App.isMac() ? 'CMD+R' : 'F5')+ '\' after editing)'
+ }).inject(self.content).adopt(
+ self.category_container = new Element('div.container'),
+ new Element('a.add_new_category', {
+ 'text': count > 0 ? 'Create another category' : 'Click here to create a category.',
+ 'events': {
+ 'click': function(){
+ var category = self.createCategory();
+ $(category).inject(self.category_container);
+ }
+ }
+ })
+ );
+
+ // Add categories, that aren't part of the core (for editing)
+ Array.each(self.categories, function(category){
+ $(category).inject(self.category_container);
+ });
+
+ },
+
+ getCategory: function(id){
+ return this.categories.filter(function(category){
+ return category.data._id == id;
+ }).pick();
+ },
+
+ getAll: function(){
+ return this.categories;
+ },
+
+ createCategory: function(data){
+ var self = this;
+
+ data = data || {'id': randomString()};
+ var category = new Category(data);
+ self.categories.include(category);
+
+ return category;
+ },
+
+ createOrdering: function(){
+ var self = this;
+
+ var category_list;
+ self.settings.createGroup({
+ 'label': 'Category ordering'
+ }).adopt(
+ new Element('.ctrlHolder#category_ordering').adopt(
+ new Element('label[text=Order]'),
+ category_list = new Element('ul'),
+ new Element('p.formHint', {
+ 'html': 'Change the order the categories are in the dropdown list.'
+ })
+ )
+ ).inject(self.content);
+
+ Array.each(self.categories, function(category){
+ new Element('li', {'data-id': category.data._id}).adopt(
+ new Element('span.category_label', {
+ 'text': category.data.label
+ }),
+ new Element('span.handle.icon-handle')
+ ).inject(category_list);
+
+ });
+
+ // Sortable
+ self.category_sortable = new Sortables(category_list, {
+ 'revert': true,
+ 'handle': '',
+ 'opacity': 0.5,
+ 'onComplete': self.saveOrdering.bind(self)
+ });
+
+ },
+
+ saveOrdering: function(){
+ var self = this;
+
+ var ids = [];
+
+ self.category_sortable.list.getElements('li').each(function(el){
+ ids.include(el.get('data-id'));
+ });
+
+ Api.request('category.save_order', {
+ 'data': {
+ 'ids': ids
+ }
+ });
+
+ }
+
+});
+
+window.CategoryList = new CategoryListBase();
+
+var Category = new Class({
+
+ data: {},
+
+ initialize: function(data){
+ var self = this;
+
+ self.data = data;
+
+ self.create();
+
+ self.el.addEvents({
+ 'change:relay(select)': self.save.bind(self, 0),
+ 'keyup:relay(input[type=text])': self.save.bind(self, [300])
+ });
+
+ },
+
+ create: function(){
+ var self = this;
+
+ var data = self.data;
+
+ self.el = new Element('div.category').adopt(
+ self.delete_button = new Element('span.delete.icon-delete', {
+ 'events': {
+ 'click': self.del.bind(self)
+ }
+ }),
+ new Element('.category_label.ctrlHolder').adopt(
+ new Element('label', {'text':'Name'}),
+ new Element('input', {
+ 'type':'text',
+ 'value': data.label,
+ 'placeholder': 'Example: Kids, Horror or His'
+ }),
+ new Element('p.formHint', {'text': 'See global filters for explanation.'})
+ ),
+ new Element('.category_preferred.ctrlHolder').adopt(
+ new Element('label', {'text':'Preferred'}),
+ new Element('input', {
+ 'type':'text',
+ 'value': data.preferred,
+ 'placeholder': 'Blu-ray, DTS'
+ })
+ ),
+ new Element('.category_required.ctrlHolder').adopt(
+ new Element('label', {'text':'Required'}),
+ new Element('input', {
+ 'type':'text',
+ 'value': data.required,
+ 'placeholder': 'Example: DTS, AC3 & English'
+ })
+ ),
+ new Element('.category_ignored.ctrlHolder').adopt(
+ new Element('label', {'text':'Ignored'}),
+ new Element('input', {
+ 'type':'text',
+ 'value': data.ignored,
+ 'placeholder': 'Example: dubbed, swesub, french'
+ })
+ )
+ );
+
+ self.makeSortable();
+
+ },
+
+ save: function(delay){
+ var self = this;
+
+ if(self.save_timer) clearRequestTimeout(self.save_timer);
+ self.save_timer = requestTimeout(function(){
+
+ Api.request('category.save', {
+ 'data': self.getData(),
+ 'useSpinner': true,
+ 'spinnerOptions': {
+ 'target': self.el
+ },
+ 'onComplete': function(json){
+ if(json.success){
+ self.data = json.category;
+ }
+ }
+ });
+
+ }, delay || 0);
+
+ },
+
+ getData: function(){
+ var self = this;
+
+ return {
+ 'id' : self.data._id,
+ 'label' : self.el.getElement('.category_label input').get('value'),
+ 'required' : self.el.getElement('.category_required input').get('value'),
+ 'preferred' : self.el.getElement('.category_preferred input').get('value'),
+ 'ignored' : self.el.getElement('.category_ignored input').get('value'),
+ 'destination': self.data.destination
+ };
+ },
+
+ del: function(){
+ var self = this;
+
+ if(self.data.label === undefined){
+ self.el.destroy();
+ return;
+ }
+
+ var label = self.el.getElement('.category_label input').get('value');
+ var qObj = new Question('Are you sure you want to delete "'+label+'"?', '', [{
+ 'text': 'Delete "'+label+'"',
+ 'class': 'delete',
+ 'events': {
+ 'click': function(e){
+ (e).preventDefault();
+ Api.request('category.delete', {
+ 'data': {
+ 'id': self.data._id
+ },
+ 'useSpinner': true,
+ 'spinnerOptions': {
+ 'target': self.el
+ },
+ 'onComplete': function(json){
+ if(json.success) {
+ qObj.close();
+ self.el.destroy();
+ } else {
+ alert(json.message);
+ }
+ }
+ });
+ }
+ }
+ }, {
+ 'text': 'Cancel',
+ 'cancel': true
+ }]);
+
+ },
+
+ makeSortable: function(){
+ var self = this;
+
+ self.sortable = new Sortables(self.category_container, {
+ 'revert': true,
+ 'handle': '.handle',
+ 'opacity': 0.5,
+ 'onComplete': self.save.bind(self, 300)
+ });
+ },
+
+ get: function(attr){
+ return this.data[attr];
+ },
+
+ toElement: function(){
+ return this.el;
+ }
+
+});
diff --git a/couchpotato/core/plugins/category/static/category.scss b/couchpotato/core/plugins/category/static/category.scss
new file mode 100644
index 0000000000..f1568f07a4
--- /dev/null
+++ b/couchpotato/core/plugins/category/static/category.scss
@@ -0,0 +1,78 @@
+@import "_mixins";
+
+.add_new_category {
+ padding: 20px;
+ display: block;
+ text-align: center;
+ font-size: 20px;
+}
+
+.category {
+ margin-bottom: 20px;
+ position: relative;
+
+ > .delete {
+ position: absolute;
+ padding: $padding/3 $padding;
+ right: 0;
+ cursor: pointer;
+ opacity: 0.6;
+ color: #fd5353;
+ font-size: 1.5em;
+ z-index: 2;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+
+ .ctrlHolder:hover {
+ background: none;
+ }
+
+ .formHint {
+ opacity: 0.1;
+ }
+
+ &:hover .formHint {
+ opacity: 1;
+ }
+}
+
+#category_ordering {
+
+ ul {
+ float: left;
+ margin: 0;
+ width: 275px;
+ padding: 0;
+ }
+
+ li {
+ cursor: grab;
+ border-bottom: 1px solid transparent;
+ @include theme(border-color, off);
+ padding: 5px;
+ list-style: none;
+
+ &:last-child { border: 0; }
+
+ .check {
+ margin: 2px 10px 0 0;
+ vertical-align: top;
+ }
+
+ > span {
+ display: inline-block;
+ height: 20px;
+ vertical-align: top;
+ line-height: 20px;
+ }
+
+ .handle {
+ width: 20px;
+ float: right;
+ }
+ }
+
+}
diff --git a/couchpotato/core/plugins/profile/static/handle.png b/couchpotato/core/plugins/category/static/handle.png
similarity index 100%
rename from couchpotato/core/plugins/profile/static/handle.png
rename to couchpotato/core/plugins/category/static/handle.png
diff --git a/couchpotato/core/plugins/custom.py b/couchpotato/core/plugins/custom.py
new file mode 100644
index 0000000000..20b4c3f7a7
--- /dev/null
+++ b/couchpotato/core/plugins/custom.py
@@ -0,0 +1,25 @@
+import os
+
+from couchpotato.core.event import addEvent
+from couchpotato.core.logger import CPLog
+from couchpotato.core.plugins.base import Plugin
+from couchpotato.environment import Env
+
+
+log = CPLog(__name__)
+
+autoload = 'Custom'
+
+
+class Custom(Plugin):
+
+ def __init__(self):
+ addEvent('app.load', self.createStructure)
+
+ def createStructure(self):
+
+ custom_dir = os.path.join(Env.get('data_dir'), 'custom_plugins')
+
+ if not os.path.isdir(custom_dir):
+ self.makeDir(custom_dir)
+ self.createFile(os.path.join(custom_dir, '__init__.py'), '# Don\'t remove this file')
diff --git a/couchpotato/core/plugins/dashboard.py b/couchpotato/core/plugins/dashboard.py
new file mode 100644
index 0000000000..16dc4188e4
--- /dev/null
+++ b/couchpotato/core/plugins/dashboard.py
@@ -0,0 +1,116 @@
+import random as rndm
+import time
+from CodernityDB.database import RecordDeleted, RecordNotFound
+
+from couchpotato import get_db
+from couchpotato.api import addApiView
+from couchpotato.core.event import fireEvent
+from couchpotato.core.helpers.variable import splitString, tryInt
+from couchpotato.core.logger import CPLog
+from couchpotato.core.plugins.base import Plugin
+
+
+log = CPLog(__name__)
+
+autoload = 'Dashboard'
+
+
+class Dashboard(Plugin):
+
+ def __init__(self):
+ addApiView('dashboard.soon', self.getSoonView)
+
+ def getSoonView(self, limit_offset = None, random = False, late = False, **kwargs):
+
+ db = get_db()
+ now = time.time()
+
+ # Get profiles first, determine pre or post theater
+ profiles = fireEvent('profile.all', single = True)
+ pre_releases = fireEvent('quality.pre_releases', single = True)
+
+ # See what the profile contain and cache it
+ profile_pre = {}
+ for profile in profiles:
+ contains = {}
+ for q_identifier in profile.get('qualities', []):
+ contains['theater' if q_identifier in pre_releases else 'dvd'] = True
+
+ profile_pre[profile.get('_id')] = contains
+
+ # Add limit
+ limit = 12
+ if limit_offset:
+ splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
+ limit = tryInt(splt[0])
+
+ # Get all active medias
+ active_ids = [x['_id'] for x in fireEvent('media.with_status', 'active', with_doc = False, single = True)]
+
+ medias = []
+
+ if len(active_ids) > 0:
+
+ # Order by title or randomize
+ if not random:
+ orders_ids = db.all('media_title')
+ active_ids = [x['_id'] for x in orders_ids if x['_id'] in active_ids]
+ else:
+ rndm.shuffle(active_ids)
+
+ for media_id in active_ids:
+ try:
+ media = db.get('id', media_id)
+ except RecordDeleted:
+ log.debug('Record already deleted: %s', media_id)
+ continue
+
+ except RecordNotFound:
+ log.debug('Record not found: %s', media_id)
+ continue
+
+ pp = profile_pre.get(media.get('profile_id'))
+ if not pp: continue
+
+ eta = media['info'].get('release_date', {}) or {}
+ coming_soon = False
+
+ # Theater quality
+ if pp.get('theater') and fireEvent('movie.searcher.could_be_released', True, eta, media['info']['year'], single = True):
+ coming_soon = 'theater'
+ elif pp.get('dvd') and fireEvent('movie.searcher.could_be_released', False, eta, media['info']['year'], single = True):
+ coming_soon = 'dvd'
+
+ if coming_soon:
+
+ # Don't list older movies
+ eta_date = eta.get(coming_soon)
+ eta_3month_passed = eta_date < (now - 7862400) # Release was more than 3 months ago
+
+ if (not late and not eta_3month_passed) or \
+ (late and eta_3month_passed):
+
+ add = True
+
+ # Check if it doesn't have any releases
+ if late:
+ media['releases'] = fireEvent('release.for_media', media['_id'], single = True)
+
+ for release in media.get('releases', []):
+ if release.get('status') in ['snatched', 'available', 'seeding', 'downloaded']:
+ add = False
+ break
+
+ if add:
+ medias.append(media)
+
+ if len(medias) >= limit:
+ break
+
+ return {
+ 'success': True,
+ 'empty': len(medias) == 0,
+ 'movies': medias,
+ }
+
+ getLateView = getSoonView
diff --git a/couchpotato/core/plugins/dashboard/__init__.py b/couchpotato/core/plugins/dashboard/__init__.py
deleted file mode 100644
index 81279291c0..0000000000
--- a/couchpotato/core/plugins/dashboard/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from .main import Dashboard
-
-def start():
- return Dashboard()
-
-config = []
diff --git a/couchpotato/core/plugins/dashboard/main.py b/couchpotato/core/plugins/dashboard/main.py
deleted file mode 100644
index d5f9ef0be8..0000000000
--- a/couchpotato/core/plugins/dashboard/main.py
+++ /dev/null
@@ -1,120 +0,0 @@
-from couchpotato import get_session
-from couchpotato.api import addApiView
-from couchpotato.core.event import fireEvent
-from couchpotato.core.helpers.request import jsonified, getParams
-from couchpotato.core.helpers.variable import splitString, tryInt
-from couchpotato.core.logger import CPLog
-from couchpotato.core.plugins.base import Plugin
-from couchpotato.core.settings.model import Movie
-from sqlalchemy.sql.expression import or_
-import random
-
-log = CPLog(__name__)
-
-
-class Dashboard(Plugin):
-
- def __init__(self):
-
- addApiView('dashboard.suggestions', self.suggestView)
- addApiView('dashboard.soon', self.getSoonView)
-
- def newSuggestions(self):
-
- movies = fireEvent('movie.list', status = ['active', 'done'], limit_offset = (20, 0), single = True)
- movie_identifiers = [m['library']['identifier'] for m in movies[1]]
-
- ignored_movies = fireEvent('movie.list', status = ['ignored', 'deleted'], limit_offset = (100, 0), single = True)
- ignored_identifiers = [m['library']['identifier'] for m in ignored_movies[1]]
-
- suggestions = fireEvent('movie.suggest', movies = movie_identifiers, ignore = ignored_identifiers, single = True)
- suggest_status = fireEvent('status.get', 'suggest', single = True)
-
- for suggestion in suggestions:
- fireEvent('movie.add', params = {'identifier': suggestion}, force_readd = False, search_after = False, status_id = suggest_status.get('id'))
-
- def suggestView(self):
-
- db = get_session()
-
- movies = db.query(Movie).limit(20).all()
- identifiers = [m.library.identifier for m in movies]
-
- suggestions = fireEvent('movie.suggest', movies = identifiers, single = True)
- print suggestions
-
- return jsonified({
- 'result': True,
- 'suggestions': suggestions
- })
-
- def getSoonView(self):
-
- params = getParams()
- db = get_session()
-
- # Get profiles first, determine pre or post theater
- profiles = fireEvent('profile.all', single = True)
- qualities = fireEvent('quality.all', single = True)
- pre_releases = fireEvent('quality.pre_releases', single = True)
-
- id_pre = {}
- for quality in qualities:
- id_pre[quality.get('id')] = quality.get('identifier') in pre_releases
-
- # See what the profile contain and cache it
- profile_pre = {}
- for profile in profiles:
- contains = {}
- for profile_type in profile.get('types', []):
- contains['theater' if id_pre.get(profile_type.get('quality_id')) else 'dvd'] = True
-
- profile_pre[profile.get('id')] = contains
-
- # Get all active movies
- q = db.query(Movie) \
- .join(Movie.profile, Movie.library) \
- .filter(or_(*[Movie.status.has(identifier = s) for s in ['active']])) \
- .group_by(Movie.id)
-
- # Add limit
- limit_offset = params.get('limit_offset')
- limit = 12
- if limit_offset:
- splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
- limit = tryInt(splt[0])
-
- all_movies = q.all()
-
- if params.get('random', False):
- random.shuffle(all_movies)
-
- movies = []
- for movie in all_movies:
- pp = profile_pre.get(movie.profile.id)
- eta = movie.library.info.get('release_date', {})
- coming_soon = False
-
- # Theater quality
- if pp.get('theater') and fireEvent('searcher.could_be_released', True, eta, single = True):
- coming_soon = True
- if pp.get('dvd') and fireEvent('searcher.could_be_released', False, eta, single = True):
- coming_soon = True
-
- if coming_soon:
- temp = movie.to_dict({
- 'profile': {'types': {}},
- 'releases': {'files':{}, 'info': {}},
- 'library': {'titles': {}, 'files':{}},
- 'files': {},
- })
- movies.append(temp)
-
- if len(movies) >= limit:
- break
-
- return jsonified({
- 'success': True,
- 'empty': len(movies) == 0,
- 'movies': movies,
- })
diff --git a/couchpotato/core/plugins/file.py b/couchpotato/core/plugins/file.py
new file mode 100644
index 0000000000..56c5230ddc
--- /dev/null
+++ b/couchpotato/core/plugins/file.py
@@ -0,0 +1,115 @@
+import os.path
+import traceback
+
+from couchpotato import get_db
+from couchpotato.api import addApiView
+from couchpotato.core.event import addEvent, fireEvent
+from couchpotato.core.helpers.encoding import toUnicode, ss, sp
+from couchpotato.core.helpers.variable import md5, getExt, isSubFolder
+from couchpotato.core.logger import CPLog
+from couchpotato.core.plugins.base import Plugin
+from couchpotato.environment import Env
+from tornado.web import StaticFileHandler
+
+
+log = CPLog(__name__)
+
+autoload = 'FileManager'
+
+
+class FileManager(Plugin):
+
+ def __init__(self):
+ addEvent('file.download', self.download)
+
+ addApiView('file.cache/(.*)', self.showCacheFile, static = True, docs = {
+ 'desc': 'Return a file from the cp_data/cache directory',
+ 'params': {
+ 'filename': {'desc': 'path/filename of the wanted file'}
+ },
+ 'return': {'type': 'file'}
+ })
+
+ fireEvent('schedule.interval', 'file.cleanup', self.cleanup, hours = 24)
+
+ addEvent('app.test', self.doSubfolderTest)
+
+ def cleanup(self):
+
+ # Wait a bit after starting before cleanup
+ log.debug('Cleaning up unused files')
+
+ try:
+ db = get_db()
+ cache_dir = Env.get('cache_dir')
+ medias = db.all('media', with_doc = True)
+
+ files = []
+ for media in medias:
+ file_dict = media['doc'].get('files', {})
+ for x in file_dict.keys():
+ files.extend(file_dict[x])
+
+ for f in os.listdir(cache_dir):
+ if os.path.splitext(f)[1] in ['.png', '.jpg', '.jpeg']:
+ file_path = os.path.join(cache_dir, f)
+ if toUnicode(file_path) not in files:
+ os.remove(file_path)
+ except:
+ log.error('Failed removing unused file: %s', traceback.format_exc())
+
+ def showCacheFile(self, route, **kwargs):
+ Env.get('app').add_handlers(".*$", [('%s%s' % (Env.get('api_base'), route), StaticFileHandler, {'path': toUnicode(Env.get('cache_dir'))})])
+
+ def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = None):
+ if not urlopen_kwargs: urlopen_kwargs = {}
+
+ # Return response object to stream download
+ urlopen_kwargs['stream'] = True
+
+ if not dest: # to Cache
+ dest = os.path.join(Env.get('cache_dir'), ss('%s.%s' % (md5(url), getExt(url))))
+
+ dest = sp(dest)
+
+ if not overwrite and os.path.isfile(dest):
+ return dest
+
+ try:
+ filedata = self.urlopen(url, **urlopen_kwargs)
+ except:
+ log.error('Failed downloading file %s: %s', (url, traceback.format_exc()))
+ return False
+
+ self.createFile(dest, filedata, binary = True)
+ return dest
+
+ def doSubfolderTest(self):
+
+ tests = {
+ ('/test/subfolder', '/test/sub'): False,
+ ('/test/sub/folder', '/test/sub'): True,
+ ('/test/sub/folder', '/test/sub2'): False,
+ ('/sub/fold', '/test/sub/fold'): False,
+ ('/sub/fold', '/test/sub/folder'): False,
+ ('/opt/couchpotato', '/var/opt/couchpotato'): False,
+ ('/var/opt', '/var/opt/couchpotato'): False,
+ ('/CapItaLs/Are/OK', '/CapItaLs/Are/OK'): True,
+ ('/CapItaLs/Are/OK', '/CapItaLs/Are/OK2'): False,
+ ('/capitals/are/not/OK', '/capitals/are/NOT'): False,
+ ('\\\\Mounted\\Volume\\Test', '\\\\Mounted\\Volume'): True,
+ ('C:\\\\test\\path', 'C:\\\\test2'): False
+ }
+
+ failed = 0
+ for x in tests:
+ if isSubFolder(x[0], x[1]) is not tests[x]:
+ log.error('Failed subfolder test %s %s', x)
+ failed += 1
+
+ if failed > 0:
+ log.error('Subfolder test failed %s tests', failed)
+ else:
+ log.info('Subfolder test succeeded')
+
+ return failed == 0
diff --git a/couchpotato/core/plugins/file/__init__.py b/couchpotato/core/plugins/file/__init__.py
deleted file mode 100644
index 54d9cbe542..0000000000
--- a/couchpotato/core/plugins/file/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from .main import FileManager
-
-def start():
- return FileManager()
-
-config = []
diff --git a/couchpotato/core/plugins/file/main.py b/couchpotato/core/plugins/file/main.py
deleted file mode 100644
index 0dc0178340..0000000000
--- a/couchpotato/core/plugins/file/main.py
+++ /dev/null
@@ -1,161 +0,0 @@
-from couchpotato import get_session
-from couchpotato.api import addApiView
-from couchpotato.core.event import addEvent
-from couchpotato.core.helpers.encoding import toUnicode
-from couchpotato.core.helpers.request import jsonified
-from couchpotato.core.helpers.variable import md5, getExt
-from couchpotato.core.logger import CPLog
-from couchpotato.core.plugins.base import Plugin
-from couchpotato.core.plugins.scanner.main import Scanner
-from couchpotato.core.settings.model import FileType, File
-from couchpotato.environment import Env
-import os.path
-import time
-import traceback
-
-log = CPLog(__name__)
-
-
-class FileManager(Plugin):
-
- def __init__(self):
- addEvent('file.add', self.add)
- addEvent('file.download', self.download)
- addEvent('file.types', self.getTypes)
-
- addApiView('file.cache/', self.showCacheFile, static = True, docs = {
- 'desc': 'Return a file from the cp_data/cache directory',
- 'params': {
- 'filename': {'desc': 'path/filename of the wanted file'}
- },
- 'return': {'type': 'file'}
- })
-
- addApiView('file.types', self.getTypesView, docs = {
- 'desc': 'Return a list of all the file types and their ids.',
- 'return': {'type': 'object', 'example': """{
- 'types': [
- {
- "identifier": "poster_original",
- "type": "image",
- "id": 1,
- "name": "Poster_original"
- },
- {
- "identifier": "poster",
- "type": "image",
- "id": 2,
- "name": "Poster"
- },
- etc
- ]
-}"""}
- })
-
- addEvent('app.load', self.cleanup)
- addEvent('app.load', self.init)
-
- def init(self):
-
- for type_tuple in Scanner.file_types.values():
- self.getType(type_tuple)
-
- def cleanup(self):
-
- # Wait a bit after starting before cleanup
- time.sleep(3)
- log.debug('Cleaning up unused files')
-
- python_cache = Env.get('cache')._path
- try:
- db = get_session()
- for root, dirs, walk_files in os.walk(Env.get('cache_dir')):
- for filename in walk_files:
- if root == python_cache or 'minified' in filename: continue
- file_path = os.path.join(root, filename)
- f = db.query(File).filter(File.path == toUnicode(file_path)).first()
- if not f:
- os.remove(file_path)
- except:
- log.error('Failed removing unused file: %s', traceback.format_exc())
-
- def showCacheFile(self, filename = ''):
-
- cache_dir = Env.get('cache_dir')
- filename = os.path.basename(filename)
-
- from flask.helpers import send_from_directory
- return send_from_directory(cache_dir, filename)
-
- def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = {}):
-
- if not dest: # to Cache
- dest = os.path.join(Env.get('cache_dir'), '%s.%s' % (md5(url), getExt(url)))
-
- if not overwrite and os.path.isfile(dest):
- return dest
-
- try:
- filedata = self.urlopen(url, **urlopen_kwargs)
- except:
- log.error('Failed downloading file %s: %s', (url, traceback.format_exc()))
- return False
-
- self.createFile(dest, filedata, binary = True)
- return dest
-
- def add(self, path = '', part = 1, type_tuple = (), available = 1, properties = {}):
- type_id = self.getType(type_tuple).get('id')
- db = get_session()
-
- f = db.query(File).filter(File.path == toUnicode(path)).first()
- if not f:
- f = File()
- db.add(f)
-
- f.path = toUnicode(path)
- f.part = part
- f.available = available
- f.type_id = type_id
-
- db.commit()
-
- file_dict = f.to_dict()
-
- return file_dict
-
- def getType(self, type_tuple):
-
- db = get_session()
- type_type, type_identifier = type_tuple
-
- ft = db.query(FileType).filter_by(identifier = type_identifier).first()
- if not ft:
- ft = FileType(
- type = toUnicode(type_type),
- identifier = type_identifier,
- name = toUnicode(type_identifier[0].capitalize() + type_identifier[1:])
- )
- db.add(ft)
- db.commit()
-
- type_dict = ft.to_dict()
- return type_dict
-
- def getTypes(self):
-
- db = get_session()
-
- results = db.query(FileType).all()
-
- types = []
- for type_object in results:
- types.append(type_object.to_dict())
-
- return types
-
- def getTypesView(self):
-
- return jsonified({
- 'types': self.getTypes()
- })
diff --git a/couchpotato/core/plugins/file/static/file.js b/couchpotato/core/plugins/file/static/file.js
deleted file mode 100644
index 7b893e88e6..0000000000
--- a/couchpotato/core/plugins/file/static/file.js
+++ /dev/null
@@ -1,83 +0,0 @@
-var File = new Class({
-
- initialize: function(file){
- var self = this;
-
- if(!file){
- self.empty = true;
- self.el = new Element('div');
- return
- }
-
- self.data = file;
- self.type = File.Type.get(file.type_id);
-
- self['create'+(self.type.type).capitalize()]()
-
- },
-
- createImage: function(){
- var self = this;
-
- var file_name = self.data.path.replace(/^.*[\\\/]/, '');
-
- self.el = new Element('div', {
- 'class': 'type_image ' + self.type.identifier
- }).adopt(
- new Element('img', {
- 'src': Api.createUrl('file.cache') + file_name
- })
- )
- },
-
- toElement: function(){
- return this.el;
- }
-
-});
-
-var FileSelect = new Class({
-
- multiple: function(type, files, single){
-
- var results = files.filter(function(file){
- return file.type_id == File.Type.get(type).id;
- });
-
- if(single)
- return new File(results.pop());
-
- return results;
-
- },
-
- single: function(type, files){
- return this.multiple(type, files, true);
- }
-
-});
-window.File.Select = new FileSelect();
-
-var FileTypeBase = new Class({
-
- setup: function(types){
- var self = this;
-
- self.typesById = {};
- self.typesByKey = {};
- Object.each(types, function(type){
- self.typesByKey[type.identifier] = type;
- self.typesById[type.id] = type;
- });
-
- },
-
- get: function(identifier){
- if(typeOf(identifier) == 'number')
- return this.typesById[identifier]
- else
- return this.typesByKey[identifier]
- }
-
-});
-window.File.Type = new FileTypeBase();
diff --git a/couchpotato/core/plugins/library/__init__.py b/couchpotato/core/plugins/library/__init__.py
deleted file mode 100644
index f597032999..0000000000
--- a/couchpotato/core/plugins/library/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from .main import LibraryPlugin
-
-def start():
- return LibraryPlugin()
-
-config = []
diff --git a/couchpotato/core/plugins/library/main.py b/couchpotato/core/plugins/library/main.py
deleted file mode 100644
index aa1611dddc..0000000000
--- a/couchpotato/core/plugins/library/main.py
+++ /dev/null
@@ -1,166 +0,0 @@
-from couchpotato import get_session
-from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
-from couchpotato.core.helpers.encoding import toUnicode, simplifyString
-from couchpotato.core.helpers.variable import mergeDicts
-from couchpotato.core.logger import CPLog
-from couchpotato.core.plugins.base import Plugin
-from couchpotato.core.settings.model import Library, LibraryTitle, File
-from string import ascii_letters
-import time
-import traceback
-
-log = CPLog(__name__)
-
-class LibraryPlugin(Plugin):
-
- default_dict = {'titles': {}, 'files':{}}
-
- def __init__(self):
- addEvent('library.add', self.add)
- addEvent('library.update', self.update)
- addEvent('library.update_release_date', self.updateReleaseDate)
-
-
- def add(self, attrs = {}, update_after = True):
-
- db = get_session()
-
- l = db.query(Library).filter_by(identifier = attrs.get('identifier')).first()
- if not l:
- status = fireEvent('status.get', 'needs_update', single = True)
- l = Library(
- year = attrs.get('year'),
- identifier = attrs.get('identifier'),
- plot = toUnicode(attrs.get('plot')),
- tagline = toUnicode(attrs.get('tagline')),
- status_id = status.get('id')
- )
-
- title = LibraryTitle(
- title = toUnicode(attrs.get('title')),
- simple_title = self.simplifyTitle(attrs.get('title'))
- )
-
- l.titles.append(title)
-
- db.add(l)
- db.commit()
-
- # Update library info
- if update_after is not False:
- handle = fireEventAsync if update_after is 'async' else fireEvent
- handle('library.update', identifier = l.identifier, default_title = toUnicode(attrs.get('title', '')))
-
- library_dict = l.to_dict(self.default_dict)
-
- return library_dict
-
- def update(self, identifier, default_title = '', force = False):
-
- db = get_session()
- library = db.query(Library).filter_by(identifier = identifier).first()
- done_status = fireEvent('status.get', 'done', single = True)
-
- if library:
- library_dict = library.to_dict(self.default_dict)
-
- do_update = True
-
- if library.status_id == done_status.get('id') and not force:
- do_update = False
- else:
- info = fireEvent('movie.info', merge = True, identifier = identifier)
-
- # Don't need those here
- try: del info['in_wanted']
- except: pass
- try: del info['in_library']
- except: pass
-
- if not info or len(info) == 0:
- log.error('Could not update, no movie info to work with: %s', identifier)
- return False
-
- # Main info
- if do_update:
- library.plot = toUnicode(info.get('plot', ''))
- library.tagline = toUnicode(info.get('tagline', ''))
- library.year = info.get('year', 0)
- library.status_id = done_status.get('id')
- library.info = info
- db.commit()
-
- # Titles
- [db.delete(title) for title in library.titles]
- db.commit()
-
- titles = info.get('titles', [])
- log.debug('Adding titles: %s', titles)
- for title in titles:
- if not title:
- continue
- title = toUnicode(title)
- t = LibraryTitle(
- title = title,
- simple_title = self.simplifyTitle(title),
- default = title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == u'' and toUnicode(titles[0]) == title)
- )
- library.titles.append(t)
-
- db.commit()
-
- # Files
- images = info.get('images', [])
- for image_type in ['poster']:
- for image in images.get(image_type, []):
- if not isinstance(image, (str, unicode)):
- continue
-
- file_path = fireEvent('file.download', url = image, single = True)
- if file_path:
- file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', image_type), single = True)
- try:
- file_obj = db.query(File).filter_by(id = file_obj.get('id')).one()
- library.files.append(file_obj)
- db.commit()
-
- break
- except:
- log.debug('Failed to attach to library: %s', traceback.format_exc())
-
- library_dict = library.to_dict(self.default_dict)
-
- return library_dict
-
- def updateReleaseDate(self, identifier):
-
- db = get_session()
- library = db.query(Library).filter_by(identifier = identifier).first()
-
- if not library.info:
- library_dict = self.update(identifier, force = True)
- dates = library_dict.get('info', {}).get('release_date')
- else:
- dates = library.info.get('release_date')
-
- if dates and dates.get('expires', 0) < time.time() or not dates:
- dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
- library.info = mergeDicts(library.info, {'release_date': dates })
- db.commit()
-
- return dates
-
-
- def simplifyTitle(self, title):
-
- title = toUnicode(title)
-
- nr_prefix = '' if title[0] in ascii_letters else '#'
- title = simplifyString(title)
-
- for prefix in ['the ']:
- if prefix == title[:len(prefix)]:
- title = title[len(prefix):]
- break
-
- return nr_prefix + title
diff --git a/couchpotato/core/plugins/log/__init__.py b/couchpotato/core/plugins/log/__init__.py
index 33dcf338a3..3760b5675f 100644
--- a/couchpotato/core/plugins/log/__init__.py
+++ b/couchpotato/core/plugins/log/__init__.py
@@ -1,6 +1,5 @@
from .main import Logging
-def start():
- return Logging()
-config = []
+def autoload():
+ return Logging()
diff --git a/couchpotato/core/plugins/log/main.py b/couchpotato/core/plugins/log/main.py
index e6ff1133c0..4bf7cf3e55 100644
--- a/couchpotato/core/plugins/log/main.py
+++ b/couchpotato/core/plugins/log/main.py
@@ -1,12 +1,14 @@
+import os
+import re
+import traceback
+
from couchpotato.api import addApiView
from couchpotato.core.helpers.encoding import toUnicode
-from couchpotato.core.helpers.request import jsonified, getParam, getParams
-from couchpotato.core.helpers.variable import tryInt
+from couchpotato.core.helpers.variable import tryInt, splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
-import os
-import traceback
+
log = CPLog(__name__)
@@ -21,7 +23,11 @@ def __init__(self):
},
'return': {'type': 'object', 'example': """{
'success': True,
- 'log': string, //Log file
+ 'log': [{
+ 'time': '03-12 09:12:59',
+ 'type': 'INFO',
+ 'message': 'Log message'
+ }, ..], //Log file
'total': int, //Total log files available
}"""}
})
@@ -33,7 +39,11 @@ def __init__(self):
},
'return': {'type': 'object', 'example': """{
'success': True,
- 'log': string, //Log file
+ 'log': [{
+ 'time': '03-12 09:12:59',
+ 'type': 'INFO',
+ 'message': 'Log message'
+ }, ..]
}"""}
})
addApiView('logging.clear', self.clear, docs = {
@@ -43,13 +53,13 @@ def __init__(self):
'desc': 'Log errors',
'params': {
'type': {'desc': 'Type of logging, default "error"'},
- '**kwargs': {'type':'object', 'desc': 'All other params will be printed in the log string.'},
+ '**kwargs': {'type': 'object', 'desc': 'All other params will be printed in the log string.'},
}
})
- def get(self):
+ def get(self, nr = 0, **kwargs):
- nr = int(getParam('nr', 0))
+ nr = tryInt(nr)
current_path = None
total = 1
@@ -66,21 +76,22 @@ def get(self):
if x is nr:
current_path = path
- log = ''
+ log_content = ''
if current_path:
f = open(current_path, 'r')
- log = f.read()
+ log_content = f.read()
+ logs = self.toList(log_content)
- return jsonified({
+ return {
'success': True,
- 'log': toUnicode(log),
+ 'log': logs,
'total': total,
- })
+ }
- def partial(self):
+ def partial(self, type = 'all', lines = 30, offset = 0, **kwargs):
- log_type = getParam('type', 'all')
- total_lines = tryInt(getParam('lines', 30))
+ total_lines = tryInt(lines)
+ offset = tryInt(offset)
log_lines = []
@@ -92,37 +103,66 @@ def partial(self):
if not os.path.isfile(path):
break
- reversed_lines = []
f = open(path, 'r')
- reversed_lines = toUnicode(f.read()).split('[0m\n')
- reversed_lines.reverse()
+ log_content = toUnicode(f.read())
+ raw_lines = self.toList(log_content)
+ raw_lines.reverse()
brk = False
- for line in reversed_lines:
+ for line in raw_lines:
- if log_type == 'all' or '%s ' % log_type.upper() in line:
+ if type == 'all' or line.get('type') == type.upper():
log_lines.append(line)
- if len(log_lines) >= total_lines:
+ if len(log_lines) >= (total_lines + offset):
brk = True
break
if brk:
break
+ log_lines = log_lines[offset:]
log_lines.reverse()
- return jsonified({
+
+ return {
'success': True,
- 'log': '[0m\n'.join(log_lines),
- })
+ 'log': log_lines,
+ }
+
+ def toList(self, log_content = ''):
- def clear(self):
+ logs_raw = re.split(r'\[0m\n', toUnicode(log_content))
+
+ logs = []
+ re_split = r'\x1b'
+ for log_line in logs_raw:
+ split = re.split(re_split, log_line)
+ if split and len(split) == 3:
+ try:
+ date, time, log_type = splitString(split[0], ' ')
+ timestamp = '%s %s' % (date, time)
+ except:
+ timestamp = 'UNKNOWN'
+ log_type = 'UNKNOWN'
+
+ message = ''.join(split[1]) if len(split) > 1 else split[0]
+ message = re.sub('\[\d+m\[', '[', message)
+
+ logs.append({
+ 'time': timestamp,
+ 'type': log_type,
+ 'message': message
+ })
+
+ return logs
+
+ def clear(self, **kwargs):
for x in range(0, 50):
path = '%s%s' % (Env.get('log_path'), '.%s' % x if x > 0 else '')
if not os.path.isfile(path):
- break
+ continue
try:
@@ -135,24 +175,21 @@ def clear(self):
except:
log.error('Couldn\'t delete file "%s": %s', (path, traceback.format_exc()))
- return jsonified({
+ return {
'success': True
- })
-
- def log(self):
+ }
- params = getParams()
+ def log(self, type = 'error', **kwargs):
try:
- log_message = 'API log: %s' % params
+ log_message = 'API log: %s' % kwargs
try:
- getattr(log, params.get('type', 'error'))(log_message)
+ getattr(log, type)(log_message)
except:
log.error(log_message)
except:
- log.error('Couldn\'t log via API: %s', params)
+ log.error('Couldn\'t log via API: %s', kwargs)
-
- return jsonified({
+ return {
'success': True
- })
+ }
diff --git a/couchpotato/core/plugins/log/static/log.css b/couchpotato/core/plugins/log/static/log.css
deleted file mode 100644
index 222b8efa03..0000000000
--- a/couchpotato/core/plugins/log/static/log.css
+++ /dev/null
@@ -1,66 +0,0 @@
-.page.log .nav {
- display: block;
- text-align: center;
- padding: 20px 0;
- margin: 0;
- font-size: 20px;
- position: fixed;
- width: 960px;
- bottom: 0;
- background: #4E5969;
-}
-
- .page.log .nav li {
- display: inline;
- padding: 5px 10px;
- margin: 0;
- cursor: pointer;
- }
-
- .page.log .nav li:hover:not(.active) {
- background: rgba(255, 255, 255, 0.1);
- }
-
- .page.log .nav li.active {
- font-weight: bold;
- cursor: default;
- font-size: 30px;
- }
-
-.page.log .loading {
- text-align: center;
- font-size: 20px;
- padding: 50px;
-}
-
-.page.log .container {
- padding: 30px 0 60px;
- overflow: hidden;
-}
-
-.page.log .container span {
- float: left;
- width: 86%;
- line-height: 150%;
- padding: 3px 0;
- border-top: 1px solid rgba(255, 255, 255, 0.2);
- font-size: 11px;
- font-family: Lucida Console, Monaco, Nimbus Mono L;
-}
-
- .page.log .container .error {
- color: #FFA4A4;
- white-space: pre-wrap;
- }
- .page.log .container .debug { color: lightgrey; }
-
- .page.log .container .time {
- clear: both;
- width: 14%;
- color: lightgrey;
- padding: 3px 0;
- font-size: 10px;
- }
-
- .page.log .container .time:last-child { display: none; }
-
diff --git a/couchpotato/core/plugins/log/static/log.js b/couchpotato/core/plugins/log/static/log.js
index 0e276b5838..39e5f6ead6 100644
--- a/couchpotato/core/plugins/log/static/log.js
+++ b/couchpotato/core/plugins/log/static/log.js
@@ -2,94 +2,304 @@ Page.Log = new Class({
Extends: PageBase,
+ order: 60,
name: 'log',
title: 'Show recent logs.',
has_tab: false,
- initialize: function(options){
- var self = this;
- self.parent(options)
-
+ navigation: null,
+ log_items: [],
+ report_text: '### Steps to reproduce:\n'+
+ '1. ..\n'+
+ '2. ..\n'+
+ '\n'+
+ '### Information:\n'+
+ 'Movie(s) I have this with: ...\n'+
+ 'Quality of the movie being searched: ...\n'+
+ 'Providers I use: ...\n'+
+ 'Version of CouchPotato: {version}\n'+
+ 'Running on: ...\n'+
+ '\n'+
+ '### Logs:\n'+
+ '```\n{issue}```',
- App.getBlock('more').addLink(new Element('a', {
- 'href': App.createUrl(self.name),
- 'text': self.name.capitalize(),
- 'title': self.title
- }))
-
- },
-
- indexAction: function(){
+ indexAction: function () {
var self = this;
self.getLogs(0);
},
- getLogs: function(nr){
+ getLogs: function (nr) {
var self = this;
- if(self.log) self.log.destroy();
+ if (self.log) self.log.destroy();
+
self.log = new Element('div.container.loading', {
- 'text': 'loading...'
- }).inject(self.el);
+ 'text': 'loading...',
+ 'events': {
+ 'mouseup:relay(.time)': function(e){
+ requestTimeout(function(){
+ self.showSelectionButton(e);
+ }, 100);
+ }
+ }
+ }).inject(self.content);
- Api.request('logging.get', {
+ if(self.navigation){
+ var nav = self.navigation.getElement('.nav');
+ nav.getElements('.active').removeClass('active');
+
+ self.navigation.getElements('li')[nr+1].addClass('active');
+ }
+
+ if(self.request && self.request.running) self.request.cancel();
+ self.request = Api.request('logging.get', {
'data': {
'nr': nr
},
- 'onComplete': function(json){
- self.log.set('html', self.addColors(json.log));
+ 'onComplete': function (json) {
+ self.log.set('text', '');
+ self.log_items = self.createLogElements(json.log);
+ self.log.adopt(self.log_items);
self.log.removeClass('loading');
+ self.scrollToBottom();
- new Fx.Scroll(window, {'duration': 0}).toBottom();
+ if(!self.navigation){
+ self.navigation = new Element('div.navigation').adopt(
+ new Element('h2[text=Logs]'),
+ new Element('div.hint', {
+ 'text': 'Select multiple lines & report an issue'
+ })
+ );
- var nav = new Element('ul.nav').inject(self.log, 'top');
- for (var i = 0; i <= json.total; i++) {
- new Element('li', {
- 'text': i+1,
- 'class': nr == i ? 'active': '',
+ var nav = new Element('ul.nav', {
'events': {
- 'click': function(e){
- self.getLogs(e.target.get('text')-1);
+ 'click:relay(li.select)': function (e, el) {
+ self.getLogs(parseInt(el.get('text')) - 1);
}
}
- }).inject(nav);
- };
+ }).inject(self.navigation);
- new Element('li', {
- 'text': 'clear',
- 'events': {
- 'click': function(){
- Api.request('logging.clear', {
- 'onComplete': function(){
- self.getLogs(0);
+ // Type selection
+ new Element('li.filter').grab(
+ new Element('select', {
+ 'events': {
+ 'change': function () {
+ var type_filter = this.getSelected()[0].get('value');
+ self.content.set('data-filter', type_filter);
+ self.scrollToBottom();
}
- });
+ }
+ }).adopt(
+ new Element('option', {'value': 'ALL', 'text': 'Show all logs'}),
+ new Element('option', {'value': 'INFO', 'text': 'Show only INFO'}),
+ new Element('option', {'value': 'DEBUG', 'text': 'Show only DEBUG'}),
+ new Element('option', {'value': 'ERROR', 'text': 'Show only ERROR'})
+ )
+ ).inject(nav);
- }
+ // Selections
+ for (var i = 0; i <= json.total; i++) {
+ new Element('li', {
+ 'text': i + 1,
+ 'class': 'select ' + (nr == i ? 'active' : '')
+ }).inject(nav);
}
- }).inject(nav)
+
+ // Clear button
+ new Element('li.clear', {
+ 'text': 'clear',
+ 'events': {
+ 'click': function () {
+ Api.request('logging.clear', {
+ 'onComplete': function () {
+ self.getLogs(0);
+ }
+ });
+
+ }
+ }
+ }).inject(nav);
+
+ // Add to page
+ self.navigation.inject(self.content, 'top');
+ }
}
});
},
- addColors: function(text){
- var self = this;
+ createLogElements: function (logs) {
+
+ var elements = [];
+
+ logs.each(function (log) {
+ elements.include(new Element('div', {
+ 'class': 'time ' + log.type.toLowerCase()
+ }).adopt(
+ new Element('span', {
+ 'text': log.time
+ }),
+ new Element('span.type', {
+ 'text': log.type
+ }),
+ new Element('span.message', {
+ 'text': log.message
+ })
+ ));
+ });
+
+ return elements;
+ },
+
+ scrollToBottom: function () {
+ new Fx.Scroll(this.content, {'duration': 0}).toBottom();
+ },
+
+ showSelectionButton: function(e){
+ var self = this,
+ selection = self.getSelected(),
+ start_node = selection.anchorNode,
+ parent_start = start_node.parentNode.getParent('.time'),
+ end_node = selection.focusNode.parentNode.getParent('.time'),
+ text = '';
+
+ var remove_button = function(){
+ self.log.getElements('.highlight').removeClass('highlight');
+ if(self.do_report)
+ self.do_report.destroy();
+ document.body.removeEvent('click', remove_button);
+ };
+ remove_button();
+
+ if(parent_start)
+ start_node = parent_start;
+
+ var index = {
+ 'start': self.log_items.indexOf(start_node),
+ 'end': self.log_items.indexOf(end_node)
+ };
+
+ if(index.start > index.end){
+ index = {
+ 'start': index.end,
+ 'end': index.start
+ };
+ }
+
+ var nodes = self.log_items.slice(index.start, index.end + 1);
+
+ nodes.each(function(node, nr){
+ node.addClass('highlight');
+ node.getElements('span').each(function(span){
+ text += self.spaceFill(span.get('text') + ' ', 6);
+ });
+ text += '\n';
+ });
+
+ self.do_report = new Element('a.do_report.button', {
+ 'text': 'Report issue',
+ 'styles': {
+ 'top': e.page.y,
+ 'left': e.page.x
+ },
+ 'events': {
+ 'click': function(e){
+ (e).stop();
+
+ self.showReport(text);
+ }
+ }
+ }).inject(document.body);
+
+ requestTimeout(function(){
+ document.body.addEvent('click', remove_button);
+ }, 0);
+
+ },
+
+ showReport: function(text){
+ var self = this,
+ version = Updater.getInfo(),
+ body = self.report_text
+ .replace('{issue}', text)
+ .replace('{version}', version ? version.version.repr : '...'),
+ textarea;
+
+ var overlay = new Element('div.mask.report_popup', {
+ 'method': 'post',
+ 'events': {
+ 'click': function(e){
+ overlay.destroy();
+ }
+ }
+ }).grab(
+ new Element('div.bug', {
+ 'events': {
+ 'click': function(e){
+ (e).stopPropagation();
+ }
+ }
+ }).adopt(
+ new Element('h1', {
+ 'text': 'Report a bug'
+ }),
+ new Element('span').adopt(
+ new Element('span', {
+ 'text': 'Read '
+ }),
+ new Element('a.button', {
+ 'target': '_blank',
+ 'text': 'the contributing guide',
+ 'href': 'https://github.com/RuudBurger/CouchPotatoServer/blob/develop/contributing.md'
+ }),
+ new Element('span', {
+ 'html': ' before posting, then copy the text below and FILL IN the dots.'
+ })
+ ),
+ textarea = new Element('textarea', {
+ 'text': body
+ }),
+ new Element('a.button', {
+ 'target': '_blank',
+ 'text': 'Create a new issue on GitHub with the text above',
+ 'href': 'https://github.com/RuudBurger/CouchPotatoServer/issues/new',
+ 'events': {
+ 'click': function(e){
+ (e).stop();
+
+ var body = textarea.get('value'),
+ bdy = '?body=' + (body.length < 2000 ? encodeURIComponent(body) : 'Paste the text here'),
+ win = window.open(e.target.get('href') + bdy, '_blank');
+ win.focus();
+ }
+ }
+ })
+ )
+ );
+
+ overlay.inject(document.body);
+ },
+
+ getSelected: function(){
+ if (window.getSelection)
+ return window.getSelection();
+ else if (document.getSelection)
+ return document.getSelection();
+ else {
+ var selection = document.selection && document.selection.createRange();
+ if (selection.text)
+ return selection.text;
+ }
+ return false;
+
+ },
- text = text
- .replace(/&/g, '&')
- .replace(//g, '>')
- .replace(/"/g, '"')
- .replace(/\u001b\[31m/gi, '
')
- .replace(/\u001b\[36m/gi, '')
- .replace(/\u001b\[33m/gi, '')
- .replace(/\u001b\[0m\n/gi, '')
- .replace(/\u001b\[0m/gi, '')
-
- return '' + text + '';
+ spaceFill: function( number, width ){
+ if ( number.toString().length >= width )
+ return number;
+ return ( new Array( width ).join( ' ' ) + number.toString() ).substr( -width );
}
-})
\ No newline at end of file
+});
diff --git a/couchpotato/core/plugins/log/static/log.scss b/couchpotato/core/plugins/log/static/log.scss
new file mode 100644
index 0000000000..e3667802bb
--- /dev/null
+++ b/couchpotato/core/plugins/log/static/log.scss
@@ -0,0 +1,189 @@
+@import "_mixins";
+
+.page.log {
+
+ .nav {
+ text-align: right;
+ padding: 0;
+ margin: 0;
+
+ li {
+ display: inline-block;
+ padding: 5px 10px;
+ margin: 0;
+
+ @include media-tablet {
+ &.filter, &:nth-child(7),&:nth-child(8),&:nth-child(9),&:nth-child(10),&:nth-child(11),&:nth-child(12) {
+ display: none;
+ }
+
+ &:last-child {
+ display: inline-block;
+ }
+ }
+
+ &.select, &.clear {
+ cursor: pointer;
+ }
+
+ &:hover:not(.active):not(.filter) {
+ background: rgba(255,255,255,.1);
+ }
+
+ &.active {
+ font-weight: bold;
+ cursor: default;
+ background: rgba(255,255,255,.1);
+ }
+ }
+ }
+
+ .hint {
+ font-style: italic;
+ opacity: .5;
+ margin-top: 3px;
+
+ @include media-tablet {
+ display: none;
+ }
+ }
+
+ .container {
+ padding: $padding;
+ overflow: hidden;
+ line-height: 150%;
+
+ @include media-phablet {
+ padding: $padding $padding/2;
+ }
+
+ &.loading {
+ text-align: center;
+ font-size: 20px;
+ padding: 100px 50px;
+ }
+
+ select {
+ vertical-align: top;
+ }
+
+ .time {
+ clear: both;
+ font-size: .75em;
+ border-top: 1px solid rgba(255,255,255,.1);
+ overflow: hidden;
+ padding: 0 3px;
+ font-family: Lucida Console, Monaco, Nimbus Mono L, monospace, serif;
+ display: flex;
+
+ @include media-phablet {
+ flex-flow: row wrap;
+ }
+
+ &.highlight {
+ @include theme(background, off);
+ }
+
+ span {
+ padding: 5px 0 3px;
+ display: inline-block;
+ vertical-align: middle;
+ width: 95px;
+ }
+
+ .message {
+ white-space: pre-wrap;
+ flex: 1 auto;
+
+ @include media-phablet {
+ flex: 1 auto;
+ margin-top: -$padding/4;
+ padding: 0;
+ display: block;
+ width: 100%;
+ }
+ }
+
+ ::selection {
+ @include theme(background-color, background);
+ @include theme(color, text);
+ }
+ }
+
+ .type.type {
+ margin-left: $padding/3;
+ width: 40px;
+ }
+
+
+ .error { color: #FFA4A4; }
+ .debug span { opacity: .6; }
+ }
+
+
+
+ [data-filter=INFO] .error,
+ [data-filter=INFO] .debug,
+ [data-filter=ERROR] .debug,
+ [data-filter=ERROR] .info,
+ [data-filter=DEBUG] .info,
+ [data-filter=DEBUG] .error {
+ display: none;
+ }
+}
+
+.report_popup.report_popup {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ top: 0;
+ z-index: 99999;
+ font-size: 14px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ opacity: 1;
+ color: #FFF;
+ pointer-events: auto;
+
+ .button {
+ margin: 10px 0;
+ padding: 10px;
+ color: #FFF;
+ @include theme(background, primary);
+ }
+
+ .bug {
+ width: 80%;
+ height: 80%;
+ max-height: 800px;
+ max-width: 800px;
+
+ display: flex;
+ flex-flow: column nowrap;
+
+ > span {
+ margin: $padding/2 0 $padding 0;
+ }
+
+ textarea {
+ display: block;
+ width: 100%;
+ background: #FFF;
+ padding: 20px;
+ overflow: auto;
+ color: #666;
+ height: 70%;
+ font-size: 12px;
+ }
+ }
+}
+
+.do_report.do_report {
+ z-index: 10000;
+ position: absolute;
+ padding: 10px;
+ @include theme(background, primary);
+ color: #FFF !important;
+}
diff --git a/couchpotato/core/plugins/manage.py b/couchpotato/core/plugins/manage.py
new file mode 100755
index 0000000000..b0e1239c54
--- /dev/null
+++ b/couchpotato/core/plugins/manage.py
@@ -0,0 +1,323 @@
+import os
+import time
+import traceback
+
+from couchpotato import get_db
+from couchpotato.api import addApiView
+from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
+from couchpotato.core.helpers.encoding import sp
+from couchpotato.core.helpers.variable import splitString, getTitle, tryInt, getIdentifier, getFreeSpace
+from couchpotato.core.logger import CPLog
+from couchpotato.core.plugins.base import Plugin
+from couchpotato.environment import Env
+
+
+log = CPLog(__name__)
+
+autoload = 'Manage'
+
+
+class Manage(Plugin):
+
+ in_progress = False
+
+ def __init__(self):
+
+ fireEvent('scheduler.interval', identifier = 'manage.update_library', handle = self.updateLibrary, hours = 2)
+
+ addEvent('manage.update', self.updateLibrary)
+ addEvent('manage.diskspace', self.getDiskSpace)
+
+ # Add files after renaming
+ def after_rename(message = None, group = None):
+ if not group: group = {}
+ return self.scanFilesToLibrary(folder = group['destination_dir'], files = group['renamed_files'], release_download = group['release_download'])
+ addEvent('renamer.after', after_rename, priority = 110)
+
+ addApiView('manage.update', self.updateLibraryView, docs = {
+ 'desc': 'Update the library by scanning for new movies',
+ 'params': {
+ 'full': {'desc': 'Do a full update or just recently changed/added movies.'},
+ }
+ })
+
+ addApiView('manage.progress', self.getProgress, docs = {
+ 'desc': 'Get the progress of current manage update',
+ 'return': {'type': 'object', 'example': """{
+ 'progress': False || object, total & to_go,
+}"""},
+ })
+
+ if not Env.get('dev') and self.conf('startup_scan'):
+ addEvent('app.load', self.updateLibraryQuick)
+
+ addEvent('app.load', self.setCrons)
+
+ # Enable / disable interval
+ addEvent('setting.save.manage.library_refresh_interval.after', self.setCrons)
+
+ def setCrons(self):
+
+ fireEvent('schedule.remove', 'manage.update_library')
+ refresh = tryInt(self.conf('library_refresh_interval'))
+ if refresh > 0:
+ fireEvent('schedule.interval', 'manage.update_library', self.updateLibrary, hours = refresh, single = True)
+
+ return True
+
+ def getProgress(self, **kwargs):
+ return {
+ 'progress': self.in_progress
+ }
+
+ def updateLibraryView(self, full = 1, **kwargs):
+
+ fireEventAsync('manage.update', full = True if full == '1' else False)
+
+ return {
+ 'progress': self.in_progress,
+ 'success': True
+ }
+
+ def updateLibraryQuick(self):
+ return self.updateLibrary(full = False)
+
+ def updateLibrary(self, full = True):
+ last_update_key = 'manage.last_update%s' % ('_full' if full else '')
+ last_update = float(Env.prop(last_update_key, default = 0))
+
+ if self.in_progress:
+ log.info('Already updating library: %s', self.in_progress)
+ return
+ elif self.isDisabled() or (last_update > time.time() - 20):
+ return
+
+ self.in_progress = {}
+ fireEvent('notify.frontend', type = 'manage.updating', data = True)
+
+ try:
+
+ directories = self.directories()
+ directories.sort()
+ added_identifiers = []
+
+ # Add some progress
+ for directory in directories:
+ self.in_progress[os.path.normpath(directory)] = {
+ 'started': False,
+ 'eta': -1,
+ 'total': None,
+ 'to_go': None,
+ }
+
+ for directory in directories:
+ folder = os.path.normpath(directory)
+ self.in_progress[os.path.normpath(directory)]['started'] = tryInt(time.time())
+
+ if not os.path.isdir(folder):
+ if len(directory) > 0:
+ log.error('Directory doesn\'t exist: %s', folder)
+ continue
+
+ log.info('Updating manage library: %s', folder)
+ fireEvent('notify.frontend', type = 'manage.update', data = True, message = 'Scanning for movies in "%s"' % folder)
+
+ onFound = self.createAddToLibrary(folder, added_identifiers)
+ fireEvent('scanner.scan', folder = folder, simple = True, newer_than = last_update if not full else 0, check_file_date = False, on_found = onFound, single = True)
+
+ # Break if CP wants to shut down
+ if self.shuttingDown():
+ break
+
+ # If cleanup option is enabled, remove offline files from database
+ if self.conf('cleanup') and full and not self.shuttingDown():
+
+ # Get movies with done status
+ total_movies, done_movies = fireEvent('media.list', types = 'movie', status = 'done', release_status = 'done', status_or = True, single = True)
+
+ deleted_releases = []
+ for done_movie in done_movies:
+ if getIdentifier(done_movie) not in added_identifiers:
+ fireEvent('media.delete', media_id = done_movie['_id'], delete_from = 'all')
+ else:
+
+ releases = done_movie.get('releases', [])
+
+ for release in releases:
+ if release.get('files'):
+ brk = False
+ for file_type in release.get('files', {}):
+ for release_file in release['files'][file_type]:
+ # Remove release not available anymore
+ if not os.path.isfile(sp(release_file)):
+ fireEvent('release.clean', release['_id'])
+ brk = True
+ break
+ if brk:
+ break
+
+ # Check if there are duplicate releases (different quality) use the last one, delete the rest
+ if len(releases) > 1:
+ used_files = {}
+ for release in releases:
+ for file_type in release.get('files', {}):
+ for release_file in release['files'][file_type]:
+ already_used = used_files.get(release_file)
+
+ if already_used:
+ release_id = release['_id'] if already_used.get('last_edit', 0) > release.get('last_edit', 0) else already_used['_id']
+ if release_id not in deleted_releases:
+ fireEvent('release.delete', release_id, single = True)
+ deleted_releases.append(release_id)
+ break
+ else:
+ used_files[release_file] = release
+ del used_files
+
+ # Break if CP wants to shut down
+ if self.shuttingDown():
+ break
+
+ if not self.shuttingDown():
+ db = get_db()
+ db.reindex()
+
+ Env.prop(last_update_key, time.time())
+ except:
+ log.error('Failed updating library: %s', (traceback.format_exc()))
+
+ while self.in_progress and len(self.in_progress) > 0 and not self.shuttingDown():
+
+ delete_me = {}
+
+ # noinspection PyTypeChecker
+ for folder in self.in_progress:
+ if self.in_progress[folder]['to_go'] <= 0:
+ delete_me[folder] = True
+
+ for delete in delete_me:
+ del self.in_progress[delete]
+
+ time.sleep(1)
+
+ fireEvent('notify.frontend', type = 'manage.updating', data = False)
+ self.in_progress = False
+
+ # noinspection PyDefaultArgument
+ def createAddToLibrary(self, folder, added_identifiers = []):
+
+ def addToLibrary(group, total_found, to_go):
+ if self.in_progress[folder]['total'] is None:
+ self.in_progress[folder].update({
+ 'total': total_found,
+ 'to_go': total_found,
+ })
+
+ self.updateProgress(folder, to_go)
+
+ if group['media'] and group['identifier']:
+ added_identifiers.append(group['identifier'])
+
+ # Add it to release and update the info
+ fireEvent('release.add', group = group, update_info = False)
+ fireEvent('movie.update', identifier = group['identifier'], on_complete = self.createAfterUpdate(folder, group['identifier']))
+
+ return addToLibrary
+
+ def createAfterUpdate(self, folder, identifier):
+
+ # Notify frontend
+ def afterUpdate():
+ if not self.in_progress or self.shuttingDown():
+ return
+
+ total = self.in_progress[folder]['total']
+ movie_dict = fireEvent('media.get', identifier, single = True)
+
+ if movie_dict:
+ fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = None if total > 5 else 'Added "%s" to manage.' % getTitle(movie_dict))
+
+ return afterUpdate
+
+ def updateProgress(self, folder, to_go):
+
+ pr = self.in_progress[folder]
+ if to_go < pr['to_go']:
+ pr['to_go'] = to_go
+
+ avg = (time.time() - pr['started']) / (pr['total'] - pr['to_go'])
+ pr['eta'] = tryInt(avg * pr['to_go'])
+
+
+ def directories(self):
+ try:
+ if self.conf('library', default = '').strip():
+ return splitString(self.conf('library', default = ''), '::')
+ except:
+ pass
+
+ return []
+
+ def scanFilesToLibrary(self, folder = None, files = None, release_download = None):
+
+ folder = os.path.normpath(folder)
+
+ groups = fireEvent('scanner.scan', folder = folder, files = files, single = True)
+
+ if groups:
+ for group in groups.values():
+ if group.get('media'):
+ if release_download and release_download.get('release_id'):
+ fireEvent('release.add', group = group, update_id = release_download.get('release_id'))
+ else:
+ fireEvent('release.add', group = group)
+
+ def getDiskSpace(self):
+ return getFreeSpace(self.directories())
+
+
+config = [{
+ 'name': 'manage',
+ 'groups': [
+ {
+ 'tab': 'manage',
+ 'label': 'Movie Library Manager',
+ 'description': 'Add your existing movie folders.',
+ 'options': [
+ {
+ 'name': 'enabled',
+ 'default': False,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'library',
+ 'type': 'directories',
+ 'description': 'Folder where the movies should be moved to.',
+ },
+ {
+ 'label': 'Cleanup After',
+ 'name': 'cleanup',
+ 'type': 'bool',
+ 'description': 'Remove movie from db if it can\'t be found after re-scan.',
+ 'default': True,
+ },
+ {
+ 'label': 'Scan at startup',
+ 'name': 'startup_scan',
+ 'type': 'bool',
+ 'default': True,
+ 'advanced': True,
+ 'description': 'Do a quick scan on startup. On slow systems better disable this.',
+ },
+ {
+ 'label': 'Full library refresh',
+ 'name': 'library_refresh_interval',
+ 'type': 'int',
+ 'default': 0,
+ 'advanced': True,
+ 'description': 'Do a full scan every X hours. (0 is disabled)',
+ },
+ ],
+ },
+ ],
+}]
diff --git a/couchpotato/core/plugins/manage/__init__.py b/couchpotato/core/plugins/manage/__init__.py
deleted file mode 100644
index 30f6ea686d..0000000000
--- a/couchpotato/core/plugins/manage/__init__.py
+++ /dev/null
@@ -1,34 +0,0 @@
-from .main import Manage
-
-def start():
- return Manage()
-
-config = [{
- 'name': 'manage',
- 'groups': [
- {
- 'tab': 'manage',
- 'label': 'movie library manager',
- 'description': 'Add your existing movie folders.',
- 'options': [
- {
- 'name': 'enabled',
- 'default': False,
- 'type': 'enabler',
- },
- {
- 'name': 'library',
- 'type': 'directories',
- 'description': 'Folder where the movies should be moved to.',
- },
- {
- 'label': 'Cleanup After',
- 'name': 'cleanup',
- 'type': 'bool',
- 'description': 'Remove movie from db if it can\'t be found after re-scan.',
- 'default': True,
- },
- ],
- },
- ],
-}]
diff --git a/couchpotato/core/plugins/manage/main.py b/couchpotato/core/plugins/manage/main.py
deleted file mode 100644
index 51094899d4..0000000000
--- a/couchpotato/core/plugins/manage/main.py
+++ /dev/null
@@ -1,248 +0,0 @@
-from couchpotato.api import addApiView
-from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
-from couchpotato.core.helpers.encoding import ss
-from couchpotato.core.helpers.request import jsonified, getParam
-from couchpotato.core.helpers.variable import splitString, getTitle
-from couchpotato.core.logger import CPLog
-from couchpotato.core.plugins.base import Plugin
-from couchpotato.environment import Env
-import ctypes
-import os
-import sys
-import time
-import traceback
-
-
-log = CPLog(__name__)
-
-class Manage(Plugin):
-
- in_progress = False
-
- def __init__(self):
-
- fireEvent('scheduler.interval', identifier = 'manage.update_library', handle = self.updateLibrary, hours = 2)
-
- addEvent('manage.update', self.updateLibrary)
- addEvent('manage.diskspace', self.getDiskSpace)
-
- # Add files after renaming
- def after_rename(message = None, group = {}):
- return self.scanFilesToLibrary(folder = group['destination_dir'], files = group['renamed_files'])
- addEvent('renamer.after', after_rename, priority = 110)
-
- addApiView('manage.update', self.updateLibraryView, docs = {
- 'desc': 'Update the library by scanning for new movies',
- 'params': {
- 'full': {'desc': 'Do a full update or just recently changed/added movies.'},
- }
- })
-
- addApiView('manage.progress', self.getProgress, docs = {
- 'desc': 'Get the progress of current manage update',
- 'return': {'type': 'object', 'example': """{
- 'progress': False || object, total & to_go,
-}"""},
- })
-
- if not Env.get('dev'):
- def updateLibrary():
- self.updateLibrary(full = False)
- addEvent('app.load', updateLibrary)
-
- def getProgress(self):
- return jsonified({
- 'progress': self.in_progress
- })
-
- def updateLibraryView(self):
-
- full = getParam('full', default = 1)
- fireEventAsync('manage.update', full = True if full == '1' else False)
-
- return jsonified({
- 'success': True
- })
-
-
- def updateLibrary(self, full = True):
- last_update = float(Env.prop('manage.last_update', default = 0))
-
- if self.in_progress:
- log.info('Already updating library: %s', self.in_progress)
- return
- elif self.isDisabled() or (last_update > time.time() - 20):
- return
-
- self.in_progress = {}
- fireEvent('notify.frontend', type = 'manage.updating', data = True)
-
- try:
-
- directories = self.directories()
- added_identifiers = []
-
- # Add some progress
- self.in_progress = {}
- for directory in directories:
- self.in_progress[os.path.normpath(directory)] = {
- 'total': None,
- 'to_go': None,
- }
-
- for directory in directories:
- folder = os.path.normpath(directory)
-
- if not os.path.isdir(folder):
- if len(directory) > 0:
- log.error('Directory doesn\'t exist: %s', folder)
- continue
-
- log.info('Updating manage library: %s', folder)
- fireEvent('notify.frontend', type = 'manage.update', data = True, message = 'Scanning for movies in "%s"' % folder)
-
- onFound = self.createAddToLibrary(folder, added_identifiers)
- fireEvent('scanner.scan', folder = folder, simple = True, newer_than = last_update if not full else 0, on_found = onFound, single = True)
-
- # Break if CP wants to shut down
- if self.shuttingDown():
- break
-
- # If cleanup option is enabled, remove offline files from database
- if self.conf('cleanup') and full and not self.shuttingDown():
-
- # Get movies with done status
- total_movies, done_movies = fireEvent('movie.list', status = 'done', single = True)
-
- for done_movie in done_movies:
- if done_movie['library']['identifier'] not in added_identifiers:
- fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all')
- else:
-
- for release in done_movie.get('releases', []):
- if len(release.get('files', [])) == 0:
- fireEvent('release.delete', release['id'])
- else:
- for release_file in release.get('files', []):
- # Remove release not available anymore
- if not os.path.isfile(ss(release_file['path'])):
- fireEvent('release.clean', release['id'])
- break
-
- # Check if there are duplicate releases (different quality) use the last one, delete the rest
- if len(done_movie.get('releases', [])) > 1:
- used_files = {}
- for release in done_movie.get('releases', []):
-
- for release_file in release.get('files', []):
- already_used = used_files.get(release_file['path'])
-
- if already_used:
- if already_used < release['id']:
- fireEvent('release.delete', release['id'], single = True) # delete this one
- else:
- fireEvent('release.delete', already_used, single = True) # delete previous one
- break
- else:
- used_files[release_file['path']] = release.get('id')
- del used_files
-
- Env.prop('manage.last_update', time.time())
- except:
- log.error('Failed updating library: %s', (traceback.format_exc()))
-
- while True and not self.shuttingDown():
-
- delete_me = {}
-
- for folder in self.in_progress:
- if self.in_progress[folder]['to_go'] <= 0:
- delete_me[folder] = True
-
- for delete in delete_me:
- del self.in_progress[delete]
-
- if len(self.in_progress) == 0:
- break
-
- time.sleep(1)
-
- fireEvent('notify.frontend', type = 'manage.updating', data = False)
- self.in_progress = False
-
- def createAddToLibrary(self, folder, added_identifiers = []):
- def addToLibrary(group, total_found, to_go):
- if self.in_progress[folder]['total'] is None:
- self.in_progress[folder] = {
- 'total': total_found,
- 'to_go': total_found,
- }
-
- if group['library'] and group['library'].get('identifier'):
- identifier = group['library'].get('identifier')
- added_identifiers.append(identifier)
-
- # Add it to release and update the info
- fireEvent('release.add', group = group)
- fireEventAsync('library.update', identifier = identifier, on_complete = self.createAfterUpdate(folder, identifier))
-
- return addToLibrary
-
- def createAfterUpdate(self, folder, identifier):
-
- # Notify frontend
- def afterUpdate():
- self.in_progress[folder]['to_go'] = self.in_progress[folder]['to_go'] - 1
- total = self.in_progress[folder]['total']
- movie_dict = fireEvent('movie.get', identifier, single = True)
-
- fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = None if total > 5 else 'Added "%s" to manage.' % getTitle(movie_dict['library']))
-
- return afterUpdate
-
- def directories(self):
- try:
- if self.conf('library', default = '').strip():
- return splitString(self.conf('library', default = ''), '::')
- except:
- pass
-
- return []
-
- def scanFilesToLibrary(self, folder = None, files = None):
-
- folder = os.path.normpath(folder)
-
- groups = fireEvent('scanner.scan', folder = folder, files = files, single = True)
-
- for group in groups.itervalues():
- if group['library'] and group['library'].get('identifier'):
- fireEvent('release.add', group = group)
-
- def getDiskSpace(self):
-
- free_space = {}
- for folder in self.directories():
-
- size = None
- if os.path.isdir(folder):
- if os.name == 'nt':
- _, total, free = ctypes.c_ulonglong(), ctypes.c_ulonglong(), \
- ctypes.c_ulonglong()
- if sys.version_info >= (3,) or isinstance(folder, unicode):
- fun = ctypes.windll.kernel32.GetDiskFreeSpaceExW #@UndefinedVariable
- else:
- fun = ctypes.windll.kernel32.GetDiskFreeSpaceExA #@UndefinedVariable
- ret = fun(folder, ctypes.byref(_), ctypes.byref(total), ctypes.byref(free))
- if ret == 0:
- raise ctypes.WinError()
- used = total.value - free.value
- return [total.value, used, free.value]
- else:
- s = os.statvfs(folder)
- size = [s.f_blocks * s.f_frsize / (1024 * 1024), (s.f_bavail * s.f_frsize) / (1024 * 1024)]
-
- free_space[folder] = size
-
- return free_space
-
diff --git a/couchpotato/core/plugins/movie/__init__.py b/couchpotato/core/plugins/movie/__init__.py
deleted file mode 100644
index 4df29ad88a..0000000000
--- a/couchpotato/core/plugins/movie/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from .main import MoviePlugin
-
-def start():
- return MoviePlugin()
-
-config = []
diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py
deleted file mode 100644
index c708ec4e04..0000000000
--- a/couchpotato/core/plugins/movie/main.py
+++ /dev/null
@@ -1,587 +0,0 @@
-from couchpotato import get_session
-from couchpotato.api import addApiView
-from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
-from couchpotato.core.helpers.encoding import toUnicode, simplifyString
-from couchpotato.core.helpers.request import getParams, jsonified, getParam
-from couchpotato.core.helpers.variable import getImdb, splitString
-from couchpotato.core.logger import CPLog
-from couchpotato.core.plugins.base import Plugin
-from couchpotato.core.settings.model import Library, LibraryTitle, Movie, \
- Release
-from couchpotato.environment import Env
-from sqlalchemy.orm import joinedload_all
-from sqlalchemy.sql.expression import or_, asc, not_, desc
-from string import ascii_lowercase
-
-log = CPLog(__name__)
-
-
-class MoviePlugin(Plugin):
-
- default_dict = {
- 'profile': {'types': {'quality': {}}},
- 'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
- 'library': {'titles': {}, 'files':{}},
- 'files': {},
- 'status': {}
- }
-
- def __init__(self):
- addApiView('movie.search', self.search, docs = {
- 'desc': 'Search the movie providers for a movie',
- 'params': {
- 'q': {'desc': 'The (partial) movie name you want to search for'},
- },
- 'return': {'type': 'object', 'example': """{
- 'success': True,
- 'empty': bool, any movies returned or not,
- 'movies': array, movies found,
-}"""}
- })
- addApiView('movie.list', self.listView, docs = {
- 'desc': 'List movies in wanted list',
- 'params': {
- 'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'},
- 'release_status': {'type': 'array or csv', 'desc': 'Filter movie by status of its releases. Example:"snatched,available"'},
- 'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'},
- 'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'},
- 'search': {'desc': 'Search movie title'},
- },
- 'return': {'type': 'object', 'example': """{
- 'success': True,
- 'empty': bool, any movies returned or not,
- 'movies': array, movies found,
-}"""}
- })
- addApiView('movie.get', self.getView, docs = {
- 'desc': 'Get a movie by id',
- 'params': {
- 'id': {'desc': 'The id of the movie'},
- }
- })
- addApiView('movie.refresh', self.refresh, docs = {
- 'desc': 'Refresh a movie by id',
- 'params': {
- 'id': {'desc': 'Movie ID(s) you want to refresh.', 'type': 'int (comma separated)'},
- }
- })
- addApiView('movie.available_chars', self.charView)
- addApiView('movie.add', self.addView, docs = {
- 'desc': 'Add new movie to the wanted list',
- 'params': {
- 'identifier': {'desc': 'IMDB id of the movie your want to add.'},
- 'profile_id': {'desc': 'ID of quality profile you want the add the movie in. If empty will use the default profile.'},
- 'title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'},
- }
- })
- addApiView('movie.edit', self.edit, docs = {
- 'desc': 'Add new movie to the wanted list',
- 'params': {
- 'id': {'desc': 'Movie ID(s) you want to edit.', 'type': 'int (comma separated)'},
- 'profile_id': {'desc': 'ID of quality profile you want the edit the movie to.'},
- 'default_title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'},
- }
- })
- addApiView('movie.delete', self.deleteView, docs = {
- 'desc': 'Delete a movie from the wanted list',
- 'params': {
- 'id': {'desc': 'Movie ID(s) you want to delete.', 'type': 'int (comma separated)'},
- 'delete_from': {'desc': 'Delete movie from this page', 'type': 'string: all (default), wanted, manage'},
- }
- })
-
- addEvent('movie.add', self.add)
- addEvent('movie.delete', self.delete)
- addEvent('movie.get', self.get)
- addEvent('movie.list', self.list)
- addEvent('movie.restatus', self.restatus)
-
- addEvent('app.load', self.cleanReleases)
-
- def cleanReleases(self):
-
- prop_name = 'cleaned_releases'
- already_cleaned = Env.prop(prop_name, default = False)
- if already_cleaned:
- return True
-
- log.info('Removing releases from library movies')
-
- db = get_session()
-
- movies = db.query(Movie).all()
-
- done_status = fireEvent('status.get', 'done', single = True)
- available_status = fireEvent('status.get', 'available', single = True)
- snatched_status = fireEvent('status.get', 'snatched', single = True)
-
- for movie in movies:
- if movie.status_id == done_status.get('id'):
- for rel in movie.releases:
- if rel.status_id in [available_status.get('id'), snatched_status.get('id')]:
- fireEvent('release.delete', id = rel.id, single = True)
-
- Env.prop(prop_name, True)
-
- def getView(self):
-
- movie_id = getParam('id')
- movie = self.get(movie_id) if movie_id else None
-
- return jsonified({
- 'success': movie is not None,
- 'movie': movie,
- })
-
- def get(self, movie_id):
-
- db = get_session()
-
- imdb_id = getImdb(str(movie_id))
-
- if(imdb_id):
- m = db.query(Movie).filter(Movie.library.has(identifier = imdb_id)).first()
- else:
- m = db.query(Movie).filter_by(id = movie_id).first()
-
- results = None
- if m:
- results = m.to_dict(self.default_dict)
-
- return results
-
- def list(self, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None):
-
- db = get_session()
-
- # Make a list from string
- if status and not isinstance(status, (list, tuple)):
- status = [status]
- if release_status and not isinstance(release_status, (list, tuple)):
- release_status = [release_status]
-
- q = db.query(Movie) \
- .outerjoin(Movie.releases, Movie.library, Library.titles) \
- .filter(LibraryTitle.default == True) \
- .group_by(Movie.id)
-
- # Filter on movie status
- if status and len(status) > 0:
- q = q.filter(or_(*[Movie.status.has(identifier = s) for s in status]))
-
- # Filter on release status
- if release_status and len(release_status) > 0:
- q = q.filter(or_(*[Release.status.has(identifier = s) for s in release_status]))
-
- total_count = q.count()
-
- filter_or = []
- if starts_with:
- starts_with = toUnicode(starts_with.lower())
- if starts_with in ascii_lowercase:
- filter_or.append(LibraryTitle.simple_title.startswith(starts_with))
- else:
- ignore = []
- for letter in ascii_lowercase:
- ignore.append(LibraryTitle.simple_title.startswith(toUnicode(letter)))
- filter_or.append(not_(or_(*ignore)))
-
- if search:
- filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%'))
-
- if filter_or:
- q = q.filter(or_(*filter_or))
-
- if order == 'release_order':
- q = q.order_by(desc(Release.last_edit))
- else:
- q = q.order_by(asc(LibraryTitle.simple_title))
-
- q = q.subquery()
- q2 = db.query(Movie).join((q, q.c.id == Movie.id)) \
- .options(joinedload_all('releases')) \
- .options(joinedload_all('profile.types')) \
- .options(joinedload_all('library.titles')) \
- .options(joinedload_all('library.files')) \
- .options(joinedload_all('status')) \
- .options(joinedload_all('files'))
-
- if limit_offset:
- splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
- limit = splt[0]
- offset = 0 if len(splt) is 1 else splt[1]
- q2 = q2.limit(limit).offset(offset)
-
- results = q2.all()
- movies = []
- for movie in results:
- temp = movie.to_dict({
- 'profile': {'types': {}},
- 'releases': {'files':{}, 'info': {}},
- 'library': {'titles': {}, 'files':{}},
- 'files': {},
- })
- movies.append(temp)
-
- #db.close()
- return (total_count, movies)
-
- def availableChars(self, status = None, release_status = None):
-
- chars = ''
-
- db = get_session()
-
- # Make a list from string
- if not isinstance(status, (list, tuple)):
- status = [status]
- if release_status and not isinstance(release_status, (list, tuple)):
- release_status = [release_status]
-
- q = db.query(Movie) \
- .outerjoin(Movie.releases, Movie.library, Library.titles, Movie.status) \
- .options(joinedload_all('library.titles'))
-
- # Filter on movie status
- if status and len(status) > 0:
- q = q.filter(or_(*[Movie.status.has(identifier = s) for s in status]))
-
- # Filter on release status
- if release_status and len(release_status) > 0:
- q = q.filter(or_(*[Release.status.has(identifier = s) for s in release_status]))
-
- results = q.all()
-
- for movie in results:
- char = movie.library.titles[0].simple_title[0]
- char = char if char in ascii_lowercase else '#'
- if char not in chars:
- chars += str(char)
-
- #db.close()
- return ''.join(sorted(chars, key = str.lower))
-
- def listView(self):
-
- params = getParams()
- status = splitString(params.get('status', None))
- release_status = splitString(params.get('release_status', None))
- limit_offset = params.get('limit_offset', None)
- starts_with = params.get('starts_with', None)
- search = params.get('search', None)
- order = params.get('order', None)
-
- total_movies, movies = self.list(
- status = status,
- release_status = release_status,
- limit_offset = limit_offset,
- starts_with = starts_with,
- search = search,
- order = order
- )
-
- return jsonified({
- 'success': True,
- 'empty': len(movies) == 0,
- 'total': total_movies,
- 'movies': movies,
- })
-
- def charView(self):
-
- params = getParams()
- status = splitString(params.get('status', None))
- release_status = splitString(params.get('release_status', None))
- chars = self.availableChars(status, release_status)
-
- return jsonified({
- 'success': True,
- 'empty': len(chars) == 0,
- 'chars': chars,
- })
-
- def refresh(self):
-
- db = get_session()
-
- for id in splitString(getParam('id')):
- movie = db.query(Movie).filter_by(id = id).first()
-
- if movie:
-
- # Get current selected title
- default_title = ''
- for title in movie.library.titles:
- if title.default: default_title = title.title
-
- fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True, message = 'Updating "%s"' % default_title)
- fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(id))
-
-
- #db.close()
- return jsonified({
- 'success': True,
- })
-
- def search(self):
-
- q = getParam('q')
- cache_key = u'%s/%s' % (__name__, simplifyString(q))
- movies = Env.get('cache').get(cache_key)
-
- if not movies:
-
- if getImdb(q):
- movies = [fireEvent('movie.info', identifier = q, merge = True)]
- else:
- movies = fireEvent('movie.search', q = q, merge = True)
- Env.get('cache').set(cache_key, movies)
-
- return jsonified({
- 'success': True,
- 'empty': len(movies) == 0 if movies else 0,
- 'movies': movies,
- })
-
- def add(self, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None):
-
- if not params.get('identifier'):
- msg = 'Can\'t add movie without imdb identifier.'
- log.error(msg)
- fireEvent('notify.frontend', type = 'movie.is_tvshow', message = msg)
- return False
- else:
- try:
- is_movie = fireEvent('movie.is_movie', identifier = params.get('identifier'), single = True)
- if not is_movie:
- msg = 'Can\'t add movie, seems to be a TV show.'
- log.error(msg)
- fireEvent('notify.frontend', type = 'movie.is_tvshow', message = msg)
- return False
- except:
- pass
-
-
- library = fireEvent('library.add', single = True, attrs = params, update_after = update_library)
-
- # Status
- status_active = fireEvent('status.add', 'active', single = True)
- status_snatched = fireEvent('status.add', 'snatched', single = True)
-
- default_profile = fireEvent('profile.default', single = True)
-
- db = get_session()
- m = db.query(Movie).filter_by(library_id = library.get('id')).first()
- added = True
- do_search = False
- if not m:
- m = Movie(
- library_id = library.get('id'),
- profile_id = params.get('profile_id', default_profile.get('id')),
- status_id = status_id if status_id else status_active.get('id'),
- )
- db.add(m)
- db.commit()
-
- onComplete = None
- if search_after:
- onComplete = self.createOnComplete(m.id)
-
- fireEventAsync('library.update', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete)
- search_after = False
- elif force_readd:
- # Clean snatched history
- for release in m.releases:
- if release.status_id == status_snatched.get('id'):
- release.delete()
-
- m.profile_id = params.get('profile_id', default_profile.get('id'))
- else:
- log.debug('Movie already exists, not updating: %s', params)
- added = False
-
- if force_readd:
- m.status_id = status_id if status_id else status_active.get('id')
- do_search = True
-
- db.commit()
-
- # Remove releases
- available_status = fireEvent('status.get', 'available', single = True)
- for rel in m.releases:
- if rel.status_id is available_status.get('id'):
- db.delete(rel)
- db.commit()
-
- movie_dict = m.to_dict(self.default_dict)
-
- if do_search and search_after:
- onComplete = self.createOnComplete(m.id)
- onComplete()
-
- if added:
- fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = 'Successfully added "%s" to your wanted list.' % params.get('title', ''))
-
- #db.close()
- return movie_dict
-
-
- def addView(self):
-
- params = getParams()
-
- movie_dict = self.add(params)
-
- return jsonified({
- 'success': True,
- 'added': True if movie_dict else False,
- 'movie': movie_dict,
- })
-
- def edit(self):
-
- params = getParams()
- db = get_session()
-
- available_status = fireEvent('status.get', 'available', single = True)
-
- ids = splitString(params.get('id'))
- for movie_id in ids:
-
- m = db.query(Movie).filter_by(id = movie_id).first()
- if not m:
- continue
-
- m.profile_id = params.get('profile_id')
-
- # Remove releases
- for rel in m.releases:
- if rel.status_id is available_status.get('id'):
- db.delete(rel)
- db.commit()
-
- # Default title
- if params.get('default_title'):
- for title in m.library.titles:
- title.default = toUnicode(params.get('default_title', '')).lower() == toUnicode(title.title).lower()
-
- db.commit()
-
- fireEvent('movie.restatus', m.id)
-
- movie_dict = m.to_dict(self.default_dict)
- fireEventAsync('searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id))
-
- #db.close()
- return jsonified({
- 'success': True,
- })
-
- def deleteView(self):
-
- params = getParams()
-
- ids = splitString(params.get('id'))
- for movie_id in ids:
- self.delete(movie_id, delete_from = params.get('delete_from', 'all'))
-
- return jsonified({
- 'success': True,
- })
-
- def delete(self, movie_id, delete_from = None):
-
- db = get_session()
-
- movie = db.query(Movie).filter_by(id = movie_id).first()
- if movie:
- deleted = False
- if delete_from == 'all':
- db.delete(movie)
- db.commit()
- deleted = True
- else:
- done_status = fireEvent('status.get', 'done', single = True)
-
- total_releases = len(movie.releases)
- total_deleted = 0
- new_movie_status = None
- for release in movie.releases:
- if delete_from in ['wanted', 'snatched']:
- if release.status_id != done_status.get('id'):
- db.delete(release)
- total_deleted += 1
- new_movie_status = 'done'
- elif delete_from == 'manage':
- if release.status_id == done_status.get('id'):
- db.delete(release)
- total_deleted += 1
- new_movie_status = 'active'
- db.commit()
-
- if total_releases == total_deleted:
- db.delete(movie)
- db.commit()
- deleted = True
- elif new_movie_status:
- new_status = fireEvent('status.get', new_movie_status, single = True)
- movie.profile_id = None
- movie.status_id = new_status.get('id')
- db.commit()
- else:
- fireEvent('movie.restatus', movie.id, single = True)
-
- if deleted:
- fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict())
-
- #db.close()
- return True
-
- def restatus(self, movie_id):
-
- active_status = fireEvent('status.get', 'active', single = True)
- done_status = fireEvent('status.get', 'done', single = True)
-
- db = get_session()
-
- m = db.query(Movie).filter_by(id = movie_id).first()
- if not m or len(m.library.titles) == 0:
- log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
- return False
-
- log.debug('Changing status for %s', (m.library.titles[0].title))
- if not m.profile:
- m.status_id = done_status.get('id')
- else:
- move_to_wanted = True
-
- for t in m.profile.types:
- for release in m.releases:
- if t.quality.identifier is release.quality.identifier and (release.status_id is done_status.get('id') and t.finish):
- move_to_wanted = False
-
- m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
-
- db.commit()
- #db.close()
-
- return True
-
- def createOnComplete(self, movie_id):
-
- def onComplete():
- db = get_session()
- movie = db.query(Movie).filter_by(id = movie_id).first()
- fireEventAsync('searcher.single', movie.to_dict(self.default_dict), on_complete = self.createNotifyFront(movie_id))
-
- return onComplete
-
-
- def createNotifyFront(self, movie_id):
-
- def notifyFront():
- db = get_session()
- movie = db.query(Movie).filter_by(id = movie_id).first()
- fireEvent('notify.frontend', type = 'movie.update.%s' % movie.id, data = movie.to_dict(self.default_dict))
-
- return notifyFront
diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js
deleted file mode 100644
index 9e76fad3da..0000000000
--- a/couchpotato/core/plugins/movie/static/list.js
+++ /dev/null
@@ -1,534 +0,0 @@
-var MovieList = new Class({
-
- Implements: [Options],
-
- options: {
- navigation: true,
- limit: 50,
- load_more: true,
- menu: [],
- add_new: false
- },
-
- movies: [],
- movies_added: {},
- letters: {},
- filter: null,
-
- initialize: function(options){
- var self = this;
- self.setOptions(options);
-
- self.offset = 0;
- self.filter = self.options.filter || {
- 'startswith': null,
- 'search': null
- }
-
- self.el = new Element('div.movies').adopt(
- self.title = self.options.title ? new Element('h2', {
- 'text': self.options.title
- }) : null,
- self.movie_list = new Element('div'),
- self.load_more = self.options.load_more ? new Element('a.load_more', {
- 'events': {
- 'click': self.loadMore.bind(self)
- }
- }) : null
- );
-
- self.changeView(self.options.view || 'details');
-
- self.getMovies();
-
- App.addEvent('movie.added', self.movieAdded.bind(self))
- App.addEvent('movie.deleted', self.movieDeleted.bind(self))
- },
-
- movieDeleted: function(notification){
- var self = this;
-
- if(self.movies_added[notification.data.id]){
- self.movies.each(function(movie){
- if(movie.get('id') == notification.data.id){
- movie.destroy();
- delete self.movies_added[notification.data.id]
- }
- })
- }
-
- self.checkIfEmpty();
- },
-
- movieAdded: function(notification){
- var self = this;
-
- if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){
- window.scroll(0,0);
- self.createMovie(notification.data, 'top');
-
- self.checkIfEmpty();
- }
- },
-
- create: function(){
- var self = this;
-
- // Create the alphabet nav
- if(self.options.navigation)
- self.createNavigation();
-
- if(self.options.load_more)
- self.scrollspy = new ScrollSpy({
- min: function(){
- var c = self.load_more.getCoordinates()
- return c.top - window.document.getSize().y - 300
- },
- onEnter: self.loadMore.bind(self)
- });
-
- self.created = true;
- },
-
- addMovies: function(movies, total){
- var self = this;
-
- if(!self.created) self.create();
-
- // do scrollspy
- if(movies.length < self.options.limit && self.scrollspy){
- self.load_more.hide();
- self.scrollspy.stop();
- }
-
- Object.each(movies, function(movie){
- self.createMovie(movie);
- });
-
- self.total_movies = total;
- self.setCounter(total);
-
- },
-
- setCounter: function(count){
- var self = this;
-
- if(!self.navigation_counter) return;
-
- self.navigation_counter.set('text', (count || 0));
-
- },
-
- createMovie: function(movie, inject_at){
- var self = this;
-
- // Attach proper actions
- var a = self.options.actions,
- status = Status.get(movie.status_id),
- actions = a ? a[status.identifier.capitalize()] || a.Wanted : {};
-
- var m = new Movie(self, {
- 'actions': actions,
- 'view': self.current_view,
- 'onSelect': self.calculateSelected.bind(self)
- }, movie);
- $(m).inject(self.movie_list, inject_at || 'bottom');
- m.fireEvent('injected');
-
- self.movies.include(m)
- self.movies_added[movie.id] = true;
- },
-
- createNavigation: function(){
- var self = this;
- var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
-
- self.current_view = self.getSavedView();
- self.el.addClass(self.current_view+'_list')
-
- self.navigation = new Element('div.alph_nav').adopt(
- self.navigation_actions = new Element('ul.inlay.actions.reversed'),
- self.navigation_counter = new Element('span.counter[title=Total]'),
- self.navigation_alpha = new Element('ul.numbers', {
- 'events': {
- 'click:relay(li)': function(e, el){
- self.movie_list.empty()
- self.activateLetter(el.get('data-letter'))
- self.getMovies()
- }
- }
- }),
- self.navigation_search_input = new Element('input.inlay', {
- 'placeholder': 'Search',
- 'events': {
- 'keyup': self.search.bind(self),
- 'change': self.search.bind(self)
- }
- }),
- self.navigation_menu = new Block.Menu(self),
- self.mass_edit_form = new Element('div.mass_edit_form').adopt(
- new Element('span.select').adopt(
- self.mass_edit_select = new Element('input[type=checkbox].inlay', {
- 'events': {
- 'change': self.massEditToggleAll.bind(self)
- }
- }),
- self.mass_edit_selected = new Element('span.count', {'text': 0}),
- self.mass_edit_selected_label = new Element('span', {'text': 'selected'})
- ),
- new Element('div.quality').adopt(
- self.mass_edit_quality = new Element('select'),
- new Element('a.button.orange', {
- 'text': 'Change quality',
- 'events': {
- 'click': self.changeQualitySelected.bind(self)
- }
- })
- ),
- new Element('div.delete').adopt(
- new Element('span[text=or]'),
- new Element('a.button.red', {
- 'text': 'Delete',
- 'events': {
- 'click': self.deleteSelected.bind(self)
- }
- })
- ),
- new Element('div.refresh').adopt(
- new Element('span[text=or]'),
- new Element('a.button.green', {
- 'text': 'Refresh',
- 'events': {
- 'click': self.refreshSelected.bind(self)
- }
- })
- )
- )
- ).inject(self.el, 'top');
-
- // Mass edit
- self.mass_edit_select_class = new Form.Check(self.mass_edit_select);
- Quality.getActiveProfiles().each(function(profile){
- new Element('option', {
- 'value': profile.id ? profile.id : profile.data.id,
- 'text': profile.label ? profile.label : profile.data.label
- }).inject(self.mass_edit_quality)
- });
-
- // Actions
- ['mass_edit', 'details', 'list'].each(function(view){
- self.navigation_actions.adopt(
- new Element('li.'+view+(self.current_view == view ? '.active' : '')+'[data-view='+view+']', {
- 'events': {
- 'click': function(e){
- var a = 'active';
- self.navigation_actions.getElements('.'+a).removeClass(a);
- self.changeView(this.get('data-view'));
- this.addClass(a);
- }
- }
- }).adopt(new Element('span'))
- )
- });
-
- // All
- self.letters['all'] = new Element('li.letter_all.available.active', {
- 'text': 'ALL',
- }).inject(self.navigation_alpha);
-
- // Chars
- chars.split('').each(function(c){
- self.letters[c] = new Element('li', {
- 'text': c,
- 'class': 'letter_'+c,
- 'data-letter': c
- }).inject(self.navigation_alpha);
- });
-
- // Get available chars and highlight
- Api.request('movie.available_chars', {
- 'data': Object.merge({
- 'status': self.options.status
- }, self.filter),
- 'onComplete': function(json){
-
- json.chars.split('').each(function(c){
- self.letters[c.capitalize()].addClass('available')
- })
-
- }
- });
-
- // Add menu or hide
- if (self.options.menu.length > 0)
- self.options.menu.each(function(menu_item){
- self.navigation_menu.addLink(menu_item);
- })
- else
- self.navigation_menu.hide()
-
- self.nav_scrollspy = new ScrollSpy({
- min: 10,
- onEnter: function(){
- self.navigation.addClass('float')
- },
- onLeave: function(){
- self.navigation.removeClass('float')
- }
- });
-
- },
-
- calculateSelected: function(){
- var self = this;
-
- var selected = 0,
- movies = self.movies.length;
- self.movies.each(function(movie){
- selected += movie.isSelected() ? 1 : 0
- })
-
- var indeterminate = selected > 0 && selected < movies,
- checked = selected == movies && selected > 0;
-
- self.mass_edit_select.set('indeterminate', indeterminate)
-
- self.mass_edit_select_class[checked ? 'check' : 'uncheck']()
- self.mass_edit_select_class.element[indeterminate ? 'addClass' : 'removeClass']('indeterminate')
-
- self.mass_edit_selected.set('text', selected);
- },
-
- deleteSelected: function(){
- var self = this,
- ids = self.getSelectedMovies(),
- help_msg = self.identifier == 'wanted' ? 'If you do, you won\'t be able to watch them, as they won\'t get downloaded!' : 'Your files will be safe, this will only delete the reference from the CouchPotato manage list';
-
- var qObj = new Question('Are you sure you want to delete '+ids.length+' movie'+ (ids.length != 1 ? 's' : '') +'?', help_msg, [{
- 'text': 'Yes, delete '+(ids.length != 1 ? 'them' : 'it'),
- 'class': 'delete',
- 'events': {
- 'click': function(e){
- (e).preventDefault();
- this.set('text', 'Deleting..')
- Api.request('movie.delete', {
- 'data': {
- 'id': ids.join(','),
- 'delete_from': self.options.identifier
- },
- 'onSuccess': function(){
- qObj.close();
-
- var erase_movies = [];
- self.movies.each(function(movie){
- if (movie.isSelected()){
- $(movie).destroy()
- erase_movies.include(movie)
- }
- });
-
- erase_movies.each(function(movie){
- self.movies.erase(movie);
-
- movie.destroy()
- });
-
- self.calculateSelected();
- }
- });
-
- }
- }
- }, {
- 'text': 'Cancel',
- 'cancel': true
- }]);
-
- },
-
- changeQualitySelected: function(){
- var self = this;
- var ids = self.getSelectedMovies()
-
- Api.request('movie.edit', {
- 'data': {
- 'id': ids.join(','),
- 'profile_id': self.mass_edit_quality.get('value')
- },
- 'onSuccess': self.search.bind(self)
- });
- },
-
- refreshSelected: function(){
- var self = this;
- var ids = self.getSelectedMovies()
-
- Api.request('movie.refresh', {
- 'data': {
- 'id': ids.join(','),
- }
- });
- },
-
- getSelectedMovies: function(){
- var self = this;
-
- var ids = []
- self.movies.each(function(movie){
- if (movie.isSelected())
- ids.include(movie.get('id'))
- });
-
- return ids
- },
-
- massEditToggleAll: function(){
- var self = this;
-
- var select = self.mass_edit_select.get('checked');
-
- self.movies.each(function(movie){
- movie.select(select)
- });
-
- self.calculateSelected()
- },
-
- reset: function(){
- var self = this;
-
- self.movies = []
- self.calculateSelected()
- self.navigation_alpha.getElements('.active').removeClass('active')
- self.offset = 0;
- if(self.scrollspy){
- self.load_more.show();
- self.scrollspy.start();
- }
- },
-
- activateLetter: function(letter){
- var self = this;
-
- self.reset()
-
- self.letters[letter || 'all'].addClass('active');
- self.filter.starts_with = letter;
-
- },
-
- changeView: function(new_view){
- var self = this;
-
- self.el
- .removeClass(self.current_view+'_list')
- .addClass(new_view+'_list')
-
- self.current_view = new_view;
- Cookie.write(self.options.identifier+'_view', new_view, {duration: 1000});
- },
-
- getSavedView: function(){
- var self = this;
- return Cookie.read(self.options.identifier+'_view') || 'thumbs';
- },
-
- search: function(){
- var self = this;
-
- if(self.search_timer) clearTimeout(self.search_timer);
- self.search_timer = (function(){
- var search_value = self.navigation_search_input.get('value');
- if (search_value == self.last_search_value) return
-
- self.reset()
-
- self.activateLetter();
- self.filter.search = search_value;
-
- self.movie_list.empty();
- self.getMovies();
-
- self.last_search_value = search_value;
-
- }).delay(250);
-
- },
-
- update: function(){
- var self = this;
-
- self.reset();
- self.movie_list.empty();
- self.getMovies();
- },
-
- getMovies: function(){
- var self = this;
-
- if(self.scrollspy){
- self.scrollspy.stop();
- self.load_more.set('text', 'loading...');
- }
-
- Api.request(self.options.api_call || 'movie.list', {
- 'data': Object.merge({
- 'status': self.options.status,
- 'limit_offset': self.options.limit + ',' + self.offset
- }, self.filter),
- 'onComplete': function(json){
- self.store(json.movies);
- self.addMovies(json.movies, json.total);
- if(self.scrollspy) {
- self.load_more.set('text', 'load more movies');
- self.scrollspy.start();
- }
-
- self.checkIfEmpty()
- }
- });
- },
-
- loadMore: function(){
- var self = this;
- if(self.offset >= self.options.limit)
- self.getMovies()
- },
-
- store: function(movies){
- var self = this;
-
- self.offset += movies.length;
-
- },
-
- checkIfEmpty: function(){
- var self = this;
-
- var is_empty = self.movies.length == 0 && self.total_movies == 0;
-
- if(self.title)
- self.title[is_empty ? 'hide' : 'show']()
-
- if(is_empty && self.options.on_empty_element){
- self.el.grab(self.options.on_empty_element);
-
- if(self.navigation)
- self.navigation.hide();
-
- self.empty_element = self.options.on_empty_element;
- }
- else if(self.empty_element){
- self.empty_element.destroy();
-
- if(self.navigation)
- self.navigation.show();
- }
-
- },
-
- toElement: function(){
- return this.el;
- }
-
-});
\ No newline at end of file
diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css
deleted file mode 100644
index 9d67e62826..0000000000
--- a/couchpotato/core/plugins/movie/static/movie.css
+++ /dev/null
@@ -1,726 +0,0 @@
-.movies {
- padding: 60px 0 20px;
-}
-
- .movies h2 {
- margin-bottom: 20px;
- }
-
- .movies.thumbs_list {
- padding: 20px 0 20px;
- }
-
- .home .movies {
- padding-top: 6px;
- }
-
- .movies.mass_edit_list {
- padding-top: 90px;
- }
-
- .movies .movie {
- position: relative;
- border-radius: 4px;
- margin: 10px 0;
- overflow: hidden;
- width: 100%;
- height: 180px;
- transition: all 0.2s linear;
- }
-
- .movies.list_list .movie:not(.details_view),
- .movies.mass_edit_list .movie {
- height: 32px;
- }
-
- .movies.thumbs_list .movie {
- width: 153px;
- height: 230px;
- display: inline-block;
- margin: 0 8px 0 0;
- }
- .movies.thumbs_list .movie:nth-child(6n+6) {
- margin: 0;
- }
-
- .movies .movie .mask {
- position: absolute;
- top: 0;
- left: 0;
- height: 100%;
- width: 100%;
- }
-
- .movies.list_list .movie:not(.details_view),
- .movies.mass_edit_list .movie {
- margin: 1px 0;
- border-radius: 0;
- background: no-repeat;
- box-shadow: none;
- border-bottom: 1px solid rgba(255,255,255,0.05);
- }
-
- .movies.list_list .movie:hover:not(.details_view),
- .movies.mass_edit_list .movie {
- background: rgba(255,255,255,0.03);
- }
-
- .movies .data {
- padding: 20px;
- height: 100%;
- width: 840px;
- position: absolute;
- right: 0;
- border-radius: 0;
- transition: all .6s cubic-bezier(0.9,0,0.1,1);
- }
- .movies.list_list .movie:not(.details_view) .data,
- .movies.mass_edit_list .movie .data {
- height: 30px;
- padding: 3px 0 3px 10px;
- width: 938px;
- box-shadow: none;
- border: 0;
- background: none;
- }
-
- .movies.thumbs_list .data {
- left: 0;
- width: 100%;
- padding: 10px;
- height: 100%;
- background: none;
- transition: none;
- }
-
- .movies.thumbs_list .movie.no_thumbnail .data { background-image: linear-gradient(-30deg, rgba(255, 0, 85, .2) 0,rgba(125, 185, 235, .2) 100%);
- }
- .movies.thumbs_list .movie.no_thumbnail:nth-child(2n+6) .data { background-image: linear-gradient(-20deg, rgba(125, 0, 215, .2) 0, rgba(4, 55, 5, .7) 100%); }
- .movies.thumbs_list .movie.no_thumbnail:nth-child(3n+6) .data { background-image: linear-gradient(-30deg, rgba(155, 0, 85, .2) 0,rgba(25, 185, 235, .7) 100%); }
- .movies.thumbs_list .movie.no_thumbnail:nth-child(4n+6) .data { background-image: linear-gradient(-30deg, rgba(115, 5, 235, .2) 0, rgba(55, 180, 5, .7) 100%); }
- .movies.thumbs_list .movie.no_thumbnail:nth-child(5n+6) .data { background-image: linear-gradient(-30deg, rgba(35, 15, 215, .2) 0, rgba(135, 215, 115, .7) 100%); }
- .movies.thumbs_list .movie.no_thumbnail:nth-child(6n+6) .data { background-image: linear-gradient(-30deg, rgba(35, 15, 215, .2) 0, rgba(135, 15, 115, .7) 100%); }
-
- .movies.thumbs_list .movie:hover .data {
- background: rgba(0,0,0,0.9);
- }
-
- .movies .data.hide_right {
- right: -100%;
- }
-
- .movies .movie .check {
- display: none;
- }
-
- .movies.mass_edit_list .movie .check {
- position: absolute;
- left: 0;
- top: 0;
- display: block;
- margin: 7px 0 0 5px;
- }
-
- .movies .poster {
- position: absolute;
- left: 0;
- width: 120px;
- line-height: 0;
- overflow: hidden;
- height: 100%;
- border-radius: 4px 0 0 4px;
- transition: all .6s cubic-bezier(0.9,0,0.1,1);
-
- }
- .movies.list_list .movie:not(.details_view) .poster,
- .movies.mass_edit_list .poster {
- width: 20px;
- height: 30px;
- border-radius: 1px 0 0 1px;
- }
- .movies.mass_edit_list .poster {
- display: none;
- }
-
- .movies.thumbs_list .poster {
- width: 100%;
- height: 100%;
- }
-
- .movies .poster img,
- .options .poster img {
- width: 101%;
- height: 101%;
- }
-
- .movies .info {
- position: relative;
- height: 100%;
- }
-
- .movies .info .title {
- display: inline;
- position: absolute;
- font-size: 28px;
- font-weight: bold;
- margin-bottom: 10px;
- left: 0;
- top: 0;
- width: 90%;
- transition: all 0.2s linear;
- }
- .movies.list_list .movie:not(.details_view) .info .title,
- .movies.mass_edit_list .info .title {
- font-size: 16px;
- font-weight: normal;
- text-overflow: ellipsis;
- width: auto;
- overflow: hidden;
-
- }
-
- .movies.thumbs_list .movie:not(.no_thumbnail) .info {
- display: none;
- }
- .movies.thumbs_list .movie:hover .info {
- display: block;
- }
-
- .movies.thumbs_list .info .title {
- font-size: 21px;
- text-shadow: 0 0 10px #000;
- word-wrap: break-word;
- }
-
- .movies .info .year {
- position: absolute;
- font-size: 30px;
- margin-bottom: 10px;
- color: #bbb;
- width: 10%;
- right: 0;
- top: 0;
- text-align: right;
- transition: all 0.2s linear;
- }
- .movies.list_list .movie:not(.details_view) .info .year,
- .movies.mass_edit_list .info .year {
- font-size: 16px;
- width: 6%;
- right: 10px;
- }
-
- .movies.thumbs_list .info .year {
- font-size: 23px;
- margin: 0;
- bottom: 0;
- left: 0;
- top: auto;
- right: auto;
- color: #FFF;
- text-shadow: none;
- text-shadow: 0 0 6px #000;
- }
-
- .movies .info .description {
- position: absolute;
- top: 30px;
- clear: both;
- height: 80px;
- overflow: hidden;
- }
- .movies .data:hover .description {
- overflow: auto;
- }
- .movies.list_list .movie:not(.details_view) .info .description,
- .movies.mass_edit_list .info .description,
- .movies.thumbs_list .info .description {
- display: none;
- }
-
- .movies .data .quality {
- position: absolute;
- bottom: 0;
- display: block;
- min-height: 20px;
- vertical-align: mid;
- }
-
- .movies .status_suggest .data .quality,
- .movies.thumbs_list .data .quality {
- display: none;
- }
-
- .movies .data .quality span {
- padding: 2px 3px;
- font-weight: bold;
- opacity: 0.5;
- font-size: 10px;
- height: 16px;
- line-height: 12px;
- vertical-align: middle;
- display: inline-block;
- text-transform: uppercase;
- text-shadow: none;
- font-weight: normal;
- margin: 0 2px;
- border-radius: 2px;
- background-color: rgba(255,255,255,0.1);
- }
- .movies.list_list .data .quality,
- .movies.mass_edit_list .data .quality {
- text-align: right;
- right: 0;
- margin-right: 50px;
- z-index: 2;
- }
-
- .movies .data .quality .available,
- .movies .data .quality .snatched {
- opacity: 1;
- box-shadow: 1px 1px 0 rgba(0,0,0,0.2);
- cursor: pointer;
- }
-
- .movies .data .quality .available { background-color: #578bc3; }
- .movies .data .quality .snatched { background-color: #369545; }
- .movies .data .quality .done {
- background-color: #369545;
- opacity: 1;
- }
- .movies .data .quality .finish {
- background-image: url('../images/sprite.png');
- background-repeat: no-repeat;
- background-position: 0 2px;
- padding-left: 14px;
- background-size: 14px
- }
-
- .movies .data .actions {
- position: absolute;
- bottom: 20px;
- right: 20px;
- line-height: 0;
- margin-top: -25px;
- }
- .movies .data:hover .action { opacity: 0.6; }
- .movies .data:hover .action:hover { opacity: 1; }
- .movies.mass_edit_list .data .actions {
- display: none;
- }
-
- .movies .data .action {
- background-repeat: no-repeat;
- background-position: center;
- display: inline-block;
- width: 26px;
- height: 26px;
- padding: 3px;
- opacity: 0;
- }
-
- .movies.list_list .movie:not(.details_view) .data:hover .actions,
- .movies.mass_edit_list .data:hover .actions {
- margin: 0;
- background: #4e5969;
- top: 2px;
- bottom: 2px;
- right: 5px;
- z-index: 3;
- }
-
- .movies .delete_container {
- clear: both;
- text-align: center;
- font-size: 20px;
- position: absolute;
- padding: 70px 0 0;
- width: 100%;
- }
- .movies .delete_container .cancel {
- }
- .movies .delete_container .or {
- padding: 10px;
- }
- .movies .delete_container .delete {
- background-color: #ff321c;
- font-weight: normal;
- }
- .movies .delete_container .delete:hover {
- color: #fff;
- background-color: #d32917;
- }
-
- .movies .options {
- position: absolute;
- margin-left: 120px;
- width: 840px;
- }
-
- .movies .options .form {
- margin: 70px 20px 0;
- float: left;
- font-size: 20px;
- }
-
- .movies .options .form select {
- margin-right: 20px;
- }
-
- .movies .options .table {
- height: 180px;
- overflow: auto;
- }
- .movies .options .table .item {
- border-bottom: 1px solid rgba(255,255,255,0.1);
- }
- .movies .options .table .item.ignored span {
- text-decoration: line-through;
- color: rgba(255,255,255,0.4);
- text-shadow: none;
- }
- .movies .options .table .item.ignored .delete {
- background-image: url('../images/icon.undo.png');
- }
-
- .movies .options .table .item:last-child { border: 0; }
- .movies .options .table .item:nth-child(even) {
- background: rgba(255,255,255,0.05);
- }
- .movies .options .table .item:not(.head):hover {
- background: rgba(255,255,255,0.03);
- }
-
- .movies .options .table .item > * {
- display: inline-block;
- padding: 0 5px;
- width: 60px;
- min-height: 24px;
- white-space: nowrap;
- text-overflow: ellipsis;
- text-align: center;
- vertical-align: top;
- border-left: 1px solid rgba(255, 255, 255, 0.1);
- }
- .movies .options .table .item > *:first-child {
- border: 0;
- }
- .movies .options .table .provider {
- width: 120px;
- text-overflow: ellipsis;
- overflow: hidden;
- }
- .movies .options .table .name {
- width: 350px;
- overflow: hidden;
- text-align: left;
- padding: 0 10px;
- }
- .movies .options .table.files .name { width: 590px; }
- .movies .options .table .type { width: 130px; }
- .movies .options .table .is_available { width: 90px; }
- .movies .options .table .age,
- .movies .options .table .size { width: 40px; }
-
- .movies .options .table a {
- width: 30px !important;
- height: 20px;
- opacity: 0.8;
- }
- .movies .options .table a:hover {
- opacity: 1;
- }
-
- .movies .options .table .head > * {
- font-weight: bold;
- font-size: 14px;
- padding-top: 4px;
- padding-bottom: 4px;
- height: auto;
- }
-
- .movies .movie .trailer_container {
- width: 100%;
- background: #000;
- text-align: center;
- transition: all .6s cubic-bezier(0.9,0,0.1,1);
- overflow: hidden;
- }
- .movies .movie .trailer_container.hide {
- height: 0 !important;
- }
-
- .movies .movie .hide_trailer {
- position: absolute;
- top: 0;
- left: 50%;
- margin-left: -50px;
- width: 100px;
- text-align: center;
- padding: 3px 10px;
- background: #4e5969;
- border-radius: 0 0 2px 2px;
- transition: all .2s cubic-bezier(0.9,0,0.1,1) .2s;
- }
- .movies .movie .hide_trailer.hide {
- top: -30px;
- }
-
- .movies .movie .try_container {
- padding: 5px 10px;
- text-align: center;
- }
-
- .movies .movie .try_container a {
- margin: 0 5px;
- padding: 2px 5px;
- }
-
- .movies .movie .releases .next_release {
- border-left: 6px solid #2aa300;
- }
-
- .movies .movie .releases .next_release > :first-child {
- margin-left: -6px;
- }
-
- .movies .movie .releases .last_release {
- border-left: 6px solid #ffa200;
- }
-
- .movies .movie .releases .last_release > :first-child {
- margin-left: -6px;
- }
-
- .movies .load_more {
- display: block;
- padding: 10px;
- text-align: center;
- font-size: 20px;
- }
- .movies .load_more.loading {
- opacity: .5;
- }
-
-.movies .alph_nav {
- transition: box-shadow .4s linear;
- position: fixed;
- z-index: 2;
- top: 0;
- padding: 100px 60px 7px;
- width: 1080px;
- margin: 0 -60px;
- box-shadow: 0 20px 20px -22px rgba(0,0,0,0.1);
- background: #4e5969;
-}
-
- .movies .alph_nav.float {
- box-shadow: 0 30px 30px -32px rgba(0,0,0,0.5);
- border-radius: 0;
- }
-
-.movies .alph_nav ul.numbers,
-.movies .alph_nav .counter,
-.movies .alph_nav ul.actions {
- list-style: none;
- padding: 0 0 1px;
- margin: 0;
- float: left;
- user-select: none;
-}
-
- .movies .alph_nav .counter {
- width: 60px;
- text-align: center;
- }
-
- .movies .alph_nav .numbers li,
- .movies .alph_nav .actions li {
- display: inline-block;
- vertical-align: top;
- width: 20px;
- height: 24px;
- line-height: 26px;
- text-align: center;
- cursor: pointer;
- color: rgba(255,255,255,0.2);
- border: 1px solid transparent;
- transition: all 0.1s ease-in-out;
- text-shadow: none;
- }
- .movies .alph_nav .numbers li:first-child {
- width: 43px;
- }
- .movies .alph_nav li.available {
- color: rgba(255,255,255,0.8);
- font-weight: bolder;
-
- }
- .movies .alph_nav li.active.available, .movies .alph_nav li.available:hover {
- color: #fff;
- font-size: 20px;
- line-height: 20px;
- }
-
- .movies .alph_nav input {
- padding: 6px 5px;
- margin: 0 0 0 6px;
- float: left;
- width: 155px;
- height: 25px;
- }
-
- .movies .alph_nav .actions {
- margin: 0 6px 0 0;
- -moz-user-select: none;
- }
- .movies .alph_nav .actions li {
- border-radius: 1px;
- width: auto;
- }
- .movies .alph_nav .actions li.active {
- background: none;
- border: 1px solid transparent;
- box-shadow: none;
- }
- .movies .alph_nav .actions li span {
- display: block;
- background: url('../images/sprite.png') no-repeat;
- width: 25px;
- height: 100%;
- }
-
- .movies .alph_nav .actions li.mass_edit span {
- background-position: 3px 3px;
- }
-
- .movies .alph_nav .actions li.list span {
- background-position: 3px -95px;
- }
-
- .movies .alph_nav .actions li.details span {
- background-position: 3px -74px;
- }
-
- .movies .alph_nav .actions li:first-child {
- border-radius: 3px 0 0 3px;
- }
- .movies .alph_nav .actions li:last-child {
- border-radius: 0 3px 3px 0;
- }
-
- .movies .alph_nav .mass_edit_form {
- clear: both;
- text-align: center;
- display: none;
- }
- .movies.mass_edit_list .mass_edit_form {
- display: block;
- }
- .movies.mass_edit_list .mass_edit_form .select {
- float: left;
- margin: 5px 0 0 5px;
- font-size: 14px;
- }
- .movies.mass_edit_list .mass_edit_form .select span {
- vertical-align: middle;
- opacity: 0.7;
- }
- .movies.mass_edit_list .mass_edit_form .select .count {
- font-weight: bold;
- margin: 0 3px 0 10px;
- }
-
- .movies .alph_nav .mass_edit_form .quality {
- float: left;
- padding: 8px 0 0;
- margin: 0 0 0 16px;
- }
- .movies .alph_nav .mass_edit_form .quality select {
- width: 120px;
- margin-right: 5px;
- }
- .movies .alph_nav .mass_edit_form .button {
- padding: 3px 7px;
- }
-
- .movies .alph_nav .mass_edit_form .refresh,
- .movies .alph_nav .mass_edit_form .delete {
- float: left;
- padding: 8px 0 0 8px;
- }
-
- .movies .alph_nav .mass_edit_form .refresh span,
- .movies .alph_nav .mass_edit_form .delete span {
- margin: 0 10px 0 0;
- }
-
- .movies .alph_nav .more_menu {
- margin-left: 48px;
- }
-
- .movies .alph_nav .more_menu > a {
- background-position: center -158px;
- }
-
-.movies .empty_wanted {
- background-image: url('../images/emptylist.png');
- height: 750px;
- width: 800px;
- padding-top: 260px;
- margin-top: -50px;
-}
-
-.movies .empty_manage {
- text-align: center;
- font-size: 25px;
- line-height: 150%;
-}
-
- .movies .empty_manage .after_manage {
- margin-top: 30px;
- font-size: 16px;
- }
-
- .movies .progress {
- border-radius: 2px;
- padding: 10px;
- margin: 5px 0;
- text-align: left;
- }
-
- .movies .progress > div {
- padding: 5px 10px;
- font-size: 12px;
- line-height: 12px;
- text-align: left;
- display: inline-block;
- width: 49%;
- background: rgba(255, 255, 255, 0.05);
- margin: 2px 0.5%;
- border-radius: 3px;
- }
-
- .movies .progress > div .folder {
- display: inline-block;
- padding: 5px 20px 5px 0;
- white-space: nowrap;
- text-overflow: ellipsis;
- overflow: hidden;
- width: 85%;
- direction: rtl;
- vertical-align: middle;
- }
-
- .movies .progress > div .percentage {
- font-weight: bold;
- display: inline-block;
- text-transform: uppercase;
- text-shadow: none;
- font-weight: normal;
- font-size: 20px;
- border-left: 1px solid rgba(255, 255, 255, .2);
- width: 15%;
- text-align: right;
- vertical-align: middle;
- }
diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js
deleted file mode 100644
index 283bb41415..0000000000
--- a/couchpotato/core/plugins/movie/static/movie.js
+++ /dev/null
@@ -1,684 +0,0 @@
-var Movie = new Class({
-
- Extends: BlockBase,
-
- action: {},
-
- initialize: function(list, options, data){
- var self = this;
-
- self.data = data;
- self.view = options.view || 'details';
- self.list = list;
-
- self.el = new Element('div.movie.inlay');
-
- self.profile = Quality.getProfile(data.profile_id) || {};
- self.parent(self, options);
-
- self.addEvents();
- },
-
- addEvents: function(){
- var self = this;
-
- App.addEvent('movie.update.'+self.data.id, self.update.bind(self));
-
- ['movie.busy', 'searcher.started'].each(function(listener){
- App.addEvent(listener+'.'+self.data.id, function(notification){
- if(notification.data)
- self.busy(true)
- });
- })
-
- App.addEvent('searcher.ended.'+self.data.id, function(notification){
- if(notification.data)
- self.busy(false)
- });
- },
-
- destroy: function(){
- var self = this;
-
- self.el.destroy();
- delete self.list.movies_added[self.get('id')];
- self.list.movies.erase(self)
-
- self.list.checkIfEmpty();
-
- // Remove events
- App.removeEvents('movie.update.'+self.data.id);
- ['movie.busy', 'searcher.started'].each(function(listener){
- App.removeEvents(listener+'.'+self.data.id);
- })
- },
-
- busy: function(set_busy){
- var self = this;
-
- if(!set_busy){
- if(self.spinner){
- self.mask.fade('out');
- setTimeout(function(){
- if(self.mask)
- self.mask.destroy();
- if(self.spinner)
- self.spinner.el.destroy();
- self.spinner = null;
- self.mask = null;
- }, 400);
- }
- }
- else if(!self.spinner) {
- self.createMask();
- self.spinner = createSpinner(self.mask);
- self.mask.fade('in');
- }
- },
-
- createMask: function(){
- var self = this;
- self.mask = new Element('div.mask', {
- 'styles': {
- 'z-index': '1'
- }
- }).inject(self.el, 'top').fade('hide');
- },
-
- positionMask: function(){
- var self = this,
- s = self.el.getSize()
-
- return self.mask.setStyles({
- 'width': s.x,
- 'height': s.y
- }).position({
- 'relativeTo': self.el
- })
- },
-
- update: function(notification){
- var self = this;
-
- self.data = notification.data;
- self.el.empty();
-
- self.profile = Quality.getProfile(self.data.profile_id) || {};
- self.create();
-
- self.busy(false);
- },
-
- create: function(){
- var self = this;
-
- var s = Status.get(self.get('status_id'));
- self.el.addClass('status_'+s.identifier);
-
- self.el.adopt(
- self.select_checkbox = new Element('input[type=checkbox].inlay', {
- 'events': {
- 'change': function(){
- self.fireEvent('select')
- }
- }
- }),
- self.thumbnail = File.Select.single('poster', self.data.library.files),
- self.data_container = new Element('div.data.inlay.light').adopt(
- self.info_container = new Element('div.info').adopt(
- self.title = new Element('div.title', {
- 'text': self.getTitle() || 'n/a'
- }),
- self.year = new Element('div.year', {
- 'text': self.data.library.year || 'n/a'
- }),
- self.rating = new Element('div.rating.icon', {
- 'text': self.data.library.rating
- }),
- self.description = new Element('div.description', {
- 'text': self.data.library.plot
- }),
- self.quality = new Element('div.quality', {
- 'events': {
- 'click': function(e){
- var releases = self.el.getElement('.actions .releases');
- if(releases)
- releases.fireEvent('click', [e])
- }
- }
- })
- ),
- self.actions = new Element('div.actions')
- )
- );
-
- if(self.thumbnail.empty)
- self.el.addClass('no_thumbnail');
-
- //self.changeView(self.view);
- self.select_checkbox_class = new Form.Check(self.select_checkbox);
-
- // Add profile
- if(self.profile.data)
- self.profile.getTypes().each(function(type){
-
- var q = self.addQuality(type.quality_id || type.get('quality_id'));
- if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){
- q.addClass('finish');
- q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.')
- }
-
- });
-
- // Add releases
- self.data.releases.each(function(release){
-
- var q = self.quality.getElement('.q_id'+ release.quality_id),
- status = Status.get(release.status_id);
-
- if(!q && (status.identifier == 'snatched' || status.identifier == 'done'))
- var q = self.addQuality(release.quality_id)
-
- if (status && q && !q.hasClass(status.identifier)){
- q.addClass(status.identifier);
- q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status.label)
- }
-
- });
-
- Object.each(self.options.actions, function(action, key){
- self.action[key.toLowerCase()] = action = new self.options.actions[key](self)
- if(action.el)
- self.actions.adopt(action)
- });
-
- if(!self.data.library.rating)
- self.rating.hide();
-
- },
-
- addQuality: function(quality_id){
- var self = this;
-
- var q = Quality.getQuality(quality_id);
- return new Element('span', {
- 'text': q.label,
- 'class': 'q_'+q.identifier + ' q_id' + q.id,
- 'title': ''
- }).inject(self.quality);
-
- },
-
- getTitle: function(){
- var self = this;
-
- var titles = self.data.library.titles;
-
- var title = titles.filter(function(title){
- return title['default']
- }).pop()
-
- if(title)
- return self.getUnprefixedTitle(title.title)
- else if(titles.length > 0)
- return self.getUnprefixedTitle(titles[0].title)
-
- return 'Unknown movie'
- },
-
- getUnprefixedTitle: function(t){
- if(t.substr(0, 4).toLowerCase() == 'the ')
- t = t.substr(4) + ', The';
- return t;
- },
-
- slide: function(direction, el){
- var self = this;
-
- if(direction == 'in'){
- self.temp_view = self.view;
- self.changeView('details')
-
- self.el.addEvent('outerClick', function(){
- self.removeView()
- self.slide('out')
- })
- el.show();
- self.data_container.addClass('hide_right');
- }
- else {
- self.el.removeEvents('outerClick')
-
- setTimeout(function(){
- self.el.getElements('> :not(.data):not(.poster):not(.movie_container)').hide();
- }, 600);
-
- self.data_container.removeClass('hide_right');
- }
- },
-
- changeView: function(new_view){
- var self = this;
-
- self.el
- .removeClass(self.view+'_view')
- .addClass(new_view+'_view')
-
- self.view = new_view;
- },
-
- removeView: function(){
- var self = this;
-
- self.el.removeClass(self.view+'_view')
- },
-
- get: function(attr){
- return this.data[attr] || this.data.library[attr]
- },
-
- select: function(bool){
- var self = this;
- self.select_checkbox_class[bool ? 'check' : 'uncheck']()
- },
-
- isSelected: function(){
- return this.select_checkbox.get('checked');
- },
-
- toElement: function(){
- return this.el;
- }
-
-});
-
-var MovieAction = new Class({
-
- class_name: 'action icon',
-
- initialize: function(movie){
- var self = this;
- self.movie = movie;
-
- self.create();
- if(self.el)
- self.el.addClass(self.class_name)
- },
-
- create: function(){},
-
- disable: function(){
- this.el.addClass('disable')
- },
-
- enable: function(){
- this.el.removeClass('disable')
- },
-
- createMask: function(){
- var self = this;
- self.mask = new Element('div.mask', {
- 'styles': {
- 'z-index': '1'
- }
- }).inject(self.movie, 'top').fade('hide');
- //self.positionMask();
- },
-
- positionMask: function(){
- var self = this,
- movie = $(self.movie),
- s = movie.getSize()
-
- return;
-
- return self.mask.setStyles({
- 'width': s.x,
- 'height': s.y
- }).position({
- 'relativeTo': movie
- })
- },
-
- toElement: function(){
- return this.el || null
- }
-
-});
-
-var IMDBAction = new Class({
-
- Extends: MovieAction,
- id: null,
-
- create: function(){
- var self = this;
-
- self.id = self.movie.get('identifier');
-
- self.el = new Element('a.imdb', {
- 'title': 'Go to the IMDB page of ' + self.movie.getTitle(),
- 'href': 'http://www.imdb.com/title/'+self.id+'/',
- 'target': '_blank'
- });
-
- if(!self.id) self.disable();
- }
-
-});
-
-var ReleaseAction = new Class({
-
- Extends: MovieAction,
-
- create: function(){
- var self = this;
-
- self.el = new Element('a.releases.icon.download', {
- 'title': 'Show the releases that are available for ' + self.movie.getTitle(),
- 'events': {
- 'click': self.show.bind(self)
- }
- });
-
- if(self.movie.data.releases.length == 0){
- self.el.hide()
- }
- else {
-
- var buttons_done = false;
-
- self.movie.data.releases.sortBy('-info.score').each(function(release){
- if(buttons_done) return;
-
- var status = Status.get(release.status_id);
-
- if((self.next_release && (status.identifier == 'ignored' || status.identifier == 'failed')) || (!self.next_release && status.identifier == 'available')){
- self.hide_on_click = false;
- self.show();
- buttons_done = true;
- }
-
- });
-
- }
-
- },
-
- show: function(e){
- var self = this;
- if(e)
- (e).preventDefault();
-
- if(!self.options_container){
- self.options_container = new Element('div.options').adopt(
- self.release_container = new Element('div.releases.table').adopt(
- self.trynext_container = new Element('div.buttons.try_container')
- )
- ).inject(self.movie, 'top');
-
- // Header
- new Element('div.item.head').adopt(
- new Element('span.name', {'text': 'Release name'}),
- new Element('span.status', {'text': 'Status'}),
- new Element('span.quality', {'text': 'Quality'}),
- new Element('span.size', {'text': 'Size'}),
- new Element('span.age', {'text': 'Age'}),
- new Element('span.score', {'text': 'Score'}),
- new Element('span.provider', {'text': 'Provider'})
- ).inject(self.release_container)
-
- self.movie.data.releases.sortBy('-info.score').each(function(release){
-
- var status = Status.get(release.status_id),
- quality = Quality.getProfile(release.quality_id) || {},
- info = release.info,
- provider = self.get(release, 'provider') + (release.info['provider_extra'] ? self.get(release, 'provider_extra') : '');
- release.status = status;
-
- // Create release
- new Element('div', {
- 'class': 'item '+status.identifier,
- 'id': 'release_'+release.id
- }).adopt(
- new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}),
- new Element('span.status', {'text': status.identifier, 'class': 'release_status '+status.identifier}),
- new Element('span.quality', {'text': quality.get('label') || 'n/a'}),
- new Element('span.size', {'text': release.info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}),
- new Element('span.age', {'text': self.get(release, 'age')}),
- new Element('span.score', {'text': self.get(release, 'score')}),
- new Element('span.provider', { 'text': provider, 'title': provider }),
- release.info['detail_url'] ? new Element('a.info.icon', {
- 'href': release.info['detail_url'],
- 'target': '_blank'
- }) : null,
- new Element('a.download.icon', {
- 'events': {
- 'click': function(e){
- (e).preventDefault();
- if(!this.hasClass('completed'))
- self.download(release);
- }
- }
- }),
- new Element('a.delete.icon', {
- 'events': {
- 'click': function(e){
- (e).preventDefault();
- self.ignore(release);
- this.getParent('.item').toggleClass('ignored')
- }
- }
- })
- ).inject(self.release_container)
-
- if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){
- if(!self.last_release || (self.last_release && self.last_release.status.identifier != 'snatched' && status.identifier == 'snatched'))
- self.last_release = release;
- }
- else if(!self.next_release && status.identifier == 'available'){
- self.next_release = release;
- }
- });
-
- if(self.last_release){
- self.release_container.getElement('#release_'+self.last_release.id).addClass('last_release');
- }
-
- if(self.next_release){
- self.release_container.getElement('#release_'+self.next_release.id).addClass('next_release');
- }
-
- if(self.next_release || self.last_release){
-
- self.trynext_container.adopt(
- new Element('span.or', {
- 'text': 'This movie is snatched, if anything went wrong, download'
- }),
- self.last_release ? new Element('a.button.orange', {
- 'text': 'the same release again',
- 'events': {
- 'click': self.trySameRelease.bind(self)
- }
- }) : null,
- self.next_release && self.last_release ? new Element('span.or', {
- 'text': ','
- }) : null,
- self.next_release ? [new Element('a.button.green', {
- 'text': self.last_release ? 'another release' : 'the best release',
- 'events': {
- 'click': self.tryNextRelease.bind(self)
- }
- }),
- new Element('span.or', {
- 'text': 'or pick one below'
- })] : null
- )
- }
-
- }
-
- self.movie.slide('in', self.options_container);
- },
-
- get: function(release, type){
- return release.info[type] || 'n/a'
- },
-
- download: function(release){
- var self = this;
-
- var release_el = self.release_container.getElement('#release_'+release.id),
- icon = release_el.getElement('.download.icon');
-
- icon.addClass('spinner');
-
- Api.request('release.download', {
- 'data': {
- 'id': release.id
- },
- 'onComplete': function(json){
- icon.removeClass('spinner')
- if(json.success)
- icon.addClass('completed');
- else
- icon.addClass('attention').set('title', 'Something went wrong when downloading, please check logs.');
- }
- });
- },
-
- ignore: function(release){
- var self = this;
-
- Api.request('release.ignore', {
- 'data': {
- 'id': release.id
- }
- })
-
- },
-
- tryNextRelease: function(movie_id){
- var self = this;
-
- if(self.last_release)
- self.ignore(self.last_release);
-
- if(self.next_release)
- self.download(self.next_release);
-
- },
-
- trySameRelease: function(movie_id){
- var self = this;
-
- if(self.last_release)
- self.download(self.last_release);
-
- }
-
-});
-
-var TrailerAction = new Class({
-
- Extends: MovieAction,
- id: null,
-
- create: function(){
- var self = this;
-
- self.el = new Element('a.trailer', {
- 'title': 'Watch the trailer of ' + self.movie.getTitle(),
- 'events': {
- 'click': self.watch.bind(self)
- }
- });
-
- },
-
- watch: function(offset){
- var self = this;
-
- var data_url = 'http://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18'
- var url = data_url.substitute({
- 'title': encodeURI(self.movie.getTitle()),
- 'year': self.movie.get('year'),
- 'offset': offset || 1
- }),
- size = $(self.movie).getSize(),
- height = (size.x/16)*9,
- id = 'trailer-'+randomString();
-
- self.player_container = new Element('div[id='+id+']');
- self.container = new Element('div.hide.trailer_container')
- .adopt(self.player_container)
- .inject($(self.movie), 'top');
-
- self.container.setStyle('height', 0);
- self.container.removeClass('hide');
-
- self.close_button = new Element('a.hide.hide_trailer', {
- 'text': 'Hide trailer',
- 'events': {
- 'click': self.stop.bind(self)
- }
- }).inject(self.movie);
-
- self.container.setStyle('height', height);
- $(self.movie).setStyle('height', height);
-
- new Request.JSONP({
- 'url': url,
- 'onComplete': function(json){
- var video_url = json.feed.entry[0].id.$t.split('/'),
- video_id = video_url[video_url.length-1];
-
- self.player = new YT.Player(id, {
- 'height': height,
- 'width': size.x,
- 'videoId': video_id,
- 'playerVars': {
- 'autoplay': 1,
- 'showsearch': 0,
- 'wmode': 'transparent',
- 'iv_load_policy': 3
- }
- });
-
- self.close_button.removeClass('hide');
-
- var quality_set = false;
- var change_quality = function(state){
- if(!quality_set && (state.data == 1 || state.data || 2)){
- try {
- self.player.setPlaybackQuality('hd720');
- quality_set = true;
- }
- catch(e){
-
- }
- }
- }
- self.player.addEventListener('onStateChange', change_quality);
-
- }
- }).send()
-
- },
-
- stop: function(){
- var self = this;
-
- self.player.stopVideo();
- self.container.addClass('hide');
- self.close_button.addClass('hide');
- $(self.movie).setStyle('height', null);
-
- setTimeout(function(){
- self.container.destroy()
- self.close_button.destroy();
- }, 1800)
- }
-
-
-});
\ No newline at end of file
diff --git a/couchpotato/core/plugins/movie/static/search.css b/couchpotato/core/plugins/movie/static/search.css
deleted file mode 100644
index 391e34763d..0000000000
--- a/couchpotato/core/plugins/movie/static/search.css
+++ /dev/null
@@ -1,212 +0,0 @@
-.search_form {
- display: inline-block;
- vertical-align: middle;
- width: 25%;
-}
-
- .search_form input {
- padding: 4px 20px 4px 4px;
- margin: 0;
- font-size: 14px;
- width: 100%;
- height: 24px;
- }
- .search_form input:focus {
- padding-right: 83px;
- }
-
- .search_form .input .enter {
- background: #369545 url('../images/sprite.png') right -188px no-repeat;
- padding: 0 20px 0 4px;
- border-radius: 2px;
- text-transform: uppercase;
- font-size: 10px;
- margin-left: -78px;
- display: inline-block;
- opacity: 0;
- position: relative;
- top: -2px;
- cursor: pointer;
- vertical-align: middle;
- visibility: hidden;
- }
- .search_form.focused .input .enter {
- visibility: visible;
- }
- .search_form.focused.filled .input .enter {
- opacity: 1;
- }
-
- .search_form .input a {
- width: 17px;
- height: 20px;
- display: inline-block;
- margin: -2px 0 0 2px;
- top: 4px;
- right: 5px;
- background: url('../images/sprite.png') left -37px no-repeat;
- cursor: pointer;
- opacity: 0;
- transition: all 0.2s ease-in-out;
- vertical-align: middle;
- }
-
- .search_form.filled .input a {
- opacity: 1;
- }
-
- .search_form .results_container {
- position: absolute;
- background: #5c697b;
- margin: 6px 0 0 -230px;
- width: 470px;
- min-height: 140px;
- border-radius: 3px;
- box-shadow: 0 20px 20px -10px rgba(0,0,0,0.55);
- display: none;
- }
- .search_form.shown.filled .results_container {
- display: block;
- }
-
- .search_form .results_container:before {
- content: ' ';
- height: 0;
- position: relative;
- width: 0;
- border: 10px solid transparent;
- border-bottom-color: #5c697b;
- display: block;
- top: -20px;
- left: 346px;
- }
-
- .search_form .results {
- max-height: 570px;
- overflow-x: hidden;
- padding: 10px 0;
- margin-top: -18px;
- }
-
- .movie_result {
- overflow: hidden;
- height: 140px;
- position: relative;
- }
-
- .movie_result .options {
- position: absolute;
- height: 100%;
- width: 100%;
- top: 0;
- left: 0;
- border: 1px solid transparent;
- border-width: 1px 0;
- border-radius: 0;
- box-shadow: inset 0 1px 8px rgba(0,0,0,0.25);
- }
-
- .movie_result .options > div {
- padding: 0 15px;
- border: 0;
- }
-
- .movie_result .options .thumbnail {
- vertical-align: middle;
- }
-
- .movie_result .options select {
- vertical-align: middle;
- display: inline-block;
- margin-right: 10px;
- }
- .movie_result .options select[name=title] { width: 180px; }
- .movie_result .options select[name=profile] { width: 90px; }
-
- .movie_result .options .button {
- vertical-align: middle;
- display: inline-block;
- }
-
- .movie_result .options .message {
- height: 100%;
- line-height: 140px;
- font-size: 20px;
- text-align: center;
- color: #fff;
- }
-
- .movie_result .data {
- padding: 0 15px;
- position: absolute;
- height: 100%;
- width: 100%;
- top: 0;
- left: 0;
- background: #5c697b;
- cursor: pointer;
-
- border-bottom: 1px solid #333;
- border-top: 1px solid rgba(255,255,255, 0.15);
- transition: all .6s cubic-bezier(0.9,0,0.1,1);
- }
- .movie_result .data.open {
- left: 100%;
- }
-
- .movie_result:last-child .data { border-bottom: 0; }
-
- .movie_result .in_wanted, .movie_result .in_library {
- position: absolute;
- margin-top: 105px;
- }
-
- .movie_result .thumbnail {
- width: 17%;
- display: inline-block;
- margin: 15px 3% 15px 0;
- vertical-align: top;
- border-radius: 3px;
- box-shadow: 0 0 3px rgba(0,0,0,0.35);
- }
-
- .movie_result .info {
- width: 80%;
- display: inline-block;
- vertical-align: top;
- padding: 15px 0;
- height: 120px;
- overflow: hidden;
- }
-
- .movie_result .info .tagline {
- max-height: 70px;
- overflow: hidden;
- display: inline-block;
- }
-
- .movie_result .add +.info {
- margin-left: 20%;
- }
-
- .movie_result .info h2 {
- margin: 0;
- font-size: 17px;
- line-height: 20px;
- }
-
- .movie_result .info h2 span {
- padding: 0 5px;
- }
-
- .movie_result .info h2 span:before { content: "("; }
- .movie_result .info h2 span:after { content: ")"; }
-
-.search_form .mask {
- border-radius: 3px;
- position: absolute;
- height: 100%;
- width: 100%;
- left: 0;
- top: 0;
-}
\ No newline at end of file
diff --git a/couchpotato/core/plugins/movie/static/search.js b/couchpotato/core/plugins/movie/static/search.js
deleted file mode 100644
index ba8b547ea0..0000000000
--- a/couchpotato/core/plugins/movie/static/search.js
+++ /dev/null
@@ -1,380 +0,0 @@
-Block.Search = new Class({
-
- Extends: BlockBase,
-
- cache: {},
-
- create: function(){
- var self = this;
-
- self.el = new Element('div.search_form').adopt(
- new Element('div.input').adopt(
- self.input = new Element('input.inlay', {
- 'placeholder': 'Search & add a new movie',
- 'events': {
- 'keyup': self.keyup.bind(self),
- 'focus': function(){
- self.el.addClass('focused')
- if(this.get('value'))
- self.hideResults(false)
- },
- 'blur': function(){
- (function(){
- self.el.removeClass('focused')
- }).delay(2000);
- }
- }
- }),
- new Element('span.enter', {
- 'events': {
- 'click': self.keyup.bind(self)
- },
- 'text':'Enter'
- }),
- new Element('a', {
- 'events': {
- 'click': self.clear.bind(self)
- }
- })
- ),
- self.result_container = new Element('div.results_container', {
- 'tween': {
- 'duration': 200
- },
- 'events': {
- 'mousewheel': function(e){
- (e).stopPropagation();
- }
- }
- }).adopt(
- self.results = new Element('div.results')
- )
- );
-
- self.mask = new Element('div.mask').inject(self.result_container).fade('hide');
-
- },
-
- clear: function(e){
- var self = this;
- (e).preventDefault();
-
- self.last_q = '';
- self.input.set('value', '');
- self.input.focus()
-
- self.movies = []
- self.results.empty()
- self.el.removeClass('filled')
- },
-
- hideResults: function(bool){
- var self = this;
-
- if(self.hidden == bool) return;
-
- self.el[bool ? 'removeClass' : 'addClass']('shown');
-
- if(bool){
- History.removeEvent('change', self.hideResults.bind(self, !bool));
- self.el.removeEvent('outerClick', self.hideResults.bind(self, !bool));
- }
- else {
- History.addEvent('change', self.hideResults.bind(self, !bool));
- self.el.addEvent('outerClick', self.hideResults.bind(self, !bool));
- }
-
- self.hidden = bool;
- },
-
- keyup: function(e){
- var self = this;
-
- self.el[self.q() ? 'addClass' : 'removeClass']('filled')
-
- if(self.q() != self.last_q && (['enter'].indexOf(e.key) > -1 || e.type == 'click'))
- self.autocomplete()
-
- },
-
- autocomplete: function(){
- var self = this;
-
- if(!self.q()){
- self.hideResults(true)
- return
- }
-
- self.list()
- },
-
- list: function(){
- var self = this;
-
- if(self.api_request && self.api_request.running) return
-
- var q = self.q();
- var cache = self.cache[q];
-
- self.hideResults(false);
-
- if(!cache){
- self.mask.fade('in');
-
- if(!self.spinner)
- self.spinner = createSpinner(self.mask);
-
- self.api_request = Api.request('movie.search', {
- 'data': {
- 'q': q
- },
- 'onComplete': self.fill.bind(self, q)
- })
- }
- else
- self.fill(q, cache)
-
- self.last_q = q;
-
- },
-
- fill: function(q, json){
- var self = this;
-
- self.cache[q] = json
-
- self.movies = {}
- self.results.empty()
-
- Object.each(json.movies, function(movie){
-
- var m = new Block.Search.Item(movie);
- $(m).inject(self.results)
- self.movies[movie.imdb || 'r-'+Math.floor(Math.random()*10000)] = m
-
- if(q == movie.imdb)
- m.showOptions()
-
- });
-
- if(q != self.q())
- self.list()
-
- // Calculate result heights
- var w = window.getSize(),
- rc = self.result_container.getCoordinates();
-
- self.results.setStyle('max-height', (w.y - rc.top - 50) + 'px')
- self.mask.fade('out')
-
- },
-
- loading: function(bool){
- this.el[bool ? 'addClass' : 'removeClass']('loading')
- },
-
- q: function(){
- return this.input.get('value').trim();
- }
-
-});
-
-Block.Search.Item = new Class({
-
- initialize: function(info, options){
- var self = this;
-
- self.info = info;
- self.alternative_titles = [];
-
- self.create();
- },
-
- create: function(){
- var self = this,
- info = self.info;
-
- self.el = new Element('div.movie_result', {
- 'id': info.imdb
- }).adopt(
- self.options_el = new Element('div.options.inlay'),
- self.data_container = new Element('div.data', {
- 'tween': {
- duration: 400,
- transition: 'quint:in:out'
- },
- 'events': {
- 'click': self.showOptions.bind(self)
- }
- }).adopt(
- self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
- 'src': info.images.poster[0],
- 'height': null,
- 'width': null
- }) : null,
- new Element('div.info').adopt(
- self.title = new Element('h2', {
- 'text': info.titles[0]
- }).adopt(
- self.year = info.year ? new Element('span.year', {
- 'text': info.year
- }) : null
- ),
- self.tagline = new Element('span.tagline', {
- 'text': info.tagline ? info.tagline : info.plot,
- 'title': info.tagline ? info.tagline : info.plot
- }),
- self.director = self.info.director ? new Element('span.director', {
- 'text': 'Director:' + info.director
- }) : null,
- self.starring = info.actors ? new Element('span.actors', {
- 'text': 'Starring:'
- }) : null
- )
- )
- )
-
- if(info.actors){
- Object.each(info.actors, function(actor){
- new Element('span', {
- 'text': actor
- }).inject(self.starring)
- })
- }
-
- info.titles.each(function(title){
- self.alternativeTitle({
- 'title': title
- });
- })
- },
-
- alternativeTitle: function(alternative){
- var self = this;
-
- self.alternative_titles.include(alternative);
- },
-
- showOptions: function(){
- var self = this;
-
- self.createOptions();
-
- self.data_container.addClass('open');
- self.el.addEvent('outerClick', self.closeOptions.bind(self))
-
- },
-
- closeOptions: function(){
- var self = this;
-
- self.data_container.removeClass('open');
- self.el.removeEvents('outerClick')
- },
-
- add: function(e){
- var self = this;
- (e).preventDefault();
-
- self.loadingMask();
-
- Api.request('movie.add', {
- 'data': {
- 'identifier': self.info.imdb,
- 'title': self.title_select.get('value'),
- 'profile_id': self.profile_select.get('value')
- },
- 'onComplete': function(json){
- self.options_el.empty();
- self.options_el.adopt(
- new Element('div.message', {
- 'text': json.added ? 'Movie successfully added.' : 'Movie didn\'t add properly. Check logs'
- })
- );
- self.mask.fade('out');
- },
- 'onFailure': function(){
- self.options_el.empty();
- self.options_el.adopt(
- new Element('div.message', {
- 'text': 'Something went wrong, check the logs for more info.'
- })
- );
- self.mask.fade('out');
- }
- });
- },
-
- createOptions: function(){
- var self = this,
- info = self.info;
-
- if(!self.options_el.hasClass('set')){
-
- if(self.info.in_library){
- var in_library = [];
- self.info.in_library.releases.each(function(release){
- in_library.include(release.quality.label)
- });
- }
-
- self.options_el.grab(
- new Element('div').adopt(
- self.thumbnail = (info.images && info.images.poster.length > 0) ? new Element('img.thumbnail', {
- 'src': info.images.poster[0],
- 'height': null,
- 'width': null
- }) : null,
- self.info.in_wanted && self.info.in_wanted.profile ? new Element('span.in_wanted', {
- 'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label
- }) : (in_library ? new Element('span.in_library', {
- 'text': 'Already in library: ' + in_library.join(', ')
- }) : null),
- self.title_select = new Element('select', {
- 'name': 'title'
- }),
- self.profile_select = new Element('select', {
- 'name': 'profile'
- }),
- new Element('a.button', {
- 'text': 'Add',
- 'events': {
- 'click': self.add.bind(self)
- }
- })
- )
- );
-
- Array.each(self.alternative_titles, function(alt){
- new Element('option', {
- 'text': alt.title
- }).inject(self.title_select)
- })
-
- Quality.getActiveProfiles().each(function(profile){
- new Element('option', {
- 'value': profile.id ? profile.id : profile.data.id,
- 'text': profile.label ? profile.label : profile.data.label
- }).inject(self.profile_select)
- });
-
- self.options_el.addClass('set');
- }
-
- },
-
- loadingMask: function(){
- var self = this;
-
- self.mask = new Element('span.mask').inject(self.el).fade('hide')
-
- createSpinner(self.mask)
- self.mask.fade('in')
-
- },
-
- toElement: function(){
- return this.el
- }
-
-});
diff --git a/couchpotato/core/plugins/profile/__init__.py b/couchpotato/core/plugins/profile/__init__.py
index ac19b01812..15a74eee51 100644
--- a/couchpotato/core/plugins/profile/__init__.py
+++ b/couchpotato/core/plugins/profile/__init__.py
@@ -1,6 +1,5 @@
from .main import ProfilePlugin
-def start():
- return ProfilePlugin()
-config = []
+def autoload():
+ return ProfilePlugin()
diff --git a/couchpotato/core/plugins/profile/index.py b/couchpotato/core/plugins/profile/index.py
new file mode 100644
index 0000000000..c2bf9445b8
--- /dev/null
+++ b/couchpotato/core/plugins/profile/index.py
@@ -0,0 +1,16 @@
+from CodernityDB.tree_index import TreeBasedIndex
+
+
+class ProfileIndex(TreeBasedIndex):
+ _version = 1
+
+ def __init__(self, *args, **kwargs):
+ kwargs['key_format'] = 'i'
+ super(ProfileIndex, self).__init__(*args, **kwargs)
+
+ def make_key(self, key):
+ return key
+
+ def make_key_value(self, data):
+ if data.get('_t') == 'profile':
+ return data.get('order', 99), None
diff --git a/couchpotato/core/plugins/profile/main.py b/couchpotato/core/plugins/profile/main.py
index 4caa54f701..29bd6cbe32 100644
--- a/couchpotato/core/plugins/profile/main.py
+++ b/couchpotato/core/plugins/profile/main.py
@@ -1,18 +1,22 @@
-from couchpotato import get_session
+import traceback
+
+from couchpotato import get_db, tryInt
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode
-from couchpotato.core.helpers.request import jsonified, getParams, getParam
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
-from couchpotato.core.settings.model import Profile, ProfileType
+from .index import ProfileIndex
+
log = CPLog(__name__)
class ProfilePlugin(Plugin):
- to_dict = {'types': {}}
+ _database = {
+ 'profile': ProfileIndex
+ }
def __init__(self):
addEvent('profile.all', self.all)
@@ -30,155 +34,209 @@ def __init__(self):
})
addEvent('app.initialize', self.fill, priority = 90)
+ addEvent('app.load', self.forceDefaults, priority = 110)
- def allView(self):
+ def forceDefaults(self):
- return jsonified({
- 'success': True,
- 'list': self.all()
- })
+ db = get_db()
- def all(self):
+ # Fill qualities and profiles if they are empty somehow..
+ if db.count(db.all, 'profile') == 0:
- db = get_session()
- profiles = db.query(Profile).all()
+ if db.count(db.all, 'quality') == 0:
+ fireEvent('quality.fill', single = True)
- temp = []
- for profile in profiles:
- temp.append(profile.to_dict(self.to_dict))
+ self.fill()
- return temp
-
- def save(self):
-
- params = getParams()
+ # Get all active movies without profile
+ try:
+ medias = fireEvent('media.with_status', 'active', single = True)
- db = get_session()
+ profile_ids = [x.get('_id') for x in self.all()]
+ default_id = profile_ids[0]
- p = db.query(Profile).filter_by(id = params.get('id')).first()
- if not p:
- p = Profile()
- db.add(p)
+ for media in medias:
+ if media.get('profile_id') not in profile_ids:
+ media['profile_id'] = default_id
+ db.update(media)
+ except:
+ log.error('Failed: %s', traceback.format_exc())
- p.label = toUnicode(params.get('label'))
- p.order = params.get('order', p.order if p.order else 0)
- p.core = params.get('core', False)
+ def allView(self, **kwargs):
- #delete old types
- [db.delete(t) for t in p.types]
+ return {
+ 'success': True,
+ 'list': self.all()
+ }
- order = 0
- for type in params.get('types', []):
- t = ProfileType(
- order = order,
- finish = type.get('finish') if order > 0 else 1,
- wait_for = params.get('wait_for'),
- quality_id = type.get('quality_id')
- )
- p.types.append(t)
+ def all(self):
- order += 1
+ db = get_db()
+ profiles = db.all('profile', with_doc = True)
- db.commit()
+ return [x['doc'] for x in profiles]
- profile_dict = p.to_dict(self.to_dict)
+ def save(self, **kwargs):
- return jsonified({
- 'success': True,
- 'profile': profile_dict
- })
+ try:
+ db = get_db()
+
+ profile = {
+ '_t': 'profile',
+ 'label': toUnicode(kwargs.get('label')),
+ 'order': tryInt(kwargs.get('order', 999)),
+ 'core': kwargs.get('core', False),
+ 'minimum_score': tryInt(kwargs.get('minimum_score', 1)),
+ 'qualities': [],
+ 'wait_for': [],
+ 'stop_after': [],
+ 'finish': [],
+ '3d': []
+ }
+
+ # Update types
+ order = 0
+ for type in kwargs.get('types', []):
+ profile['qualities'].append(type.get('quality'))
+ profile['wait_for'].append(tryInt(kwargs.get('wait_for', 0)))
+ profile['stop_after'].append(tryInt(kwargs.get('stop_after', 0)))
+ profile['finish'].append((tryInt(type.get('finish')) == 1) if order > 0 else True)
+ profile['3d'].append(tryInt(type.get('3d')))
+ order += 1
+
+ id = kwargs.get('id')
+ try:
+ p = db.get('id', id)
+ profile['order'] = tryInt(kwargs.get('order', p.get('order', 999)))
+ except:
+ p = db.insert(profile)
+
+ p.update(profile)
+ db.update(p)
+
+ return {
+ 'success': True,
+ 'profile': p
+ }
+ except:
+ log.error('Failed: %s', traceback.format_exc())
+
+ return {
+ 'success': False
+ }
def default(self):
+ db = get_db()
+ return list(db.all('profile', limit = 1, with_doc = True))[0]['doc']
- db = get_session()
- default = db.query(Profile).first()
- default_dict = default.to_dict(self.to_dict)
+ def saveOrder(self, **kwargs):
- return default_dict
+ try:
+ db = get_db()
- def saveOrder(self):
+ order = 0
- params = getParams()
- db = get_session()
+ for profile_id in kwargs.get('ids', []):
+ p = db.get('id', profile_id)
+ p['hide'] = tryInt(kwargs.get('hidden')[order]) == 1
+ p['order'] = order
+ db.update(p)
- order = 0
- for profile in params.get('ids', []):
- p = db.query(Profile).filter_by(id = profile).first()
- p.hide = params.get('hidden')[order]
- p.order = order
+ order += 1
- order += 1
+ return {
+ 'success': True
+ }
+ except:
+ log.error('Failed: %s', traceback.format_exc())
- db.commit()
+ return {
+ 'success': False
+ }
- return jsonified({
- 'success': True
- })
+ def delete(self, id = None, **kwargs):
- def delete(self):
+ try:
+ db = get_db()
- id = getParam('id')
+ success = False
+ message = ''
- db = get_session()
+ try:
+ p = db.get('id', id)
+ db.delete(p)
- success = False
- message = ''
- try:
- p = db.query(Profile).filter_by(id = id).first()
+ # Force defaults on all empty profile movies
+ self.forceDefaults()
- db.delete(p)
- db.commit()
+ success = True
+ except Exception as e:
+ message = log.error('Failed deleting Profile: %s', e)
- success = True
- except Exception, e:
- message = log.error('Failed deleting Profile: %s', e)
+ return {
+ 'success': success,
+ 'message': message
+ }
+ except:
+ log.error('Failed: %s', traceback.format_exc())
- return jsonified({
- 'success': success,
- 'message': message
- })
+ return {
+ 'success': False
+ }
def fill(self):
- db = get_session();
-
- profiles = [{
- 'label': 'Best',
- 'qualities': ['720p', '1080p', 'brrip', 'dvdrip']
- }, {
- 'label': 'HD',
- 'qualities': ['720p', '1080p']
- }, {
- 'label': 'SD',
- 'qualities': ['dvdrip', 'dvdr']
- }]
-
- # Create default quality profile
- order = -2
- for profile in profiles:
- log.info('Creating default profile: %s', profile.get('label'))
- p = Profile(
- label = toUnicode(profile.get('label')),
- order = order
- )
- db.add(p)
-
- quality_order = 0
- for quality in profile.get('qualities'):
- quality = fireEvent('quality.single', identifier = quality, single = True)
- profile_type = ProfileType(
- quality_id = quality.get('id'),
- profile = p,
- finish = True,
- wait_for = 0,
- order = quality_order
- )
- p.types.append(profile_type)
-
- quality_order += 1
-
- order += 1
-
- db.commit()
-
- return True
+ try:
+ db = get_db()
+
+ profiles = [{
+ 'label': 'Best',
+ 'qualities': ['720p', '1080p', 'brrip', 'dvdrip']
+ }, {
+ 'label': 'HD',
+ 'qualities': ['720p', '1080p']
+ }, {
+ 'label': 'SD',
+ 'qualities': ['dvdrip', 'dvdr']
+ }, {
+ 'label': 'Prefer 3D HD',
+ 'qualities': ['1080p', '720p', '720p', '1080p'],
+ '3d': [True, True]
+ }, {
+ 'label': '3D HD',
+ 'qualities': ['1080p', '720p'],
+ '3d': [True, True]
+ }]
+
+ # Create default quality profile
+ order = 0
+ for profile in profiles:
+ log.info('Creating default profile: %s', profile.get('label'))
+
+ pro = {
+ '_t': 'profile',
+ 'label': toUnicode(profile.get('label')),
+ 'order': order,
+ 'qualities': profile.get('qualities'),
+ 'minimum_score': 1,
+ 'finish': [],
+ 'wait_for': [],
+ 'stop_after': [],
+ '3d': []
+ }
+
+ threed = profile.get('3d', [])
+ for q in profile.get('qualities'):
+ pro['finish'].append(True)
+ pro['wait_for'].append(0)
+ pro['stop_after'].append(0)
+ pro['3d'].append(threed.pop() if threed else False)
+
+ db.insert(pro)
+ order += 1
+
+ return True
+ except:
+ log.error('Failed: %s', traceback.format_exc())
+
+ return False
diff --git a/couchpotato/core/plugins/profile/static/profile.css b/couchpotato/core/plugins/profile/static/profile.css
deleted file mode 100644
index 9d50d2fd00..0000000000
--- a/couchpotato/core/plugins/profile/static/profile.css
+++ /dev/null
@@ -1,139 +0,0 @@
-.add_new_profile {
- padding: 20px;
- display: block;
- text-align: center;
- font-size: 20px;
- border-bottom: 1px solid rgba(255,255,255,0.2);
-}
-
-.profile { border-bottom: 1px solid rgba(255,255,255,0.2) }
-
- .profile > .delete {
- height: 20px;
- width: 20px;
- position: absolute;
- margin-left: 690px;
- padding: 14px;
- background-position: center;
- }
-
- .profile .qualities {
- min-height: 80px;
- }
-
- .profile .formHint {
- width: 250px !important;
- vertical-align: top !important;
- margin: 0 !important;
- padding-left: 3px !important;
- opacity: 0.1;
- }
- .profile:hover .formHint {
- opacity: 1;
- }
-
- .profile .wait_for {
- position: absolute;
- margin: -45px 0 0 437px;
- }
-
- .profile .wait_for input {
- margin: 0 5px !important;
- }
-
- .profile .types {
- padding: 0;
- margin: 0 20px 0 -4px;
- display: inline-block;
- }
-
- .profile .types li {
- padding: 3px 5px;
- border-bottom: 1px solid rgba(255,255,255,0.2);
- list-style: none;
- }
- .profile .types li:last-child { border: 0; }
-
- .profile .types li > * {
- display: inline-block;
- vertical-align: middle;
- line-height: 0;
- margin-right: 10px;
- }
-
- .profile .quality_type select {
- width: 186px;
- margin-left: -1px;
- }
-
- .profile .types li.is_empty .check, .profile .types li.is_empty .delete, .profile .types li.is_empty .handle {
- visibility: hidden;
- }
-
- .profile .types .type .handle {
- background: url('./handle.png') center;
- display: inline-block;
- height: 20px;
- width: 20px;
- cursor: grab;
- cursor: -moz-grab;
- cursor: -webkit-grab;
- margin: 0;
- }
-
- .profile .types .type .delete {
- background-position: left center;
- height: 20px;
- width: 20px;
- visibility: hidden;
- cursor: pointer;
- }
-
- .profile .types .type:hover:not(.is_empty) .delete {
- visibility: visible;
- }
-
-#profile_ordering {
-
-}
-
- #profile_ordering ul {
- float: left;
- margin: 0;
- width: 275px;
- padding: 0;
- }
-
- #profile_ordering li {
- cursor: grab;
- cursor: -moz-grab;
- cursor: -webkit-grab;
- border-bottom: 1px solid rgba(255,255,255,0.2);
- padding: 0 5px;
- }
- #profile_ordering li:last-child { border: 0; }
-
- #profile_ordering li .check {
- margin: 2px 10px 0 0;
- vertical-align: top;
- }
-
- #profile_ordering li > span {
- display: inline-block;
- height: 20px;
- vertical-align: top;
- line-height: 20px;
- }
-
- #profile_ordering li .handle {
- background: url('./handle.png') center;
- width: 20px;
- float: right;
- }
-
- #profile_ordering .formHint {
- clear: none;
- float: right;
- width: 250px;
- margin: 0;
- }
\ No newline at end of file
diff --git a/couchpotato/core/plugins/profile/static/profile.js b/couchpotato/core/plugins/profile/static/profile.js
index 3bb44989cf..01fad1e5f2 100644
--- a/couchpotato/core/plugins/profile/static/profile.js
+++ b/couchpotato/core/plugins/profile/static/profile.js
@@ -24,40 +24,69 @@ var Profile = new Class({
var data = self.data;
self.el = new Element('div.profile').adopt(
- self.delete_button = new Element('span.delete.icon', {
+ self.delete_button = new Element('span.delete.icon-delete', {
'events': {
'click': self.del.bind(self)
}
}),
new Element('.quality_label.ctrlHolder').adopt(
new Element('label', {'text':'Name'}),
- new Element('input.inlay', {
+ new Element('input', {
'type':'text',
'value': data.label,
'placeholder': 'Profile name'
})
),
- new Element('div.wait_for.ctrlHolder').adopt(
- new Element('span', {'text':'Wait'}),
- new Element('input.inlay.xsmall', {
- 'type':'text',
- 'value': data.types && data.types.length > 0 ? data.types[0].wait_for : 0
- }),
- new Element('span', {'text':'day(s) for a better quality.'})
- ),
new Element('div.qualities.ctrlHolder').adopt(
new Element('label', {'text': 'Search for'}),
self.type_container = new Element('ol.types'),
new Element('div.formHint', {
'html': "Search these qualities (2 minimum), from top to bottom. Use the checkbox, to stop searching after it found this quality."
})
+ ),
+ new Element('div.wait_for.ctrlHolder').adopt(
+ // "Wait the entered number of days for a checked quality, before downloading a lower quality release."
+ new Element('span', {'text':'Wait'}),
+ new Element('input.wait_for_input.xsmall', {
+ 'type':'text',
+ 'value': data.wait_for && data.wait_for.length > 0 ? data.wait_for[0] : 0
+ }),
+ new Element('span', {'text':'day(s) for a better quality '}),
+ new Element('span.advanced', {'text':'and keep searching'}),
+
+ // "After a checked quality is found and downloaded, continue searching for even better quality releases for the entered number of days."
+ new Element('input.xsmall.stop_after_input.advanced', {
+ 'type':'text',
+ 'value': data.stop_after && data.stop_after.length > 0 ? data.stop_after[0] : 0
+ }),
+ new Element('span.advanced', {'text':'day(s) for a better (checked) quality.'}),
+
+ // Minimum score of
+ new Element('span.advanced', {'html':' Releases need a minimum score of'}),
+ new Element('input.advanced.xsmall.minimum_score_input', {
+ 'size': 4,
+ 'type':'text',
+ 'value': data.minimum_score || 1
+ })
)
);
- self.makeSortable()
+ self.makeSortable();
+
+ // Combine qualities and properties into types
+ if(data.qualities){
+ data.types = [];
+ data.qualities.each(function(quality, nr){
+ data.types.include({
+ 'quality': quality,
+ 'finish': data.finish[nr] || false,
+ '3d': data['3d'] ? data['3d'][nr] || false : false
+ });
+ });
+ }
if(data.types)
- Object.each(data.types, self.addType.bind(self))
+ data.types.each(self.addType.bind(self));
else
self.delete_button.hide();
@@ -67,8 +96,8 @@ var Profile = new Class({
save: function(delay){
var self = this;
- if(self.save_timer) clearTimeout(self.save_timer);
- self.save_timer = (function(){
+ if(self.save_timer) clearRequestTimeout(self.save_timer);
+ self.save_timer = requestTimeout(function(){
self.addType();
@@ -87,14 +116,14 @@ var Profile = new Class({
'onComplete': function(json){
if(json.success){
self.data = json.profile;
- self.type_container.getElement('li:first-child input[type=checkbox]')
+ self.type_container.getElement('li:first-child input.finish[type=checkbox]')
.set('checked', true)
.getParent().addClass('checked');
}
}
});
- }).delay(delay, self)
+ }, delay);
},
@@ -102,21 +131,24 @@ var Profile = new Class({
var self = this;
var data = {
- 'id' : self.data.id,
+ 'id' : self.data._id,
'label' : self.el.getElement('.quality_label input').get('value'),
- 'wait_for' : self.el.getElement('.wait_for input').get('value'),
+ 'wait_for' : self.el.getElement('.wait_for_input').get('value'),
+ 'stop_after' : self.el.getElement('.stop_after_input').get('value'),
+ 'minimum_score' : self.el.getElement('.minimum_score_input').get('value'),
'types': []
- }
+ };
Array.each(self.type_container.getElements('.type'), function(type){
- if(!type.hasClass('deleted') && type.getElement('select').get('value') > 0)
+ if(!type.hasClass('deleted') && type.getElement('select').get('value') != -1)
data.types.include({
- 'quality_id': type.getElement('select').get('value'),
- 'finish': +type.getElement('input[type=checkbox]').checked
+ 'quality': type.getElement('select').get('value'),
+ 'finish': +type.getElement('input.finish[type=checkbox]').checked,
+ '3d': +type.getElement('input.3d[type=checkbox]').checked
});
- })
+ });
- return data
+ return data;
},
addType: function(data){
@@ -145,7 +177,7 @@ var Profile = new Class({
var self = this;
return self.types.filter(function(type){
- return type.get('quality_id')
+ return type.get('quality');
});
},
@@ -162,7 +194,7 @@ var Profile = new Class({
(e).preventDefault();
Api.request('profile.delete', {
'data': {
- 'id': self.data.id
+ 'id': self.data._id
},
'useSpinner': true,
'spinnerOptions': {
@@ -199,15 +231,15 @@ var Profile = new Class({
},
get: function(attr){
- return this.data[attr]
+ return this.data[attr];
},
isCore: function(){
- return this.data.core
+ return this.data.core;
},
toElement: function(){
- return this.el
+ return this.el;
}
});
@@ -227,6 +259,7 @@ Profile.Type = new Class({
self.addEvent('change', function(){
self.el[self.qualities.get('value') == '-1' ? 'addClass' : 'removeClass']('is_empty');
+ self.el[Quality.getQuality(self.qualities.get('value')).allow_3d ? 'addClass': 'removeClass']('allow_3d');
self.deleted = self.qualities.get('value') == '-1';
});
@@ -237,36 +270,48 @@ Profile.Type = new Class({
var data = self.data;
self.el = new Element('li.type').adopt(
- new Element('span.quality_type').adopt(
+ new Element('span.quality_type.select_wrapper.icon-dropdown').grab(
self.fillQualities()
),
- new Element('span.finish').adopt(
- self.finish = new Element('input.inlay.finish[type=checkbox]', {
+ self.finish_container = new Element('label.finish').adopt(
+ self.finish = new Element('input.finish[type=checkbox]', {
'checked': data.finish !== undefined ? data.finish : 1,
'events': {
- 'change': function(e){
+ 'change': function(){
if(self.el == self.el.getParent().getElement(':first-child')){
- self.finish_class.check();
- alert('Top quality always finishes the search')
+ alert('Top quality always finishes the search');
return;
}
self.fireEvent('change');
}
}
- })
+ }),
+ new Element('span.check_label[text=finish]')
),
- new Element('span.delete.icon', {
+ self['3d_container'] = new Element('label.threed').adopt(
+ self['3d'] = new Element('input.3d[type=checkbox]', {
+ 'checked': data['3d'] !== undefined ? data['3d'] : 0,
+ 'events': {
+ 'change': function(){
+ self.fireEvent('change');
+ }
+ }
+ }),
+ new Element('span.check_label[text=3D]')
+ ),
+ new Element('span.delete.icon-cancel', {
'events': {
'click': self.del.bind(self)
}
}),
- new Element('span.handle')
+ new Element('span.handle.icon-handle')
);
- self.el[self.data.quality_id > 0 ? 'removeClass' : 'addClass']('is_empty');
+ self.el[self.data.quality ? 'removeClass' : 'addClass']('is_empty');
- self.finish_class = new Form.Check(self.finish);
+ if(self.data.quality && Quality.getQuality(self.data.quality).allow_3d)
+ self.el.addClass('allow_3d');
},
@@ -277,7 +322,7 @@ Profile.Type = new Class({
'events': {
'change': self.fireEvent.bind(self, 'change')
}
- }).adopt(
+ }).grab(
new Element('option', {
'text': '+ Add another quality',
'value': -1
@@ -287,11 +332,12 @@ Profile.Type = new Class({
Object.each(Quality.qualities, function(q){
new Element('option', {
'text': q.label,
- 'value': q.id
- }).inject(self.qualities)
+ 'value': q.identifier,
+ 'data-allow_3d': q.allow_3d
+ }).inject(self.qualities);
});
- self.qualities.set('value', self.data.quality_id);
+ self.qualities.set('value', self.data.quality);
return self.qualities;
@@ -301,9 +347,10 @@ Profile.Type = new Class({
var self = this;
return {
- 'quality_id': self.qualities.get('value'),
- 'finish': +self.finish.checked
- }
+ 'quality': self.qualities.get('value'),
+ 'finish': +self.finish.checked,
+ '3d': +self['3d'].checked
+ };
},
get: function(key){
@@ -324,4 +371,4 @@ Profile.Type = new Class({
return this.el;
}
-})
\ No newline at end of file
+});
diff --git a/couchpotato/core/plugins/profile/static/profile.scss b/couchpotato/core/plugins/profile/static/profile.scss
new file mode 100644
index 0000000000..a48f09ae95
--- /dev/null
+++ b/couchpotato/core/plugins/profile/static/profile.scss
@@ -0,0 +1,167 @@
+@import "_mixins";
+
+.add_new_profile {
+ padding: 20px;
+ display: block;
+ text-align: center;
+ font-size: 20px;
+ border-bottom: 1px solid transparent;
+ @include theme(border-color, off);
+}
+
+.profile {
+ margin-bottom: 20px;
+
+ .quality_label input {
+ font-weight: bold;
+ }
+
+ > .delete {
+ position: absolute;
+ padding: $padding/3 $padding;
+ right: 0;
+ cursor: pointer;
+ opacity: 0.6;
+ color: #fd5353;
+ font-size: 1.5em;
+ z-index: 2;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+
+ .ctrlHolder {
+
+ .types {
+ flex: 1 1 auto;
+ min-width: 360px;
+
+ .type {
+ display: flex;
+ flex-row: row nowrap;
+ align-items: center;
+ padding: 2px 0;
+
+ label {
+ min-width: 0;
+ margin-left: $padding/2;
+
+ span {
+ font-size: .9em;
+ }
+ }
+
+ input[type=checkbox] {
+ margin-right: 3px;
+ }
+
+ .delete, .handle {
+ margin-left: $padding/4;
+ width: 20px;
+ font-size: 20px;
+ opacity: .1;
+ text-align: center;
+ cursor: pointer;
+
+ &.handle {
+ cursor: move;
+ cursor: grab;
+ }
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+
+ &.is_empty {
+ .delete, .handle {
+ display: none;
+ }
+ }
+ }
+
+ }
+
+ &.wait_for.wait_for {
+ display: block;
+
+ input {
+ min-width: 0;
+ width: 40px;
+ text-align: center;
+ margin: 0 2px;
+ }
+
+ .advanced {
+ display: none;
+ @include theme(color, primary);
+
+ .show_advanced & {
+ display: inline;
+ }
+ }
+
+ }
+
+ .formHint {
+ }
+
+ }
+}
+
+#profile_ordering {
+ ul {
+ list-style: none;
+ margin: 0;
+ width: 275px;
+ padding: 0;
+ }
+
+ li {
+ border-bottom: 1px solid transparent;
+ @include theme(border-color, off);
+ padding: 5px;
+ display: flex;
+ align-items: center;
+
+ &:hover {
+ @include theme(background, off);
+ }
+
+ &:last-child { border: 0; }
+
+ input[type=checkbox] {
+ margin: 2px 10px 0 0;
+ vertical-align: top;
+ }
+
+ > span {
+ display: inline-block;
+ height: 20px;
+ vertical-align: top;
+ line-height: 20px;
+
+ &.profile_label {
+ flex: 1 1 auto;
+ }
+ }
+
+ .handle {
+ font-size: 20px;
+ width: 20px;
+ float: right;
+ cursor: move;
+ cursor: grab;
+ opacity: .5;
+ text-align: center;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+ }
+
+ .formHint {
+ }
+}
diff --git a/couchpotato/core/plugins/quality/__init__.py b/couchpotato/core/plugins/quality/__init__.py
index e1b97ad0d8..7710251c7c 100644
--- a/couchpotato/core/plugins/quality/__init__.py
+++ b/couchpotato/core/plugins/quality/__init__.py
@@ -1,6 +1,5 @@
from .main import QualityPlugin
-def start():
- return QualityPlugin()
-config = []
+def autoload():
+ return QualityPlugin()
diff --git a/couchpotato/core/plugins/quality/index.py b/couchpotato/core/plugins/quality/index.py
new file mode 100644
index 0000000000..7804397216
--- /dev/null
+++ b/couchpotato/core/plugins/quality/index.py
@@ -0,0 +1,18 @@
+from hashlib import md5
+
+from CodernityDB.hash_index import HashIndex
+
+
+class QualityIndex(HashIndex):
+ _version = 1
+
+ def __init__(self, *args, **kwargs):
+ kwargs['key_format'] = '32s'
+ super(QualityIndex, self).__init__(*args, **kwargs)
+
+ def make_key(self, key):
+ return md5(key).hexdigest()
+
+ def make_key_value(self, data):
+ if data.get('_t') == 'quality' and data.get('identifier'):
+ return md5(data.get('identifier')).hexdigest(), None
diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py
index 4cd5b8de68..3ed1a7a4a7 100644
--- a/couchpotato/core/plugins/quality/main.py
+++ b/couchpotato/core/plugins/quality/main.py
@@ -1,42 +1,59 @@
-from couchpotato import get_session
+from math import fabs, ceil
+import traceback
+import re
+
+from CodernityDB.database import RecordNotFound
+from couchpotato import get_db
from couchpotato.api import addApiView
-from couchpotato.core.event import addEvent
-from couchpotato.core.helpers.encoding import toUnicode
-from couchpotato.core.helpers.request import jsonified, getParams
-from couchpotato.core.helpers.variable import mergeDicts, md5, getExt
+from couchpotato.core.event import addEvent, fireEvent
+from couchpotato.core.helpers.encoding import toUnicode, ss
+from couchpotato.core.helpers.variable import mergeDicts, getExt, tryInt, splitString, tryFloat
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
-from couchpotato.core.settings.model import Quality, Profile, ProfileType
-from sqlalchemy.sql.expression import or_
-import os.path
-import re
-import time
+from couchpotato.core.plugins.quality.index import QualityIndex
+
log = CPLog(__name__)
class QualityPlugin(Plugin):
+ _database = {
+ 'quality': QualityIndex
+ }
+
qualities = [
- {'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]},
- {'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
- {'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
- {'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi']},
- {'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']},
- {'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
- {'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']},
- {'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
- {'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
- {'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
- {'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}
+ {'identifier': 'bd50', 'hd': True, 'allow_3d': True, 'size': (20000, 60000), 'median_size': 40000, 'label': 'BR-Disk', 'alternative': ['bd25', ('br', 'disk')], 'allow': ['1080p'], 'ext':['iso', 'img'], 'tags': ['bdmv', 'certificate', ('complete', 'bluray'), 'avc', 'mvc']},
+ {'identifier': '1080p', 'hd': True, 'allow_3d': True, 'size': (4000, 20000), 'median_size': 10000, 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts', 'ts'], 'tags': ['m2ts', 'x264', 'h264', '1080']},
+ {'identifier': '720p', 'hd': True, 'allow_3d': True, 'size': (3000, 10000), 'median_size': 5500, 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts'], 'tags': ['x264', 'h264', '720']},
+ {'identifier': 'brrip', 'hd': True, 'allow_3d': True, 'size': (700, 7000), 'median_size': 2000, 'label': 'BR-Rip', 'alternative': ['bdrip', ('br', 'rip'), 'hdtv', 'hdrip'], 'allow': ['720p', '1080p'], 'ext':['mp4', 'avi'], 'tags': ['webdl', ('web', 'dl')]},
+ {'identifier': 'dvdr', 'size': (3000, 10000), 'median_size': 4500, 'label': 'DVD-R', 'alternative': ['br2dvd', ('dvd', 'r')], 'allow': [], 'ext':['iso', 'img', 'vob'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts', ('dvd', 'r'), 'dvd9']},
+ {'identifier': 'dvdrip', 'size': (600, 2400), 'median_size': 1500, 'label': 'DVD-Rip', 'width': 720, 'alternative': [('dvd', 'rip')], 'allow': [], 'ext':['avi'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
+ {'identifier': 'scr', 'size': (600, 1600), 'median_size': 700, 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr', 'webrip', ('web', 'rip')], 'allow': ['dvdr', 'dvdrip', '720p', '1080p'], 'ext':[], 'tags': []},
+ {'identifier': 'r5', 'size': (600, 1000), 'median_size': 700, 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr', '720p', '1080p'], 'ext':[]},
+ {'identifier': 'tc', 'size': (600, 1000), 'median_size': 700, 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': ['720p', '1080p'], 'ext':[]},
+ {'identifier': 'ts', 'size': (600, 1000), 'median_size': 700, 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': ['720p', '1080p'], 'ext':[]},
+ {'identifier': 'cam', 'size': (600, 1000), 'median_size': 700, 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': ['720p', '1080p'], 'ext':[]}
]
pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr']
+ threed_tags = {
+ 'sbs': [('half', 'sbs'), 'hsbs', ('full', 'sbs'), 'fsbs'],
+ 'ou': [('half', 'ou'), 'hou', ('full', 'ou'), 'fou'],
+ '3d': ['2d3d', '3d2d', '3d'],
+ }
+
+ cached_qualities = None
+ cached_order = None
def __init__(self):
addEvent('quality.all', self.all)
addEvent('quality.single', self.single)
addEvent('quality.guess', self.guess)
addEvent('quality.pre_releases', self.preReleases)
+ addEvent('quality.order', self.getOrder)
+ addEvent('quality.ishigher', self.isHigher)
+ addEvent('quality.isfinish', self.isFinish)
+ addEvent('quality.fill', self.fill)
addApiView('quality.size.save', self.saveSize)
addApiView('quality.list', self.allView, docs = {
@@ -49,37 +66,55 @@ def __init__(self):
addEvent('app.initialize', self.fill, priority = 10)
+ addEvent('app.test', self.doTest)
+
+ self.order = []
+ self.addOrder()
+
+ def addOrder(self):
+ self.order = []
+ for q in self.qualities:
+ self.order.append(q.get('identifier'))
+
+ def getOrder(self):
+ return self.order
+
def preReleases(self):
return self.pre_releases
- def allView(self):
+ def allView(self, **kwargs):
- return jsonified({
+ return {
'success': True,
'list': self.all()
- })
+ }
def all(self):
- db = get_session()
+ if self.cached_qualities:
+ return self.cached_qualities
- qualities = db.query(Quality).all()
+ db = get_db()
temp = []
- for quality in qualities:
- q = mergeDicts(self.getQuality(quality.identifier), quality.to_dict())
+ for quality in self.qualities:
+ quality_doc = db.get('quality', quality.get('identifier'), with_doc = True)['doc']
+ q = mergeDicts(quality, quality_doc)
temp.append(q)
+ if len(temp) == len(self.qualities):
+ self.cached_qualities = temp
+
return temp
def single(self, identifier = ''):
- db = get_session()
+ db = get_db()
quality_dict = {}
- quality = db.query(Quality).filter(or_(Quality.identifier == identifier, Quality.id == identifier)).first()
+ quality = db.get('quality', identifier, with_doc = True)['doc']
if quality:
- quality_dict = dict(self.getQuality(quality.identifier), **quality.to_dict())
+ quality_dict = mergeDicts(self.getQuality(quality['identifier']), quality)
return quality_dict
@@ -89,128 +124,393 @@ def getQuality(self, identifier):
if identifier == q.get('identifier'):
return q
- def saveSize(self):
+ def saveSize(self, **kwargs):
- params = getParams()
+ try:
+ db = get_db()
+ quality = db.get('quality', kwargs.get('identifier'), with_doc = True)
- db = get_session()
- quality = db.query(Quality).filter_by(identifier = params.get('identifier')).first()
+ if quality:
+ quality['doc'][kwargs.get('value_type')] = tryInt(kwargs.get('value'))
+ db.update(quality['doc'])
- if quality:
- setattr(quality, params.get('value_type'), params.get('value'))
- db.commit()
+ self.cached_qualities = None
- return jsonified({
- 'success': True
- })
+ return {
+ 'success': True
+ }
+ except:
+ log.error('Failed: %s', traceback.format_exc())
+
+ return {
+ 'success': False
+ }
def fill(self):
- db = get_session();
+ try:
+ db = get_db()
+
+ order = 0
+ for q in self.qualities:
+
+ existing = None
+ try:
+ existing = db.get('quality', q.get('identifier'))
+ except RecordNotFound:
+ pass
+
+ if not existing:
+ db.insert({
+ '_t': 'quality',
+ 'order': order,
+ 'identifier': q.get('identifier'),
+ 'size_min': tryInt(q.get('size')[0]),
+ 'size_max': tryInt(q.get('size')[1]),
+ })
+
+ log.info('Creating profile: %s', q.get('label'))
+ db.insert({
+ '_t': 'profile',
+ 'order': order + 20, # Make sure it goes behind other profiles
+ 'core': True,
+ 'qualities': [q.get('identifier')],
+ 'label': toUnicode(q.get('label')),
+ 'finish': [True],
+ 'wait_for': [0],
+ })
+
+ order += 1
+
+ return True
+ except:
+ log.error('Failed: %s', traceback.format_exc())
+
+ return False
+
+ def guess(self, files, extra = None, size = None, use_cache = True):
+ if not extra: extra = {}
- order = 0
- for q in self.qualities:
+ # Create hash for cache
+ cache_key = str([f.replace('.' + getExt(f), '') if len(getExt(f)) < 4 else f for f in files])
+ if use_cache:
+ cached = self.getCache(cache_key)
+ if cached and len(extra) == 0:
+ return cached
- # Create quality
- qual = db.query(Quality).filter_by(identifier = q.get('identifier')).first()
+ qualities = self.all()
- if not qual:
- log.info('Creating quality: %s', q.get('label'))
- qual = Quality()
- qual.order = order
- qual.identifier = q.get('identifier')
- qual.label = toUnicode(q.get('label'))
- qual.size_min, qual.size_max = q.get('size')
+ # Start with 0
+ score = {}
+ for quality in qualities:
+ score[quality.get('identifier')] = {
+ 'score': 0,
+ '3d': {}
+ }
- db.add(qual)
+ # Use metadata titles as extra check
+ if extra and extra.get('titles'):
+ files.extend(extra.get('titles'))
- # Create single quality profile
- prof = db.query(Profile).filter(
- Profile.core == True
- ).filter(
- Profile.types.any(quality = qual)
- ).all()
+ for cur_file in files:
+ words = re.split('\W+', cur_file.lower())
+ name_year = fireEvent('scanner.name_year', cur_file, file_name = cur_file, single = True)
+ threed_words = words
+ if name_year and name_year.get('name'):
+ split_name = splitString(name_year.get('name'), ' ')
+ threed_words = [x for x in words if x not in split_name]
- if not prof:
- log.info('Creating profile: %s', q.get('label'))
- prof = Profile(
- core = True,
- label = toUnicode(qual.label),
- order = order
- )
- db.add(prof)
+ for quality in qualities:
+ contains_score = self.containsTagScore(quality, words, cur_file)
+ threedscore = self.contains3D(quality, threed_words, cur_file) if quality.get('allow_3d') else (0, None)
- profile_type = ProfileType(
- quality = qual,
- profile = prof,
- finish = True,
- order = 0
- )
- prof.types.append(profile_type)
+ self.calcScore(score, quality, contains_score, threedscore, penalty = contains_score)
- order += 1
+ size_scores = []
+ for quality in qualities:
- db.commit()
+ # Evaluate score based on size
+ size_score = self.guessSizeScore(quality, size = size)
+ loose_score = self.guessLooseScore(quality, extra = extra)
- time.sleep(0.3) # Wait a moment
+ if size_score > 0:
+ size_scores.append(quality)
- return True
+ self.calcScore(score, quality, size_score + loose_score)
- def guess(self, files, extra = {}):
+ # Add additional size score if only 1 size validated
+ if len(size_scores) == 1:
+ self.calcScore(score, size_scores[0], 7)
+ del size_scores
- # Create hash for cache
- hash = md5(str([f.replace('.' + getExt(f), '') for f in files]))
- cached = self.getCache(hash)
- if cached and extra is {}: return cached
+ # Return nothing if all scores are <= 0
+ has_non_zero = 0
+ for s in score:
+ if score[s]['score'] > 0:
+ has_non_zero += 1
- for cur_file in files:
- size = (os.path.getsize(cur_file) / 1024 / 1024) if os.path.isfile(cur_file) else 0
- words = re.split('\W+', cur_file.lower())
+ if not has_non_zero:
+ return None
+
+ heighest_quality = max(score, key = lambda p: score[p]['score'])
+ if heighest_quality:
+ for quality in qualities:
+ if quality.get('identifier') == heighest_quality:
+ quality['is_3d'] = False
+ if score[heighest_quality].get('3d'):
+ quality['is_3d'] = True
+ return self.setCache(cache_key, quality)
- for quality in self.all():
+ return None
- # Check tags
- if quality['identifier'] in words:
- log.debug('Found via identifier "%s" in %s', (quality['identifier'], cur_file))
- return self.setCache(hash, quality)
+ def containsTagScore(self, quality, words, cur_file = ''):
+ cur_file = ss(cur_file)
+ score = 0.0
- if list(set(quality.get('alternative', [])) & set(words)):
- log.debug('Found %s via alt %s in %s', (quality['identifier'], quality.get('alternative'), cur_file))
- return self.setCache(hash, quality)
+ extension = words[-1]
+ words = words[:-1]
- for tag in quality.get('tags', []):
- if isinstance(tag, tuple) and '.'.join(tag) in '.'.join(words):
- log.debug('Found %s via tag %s in %s', (quality['identifier'], quality.get('tags'), cur_file))
- return self.setCache(hash, quality)
+ points = {
+ 'identifier': 25,
+ 'label': 25,
+ 'alternative': 20,
+ 'tags': 11,
+ 'ext': 5,
+ }
- if list(set(quality.get('tags', [])) & set(words)):
- log.debug('Found %s via tag %s in %s', (quality['identifier'], quality.get('tags'), cur_file))
- return self.setCache(hash, quality)
+ scored_on = []
- # Try again with loose testing
- quality = self.guessLoose(hash, extra = extra)
- if quality:
- return self.setCache(hash, quality)
+ # Check alt and tags
+ for tag_type in ['identifier', 'alternative', 'tags', 'label']:
+ qualities = quality.get(tag_type, [])
+ qualities = [qualities] if isinstance(qualities, (str, unicode)) else qualities
- log.debug('Could not identify quality for: %s', files)
- return None
+ for alt in qualities:
+ if isinstance(alt, tuple):
+ if len(set(words) & set(alt)) == len(alt):
+ log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
+ score += points.get(tag_type)
+
+ if isinstance(alt, (str, unicode)) and ss(alt.lower()) in words and ss(alt.lower()) not in scored_on:
+ log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
+ score += points.get(tag_type)
+
+ # Don't score twice on same tag
+ scored_on.append(ss(alt).lower())
+
+ # Check extention
+ for ext in quality.get('ext', []):
+ if ext == extension:
+ log.debug('Found %s with .%s extension in %s', (quality['identifier'], ext, cur_file))
+ score += points['ext']
+
+ return score
+
+ def contains3D(self, quality, words, cur_file = ''):
+ cur_file = ss(cur_file)
+
+ for key in self.threed_tags:
+ tags = self.threed_tags.get(key, [])
- def guessLoose(self, hash, extra):
+ for tag in tags:
+ if isinstance(tag, tuple):
+ if len(set(words) & set(tag)) == len(tag):
+ log.debug('Found %s in %s', (tag, cur_file))
+ return 1, key
+ elif tag in words:
+ log.debug('Found %s in %s', (tag, cur_file))
+ return 1, key
- for quality in self.all():
+ return 0, None
+
+ def guessLooseScore(self, quality, extra = None):
+
+ score = 0
+
+ if extra:
# Check width resolution, range 20
- if (quality.get('width', 720) - 20) <= extra.get('resolution_width', 0) <= (quality.get('width', 720) + 20):
- log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width', 720), extra.get('resolution_width', 0)))
- return self.setCache(hash, quality)
+ if quality.get('width') and (quality.get('width') - 20) <= extra.get('resolution_width', 0) <= (quality.get('width') + 20):
+ log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width'), extra.get('resolution_width', 0)))
+ score += 10
# Check height resolution, range 20
- if (quality.get('height', 480) - 20) <= extra.get('resolution_height', 0) <= (quality.get('height', 480) + 20):
- log.debug('Found %s via resolution_height: %s == %s', (quality['identifier'], quality.get('height', 480), extra.get('resolution_height', 0)))
- return self.setCache(hash, quality)
+ if quality.get('height') and (quality.get('height') - 20) <= extra.get('resolution_height', 0) <= (quality.get('height') + 20):
+ log.debug('Found %s via resolution_height: %s == %s', (quality['identifier'], quality.get('height'), extra.get('resolution_height', 0)))
+ score += 5
+
+ if quality.get('identifier') == 'dvdrip' and 480 <= extra.get('resolution_width', 0) <= 720:
+ log.debug('Add point for correct dvdrip resolutions')
+ score += 1
+
+ return score
+
+
+ def guessSizeScore(self, quality, size = None):
+
+ score = 0
+
+ if size:
+
+ size = tryFloat(size)
+ size_min = tryFloat(quality['size_min'])
+ size_max = tryFloat(quality['size_max'])
+
+ if size_min <= size <= size_max:
+ log.debug('Found %s via release size: %s MB < %s MB < %s MB', (quality['identifier'], size_min, size, size_max))
+
+ proc_range = size_max - size_min
+ size_diff = size - size_min
+ size_proc = (size_diff / proc_range)
+
+ median_diff = quality['median_size'] - size_min
+ median_proc = (median_diff / proc_range)
+
+ max_points = 8
+ score += ceil(max_points - (fabs(size_proc - median_proc) * max_points))
+ else:
+ score -= 5
+
+ return score
+
+ def calcScore(self, score, quality, add_score, threedscore = (0, None), penalty = 0):
+
+ score[quality['identifier']]['score'] += add_score
+
+ threedscore, threedtag = threedscore
+ if threedscore and threedtag:
+ if threedscore not in score[quality['identifier']]['3d']:
+ score[quality['identifier']]['3d'][threedtag] = 0
+
+ score[quality['identifier']]['3d'][threedtag] += threedscore
+
+ # Set order for allow calculation (and cache)
+ if not self.cached_order:
+ self.cached_order = {}
+ for q in self.qualities:
+ self.cached_order[q.get('identifier')] = self.qualities.index(q)
+
+ if penalty and add_score != 0:
+ for allow in quality.get('allow', []):
+ score[allow]['score'] -= ((penalty * 2) if self.cached_order[allow] < self.cached_order[quality['identifier']] else penalty) * 2
+
+ # Give panelty for all other qualities
+ for q in self.qualities:
+ if quality.get('identifier') != q.get('identifier') and score.get(q.get('identifier')):
+ score[q.get('identifier')]['score'] -= 1
+
+ def isFinish(self, quality, profile, release_age = 0):
+ if not isinstance(profile, dict) or not profile.get('qualities'):
+ # No profile so anything (scanned) is good enough
+ return True
+
+ try:
+ index = [i for i, identifier in enumerate(profile['qualities']) if identifier == quality['identifier'] and bool(profile['3d'][i] if profile.get('3d') else False) == bool(quality.get('is_3d', False))][0]
+
+ if index == 0 or (profile['finish'][index] and int(release_age) >= int(profile.get('stop_after', [0])[0])):
+ return True
+
+ return False
+ except:
+ return False
+
+ def isHigher(self, quality, compare_with, profile = None):
+ if not isinstance(profile, dict) or not profile.get('qualities'):
+ profile = fireEvent('profile.default', single = True)
+
+ # Try to find quality in profile, if not found: a quality we do not want is lower than anything else
+ try:
+ quality_order = [i for i, identifier in enumerate(profile['qualities']) if identifier == quality['identifier'] and bool(profile['3d'][i] if profile.get('3d') else 0) == bool(quality.get('is_3d', 0))][0]
+ except:
+ log.debug('Quality %s not found in profile identifiers %s', (quality['identifier'] + (' 3D' if quality.get('is_3d', 0) else ''), \
+ [identifier + (' 3D' if (profile['3d'][i] if profile.get('3d') else 0) else '') for i, identifier in enumerate(profile['qualities'])]))
+ return 'lower'
+
+ # Try to find compare quality in profile, if not found: anything is higher than a not wanted quality
+ try:
+ compare_order = [i for i, identifier in enumerate(profile['qualities']) if identifier == compare_with['identifier'] and bool(profile['3d'][i] if profile.get('3d') else 0) == bool(compare_with.get('is_3d', 0))][0]
+ except:
+ log.debug('Compare quality %s not found in profile identifiers %s', (compare_with['identifier'] + (' 3D' if compare_with.get('is_3d', 0) else ''), \
+ [identifier + (' 3D' if (profile['3d'][i] if profile.get('3d') else 0) else '') for i, identifier in enumerate(profile['qualities'])]))
+ return 'higher'
+
+ # Note to self: a lower number means higher quality
+ if quality_order > compare_order:
+ return 'lower'
+ elif quality_order == compare_order:
+ return 'equal'
+ else:
+ return 'higher'
+
+ def doTest(self):
+
+ tests = {
+ 'Movie Name (1999)-DVD-Rip.avi': {'size': 700, 'quality': 'dvdrip'},
+ 'Movie Name 1999 720p Bluray.mkv': {'size': 4200, 'quality': '720p'},
+ 'Movie Name 1999 BR-Rip 720p.avi': {'size': 1000, 'quality': 'brrip'},
+ 'Movie Name 1999 720p Web Rip.avi': {'size': 1200, 'quality': 'scr'},
+ 'Movie Name 1999 Web DL.avi': {'size': 800, 'quality': 'brrip'},
+ 'Movie.Name.1999.1080p.WEBRip.H264-Group': {'size': 1500, 'quality': 'scr'},
+ 'Movie.Name.1999.DVDRip-Group': {'size': 750, 'quality': 'dvdrip'},
+ 'Movie.Name.1999.DVD-Rip-Group': {'size': 700, 'quality': 'dvdrip'},
+ 'Movie.Name.1999.DVD-R-Group': {'size': 4500, 'quality': 'dvdr'},
+ 'Movie.Name.Camelie.1999.720p.BluRay.x264-Group': {'size': 5500, 'quality': '720p'},
+ 'Movie.Name.2008.German.DL.AC3.1080p.BluRay.x264-Group': {'size': 8500, 'extra': {'resolution_width': 1920, 'resolution_height': 1080} , 'quality': '1080p'},
+ 'Movie.Name.2004.GERMAN.AC3D.DL.1080p.BluRay.x264-Group': {'size': 8000, 'quality': '1080p'},
+ 'Movie.Name.2013.BR-Disk-Group.iso': {'size': 48000, 'quality': 'bd50'},
+ 'Movie.Name.2013.2D+3D.BR-Disk-Group.iso': {'size': 52000, 'quality': 'bd50', 'is_3d': True},
+ 'Movie.Rising.Name.Girl.2011.NTSC.DVD9-GroupDVD': {'size': 7200, 'quality': 'dvdr'},
+ 'Movie Name (2013) 2D + 3D': {'size': 49000, 'quality': 'bd50', 'is_3d': True},
+ 'Movie Monuments 2013 BrRip 1080p': {'size': 1800, 'quality': 'brrip'},
+ 'Movie Monuments 2013 BrRip 720p': {'size': 1300, 'quality': 'brrip'},
+ 'The.Movie.2014.3D.1080p.BluRay.AVC.DTS-HD.MA.5.1-GroupName': {'size': 30000, 'quality': 'bd50', 'is_3d': True},
+ '/home/namehou/Movie Monuments (2012)/Movie Monuments.mkv': {'size': 5500, 'quality': '720p', 'is_3d': False},
+ '/home/namehou/Movie Monuments (2012)/Movie Monuments Full-OU.mkv': {'size': 5500, 'quality': '720p', 'is_3d': True},
+ '/home/namehou/Movie Monuments (2013)/Movie Monuments.mkv': {'size': 10000, 'quality': '1080p', 'is_3d': False},
+ '/home/namehou/Movie Monuments (2013)/Movie Monuments Full-OU.mkv': {'size': 10000, 'quality': '1080p', 'is_3d': True},
+ '/volume1/Public/3D/Moviename/Moviename (2009).3D.SBS.ts': {'size': 7500, 'quality': '1080p', 'is_3d': True},
+ '/volume1/Public/Moviename/Moviename (2009).ts': {'size': 7500, 'quality': '1080p'},
+ '/movies/BluRay HDDVD H.264 MKV 720p EngSub/QuiQui le fou (criterion collection #123, 1915)/QuiQui le fou (1915) 720p x264 BluRay.mkv': {'size': 5500, 'quality': '720p'},
+ 'C:\\movies\QuiQui le fou (collection #123, 1915)\QuiQui le fou (1915) 720p x264 BluRay.mkv': {'size': 5500, 'quality': '720p'},
+ 'C:\\movies\QuiQui le fou (collection #123, 1915)\QuiQui le fou (1915) half-sbs 720p x264 BluRay.mkv': {'size': 5500, 'quality': '720p', 'is_3d': True},
+ 'Moviename 2014 720p HDCAM XviD DualAudio': {'size': 4000, 'quality': 'cam'},
+ 'Moviename (2014) - 720p CAM x264': {'size': 2250, 'quality': 'cam'},
+ 'Movie Name (2014).mp4': {'size': 750, 'quality': 'brrip'},
+ 'Moviename.2014.720p.R6.WEB-DL.x264.AC3-xyz': {'size': 750, 'quality': 'r5'},
+ 'Movie name 2014 New Source 720p HDCAM x264 AC3 xyz': {'size': 750, 'quality': 'cam'},
+ 'Movie.Name.2014.720p.HD.TS.AC3.x264': {'size': 750, 'quality': 'ts'},
+ 'Movie.Name.2014.1080p.HDrip.x264.aac-ReleaseGroup': {'size': 7000, 'quality': 'brrip'},
+ 'Movie.Name.2014.HDCam.Chinese.Subs-ReleaseGroup': {'size': 15000, 'quality': 'cam'},
+ 'Movie Name 2014 HQ DVDRip X264 AC3 (bla)': {'size': 0, 'quality': 'dvdrip'},
+ 'Movie Name1 (2012).mkv': {'size': 4500, 'quality': '720p'},
+ 'Movie Name (2013).mkv': {'size': 8500, 'quality': '1080p'},
+ 'Movie Name (2014).mkv': {'size': 4500, 'quality': '720p', 'extra': {'titles': ['Movie Name 2014 720p Bluray']}},
+ 'Movie Name (2015).mkv': {'size': 500, 'quality': '1080p', 'extra': {'resolution_width': 1920}},
+ 'Movie Name (2015).mp4': {'size': 6500, 'quality': 'brrip'},
+ 'Movie Name.2014.720p Web-Dl Aac2.0 h264-ReleaseGroup': {'size': 3800, 'quality': 'brrip'},
+ 'Movie Name.2014.720p.WEBRip.x264.AC3-ReleaseGroup': {'size': 3000, 'quality': 'scr'},
+ 'Movie.Name.2014.1080p.HDCAM.-.ReleaseGroup': {'size': 5300, 'quality': 'cam'},
+ 'Movie.Name.2014.720p.HDSCR.4PARTS.MP4.AAC.ReleaseGroup': {'size': 2401, 'quality': 'scr'},
+ 'Movie.Name.2014.720p.BluRay.x264-ReleaseGroup': {'size': 10300, 'quality': '720p'},
+ 'Movie.Name.2014.720.Bluray.x264.DTS-ReleaseGroup': {'size': 9700, 'quality': '720p'},
+ }
+
+ correct = 0
+ for name in tests:
+ test_quality = self.guess(files = [name], extra = tests[name].get('extra', None), size = tests[name].get('size', None), use_cache = False) or {}
+ success = test_quality.get('identifier') == tests[name]['quality'] and test_quality.get('is_3d') == tests[name].get('is_3d', False)
+ if not success:
+ log.error('%s failed check, thinks it\'s "%s" expecting "%s"', (name,
+ test_quality.get('identifier') + (' 3D' if test_quality.get('is_3d') else ''),
+ tests[name]['quality'] + (' 3D' if tests[name].get('is_3d') else '')
+ ))
+
+ correct += success
+
+ if correct == len(tests):
+ log.info('Quality test successful')
+ return True
+ else:
+ log.error('Quality test failed: %s out of %s succeeded', (correct, len(tests)))
- if 480 <= extra.get('resolution_width', 0) <= 720:
- log.debug('Found as dvdrip')
- return self.setCache(hash, self.single('dvdrip'))
- return None
diff --git a/couchpotato/core/plugins/quality/static/quality.css b/couchpotato/core/plugins/quality/static/quality.css
deleted file mode 100644
index f71f007e8c..0000000000
--- a/couchpotato/core/plugins/quality/static/quality.css
+++ /dev/null
@@ -1,26 +0,0 @@
-.group_sizes {
-
-}
-
- .group_sizes .head {
- font-weight: bold;
- }
-
- .group_sizes .ctrlHolder {
- padding-top: 4px !important;
- padding-bottom: 4px !important;
- font-size: 12px;
- }
-
- .group_sizes .label {
- max-width: 120px;
- }
-
- .group_sizes .min, .group_sizes .max {
- text-align: center;
- width: 50px;
- max-width: 50px;
- margin: 0 5px !important;
- padding: 0 3px;
- display: inline-block;
- }
\ No newline at end of file
diff --git a/couchpotato/core/plugins/quality/static/quality.js b/couchpotato/core/plugins/quality/static/quality.js
index bd2ff2acbc..60a24498e2 100644
--- a/couchpotato/core/plugins/quality/static/quality.js
+++ b/couchpotato/core/plugins/quality/static/quality.js
@@ -8,40 +8,47 @@ var QualityBase = new Class({
self.qualities = data.qualities;
- self.profiles = []
+ self.profiles_list = null;
+ self.profiles = [];
Array.each(data.profiles, self.createProfilesClass.bind(self));
- App.addEvent('load', self.addSettings.bind(self))
+ App.addEvent('loadSettings', self.addSettings.bind(self));
},
getProfile: function(id){
return this.profiles.filter(function(profile){
- return profile.data.id == id
- }).pick()
+ return profile.data._id == id;
+ }).pick();
},
// Hide items when getting profiles
getActiveProfiles: function(){
return Array.filter(this.profiles, function(profile){
- return !profile.data.hide
+ return !profile.data.hide;
});
},
- getQuality: function(id){
- return this.qualities.filter(function(q){
- return q.id == id;
- }).pick();
+ getQuality: function(identifier){
+ try {
+ return this.qualities.filter(function(q){
+ return q.identifier == identifier;
+ }).pick();
+ }
+ catch(e){}
+
+ return {};
},
addSettings: function(){
var self = this;
- self.settings = App.getPage('Settings')
+ self.settings = App.getPage('Settings');
self.settings.addEvent('create', function(){
var tab = self.settings.createSubTab('profile', {
'label': 'Quality',
- 'name': 'profile'
+ 'name': 'profile',
+ 'subtab_label': 'Qualities'
}, self.settings.tabs.searcher ,'searcher');
self.tab = tab.tab;
@@ -51,7 +58,7 @@ var QualityBase = new Class({
self.createProfileOrdering();
self.createSizes();
- })
+ });
},
@@ -61,7 +68,7 @@ var QualityBase = new Class({
createProfiles: function(){
var self = this;
- var non_core_profiles = Array.filter(self.profiles, function(profile){ return !profile.isCore() });
+ var non_core_profiles = Array.filter(self.profiles, function(profile){ return !profile.isCore(); });
var count = non_core_profiles.length;
self.settings.createGroup({
@@ -74,7 +81,7 @@ var QualityBase = new Class({
'events': {
'click': function(){
var profile = self.createProfilesClass();
- $(profile).inject(self.profile_container)
+ $(profile).inject(self.profile_container);
}
}
})
@@ -82,7 +89,7 @@ var QualityBase = new Class({
// Add profiles, that aren't part of the core (for editing)
Array.each(non_core_profiles, function(profile){
- $(profile).inject(self.profile_container)
+ $(profile).inject(self.profile_container);
});
},
@@ -90,9 +97,9 @@ var QualityBase = new Class({
createProfilesClass: function(data){
var self = this;
- var data = data || {'id': randomString()}
- var profile = new Profile(data)
- self.profiles.include(profile)
+ data = data || {'id': randomString()};
+ var profile = new Profile(data);
+ self.profiles.include(profile);
return profile;
},
@@ -100,23 +107,23 @@ var QualityBase = new Class({
createProfileOrdering: function(){
var self = this;
- var profile_list;
- var group = self.settings.createGroup({
- 'label': 'Profile Defaults'
- }).adopt(
+ self.settings.createGroup({
+ 'label': 'Profile Defaults',
+ 'description': '(Needs refresh \'' +(App.isMac() ? 'CMD+R' : 'F5')+ '\' after editing)'
+ }).grab(
new Element('.ctrlHolder#profile_ordering').adopt(
new Element('label[text=Order]'),
- profile_list = new Element('ul'),
+ self.profiles_list = new Element('ul'),
new Element('p.formHint', {
'html': 'Change the order the profiles are in the dropdown list. Uncheck to hide it completely. First one will be default.'
})
)
- ).inject(self.content)
+ ).inject(self.content);
Array.each(self.profiles, function(profile){
var check;
- new Element('li', {'data-id': profile.data.id}).adopt(
- check = new Element('input.inlay[type=checkbox]', {
+ new Element('li', {'data-id': profile.data._id}).adopt(
+ check = new Element('input[type=checkbox]', {
'checked': !profile.data.hide,
'events': {
'change': self.saveProfileOrdering.bind(self)
@@ -125,30 +132,35 @@ var QualityBase = new Class({
new Element('span.profile_label', {
'text': profile.data.label
}),
- new Element('span.handle')
- ).inject(profile_list);
-
- new Form.Check(check);
-
+ new Element('span.handle.icon-handle')
+ ).inject(self.profiles_list);
});
// Sortable
- self.profile_sortable = new Sortables(profile_list, {
+ var sorted_changed = false;
+ self.profile_sortable = new Sortables(self.profiles_list, {
'revert': true,
- 'handle': '',
+ 'handle': '.handle',
'opacity': 0.5,
- 'onComplete': self.saveProfileOrdering.bind(self)
+ 'onSort': function(){
+ sorted_changed = true;
+ },
+ 'onComplete': function(){
+ if(sorted_changed){
+ self.saveProfileOrdering();
+ sorted_changed = false;
+ }
+ }
});
},
saveProfileOrdering: function(){
- var self = this;
-
- var ids = [];
- var hidden = [];
+ var self = this,
+ ids = [],
+ hidden = [];
- self.profile_sortable.list.getElements('li').each(function(el, nr){
+ self.profiles_list.getElements('li').each(function(el, nr){
ids.include(el.get('data-id'));
hidden[nr] = +!el.getElement('input[type=checkbox]').get('checked');
});
@@ -173,35 +185,34 @@ var QualityBase = new Class({
'description': 'Edit the minimal and maximum sizes (in MB) for each quality.',
'advanced': true,
'name': 'sizes'
- }).inject(self.content)
-
+ }).inject(self.content);
new Element('div.item.head.ctrlHolder').adopt(
new Element('span.label', {'text': 'Quality'}),
new Element('span.min', {'text': 'Min'}),
new Element('span.max', {'text': 'Max'})
- ).inject(group)
+ ).inject(group);
Array.each(self.qualities, function(quality){
new Element('div.ctrlHolder.item').adopt(
new Element('span.label', {'text': quality.label}),
- new Element('input.min.inlay[type=text]', {
+ new Element('input.min[type=text]', {
'value': quality.size_min,
'events': {
'keyup': function(e){
- self.changeSize(quality.identifier, 'size_min', e.target.get('value'))
+ self.changeSize(quality.identifier, 'size_min', e.target.get('value'));
}
}
}),
- new Element('input.max.inlay[type=text]', {
+ new Element('input.max[type=text]', {
'value': quality.size_max,
'events': {
'keyup': function(e){
- self.changeSize(quality.identifier, 'size_max', e.target.get('value'))
+ self.changeSize(quality.identifier, 'size_max', e.target.get('value'));
}
}
})
- ).inject(group)
+ ).inject(group);
});
},
@@ -210,9 +221,9 @@ var QualityBase = new Class({
changeSize: function(identifier, type, value){
var self = this;
- if(self.size_timer[identifier + type]) clearTimeout(self.size_timer[identifier + type]);
+ if(self.size_timer[identifier + type]) clearRequestTimeout(self.size_timer[identifier + type]);
- self.size_timer[identifier + type] = (function(){
+ self.size_timer[identifier + type] = requestTimeout(function(){
Api.request('quality.size.save', {
'data': {
'identifier': identifier,
@@ -220,7 +231,7 @@ var QualityBase = new Class({
'value': value
}
});
- }).delay(300)
+ }, 300);
}
diff --git a/couchpotato/core/plugins/quality/static/quality.scss b/couchpotato/core/plugins/quality/static/quality.scss
new file mode 100644
index 0000000000..c2aa9f99b7
--- /dev/null
+++ b/couchpotato/core/plugins/quality/static/quality.scss
@@ -0,0 +1,19 @@
+@import "_mixins";
+
+.group_sizes {
+
+ .item {
+ .label {
+ min-width: 150px;
+ }
+
+ .min, .max {
+ display: inline-block;
+ width: 70px !important;
+ min-width: 0 !important;
+ margin-right: $padding/2;
+ text-align: center;
+ }
+ }
+
+}
diff --git a/couchpotato/core/plugins/release/__init__.py b/couchpotato/core/plugins/release/__init__.py
index b6a667c219..e6e60c4bbd 100644
--- a/couchpotato/core/plugins/release/__init__.py
+++ b/couchpotato/core/plugins/release/__init__.py
@@ -1,6 +1,5 @@
from .main import Release
-def start():
- return Release()
-config = []
+def autoload():
+ return Release()
diff --git a/couchpotato/core/plugins/release/index.py b/couchpotato/core/plugins/release/index.py
new file mode 100644
index 0000000000..8265fe332d
--- /dev/null
+++ b/couchpotato/core/plugins/release/index.py
@@ -0,0 +1,64 @@
+from hashlib import md5
+
+from CodernityDB.hash_index import HashIndex
+from CodernityDB.tree_index import TreeBasedIndex
+
+
+class ReleaseIndex(TreeBasedIndex):
+ _version = 1
+
+ def __init__(self, *args, **kwargs):
+ kwargs['key_format'] = '32s'
+ super(ReleaseIndex, self).__init__(*args, **kwargs)
+
+ def make_key(self, key):
+ return key
+
+ def make_key_value(self, data):
+ if data.get('_t') == 'release' and data.get('media_id'):
+ return data['media_id'], None
+
+
+class ReleaseStatusIndex(TreeBasedIndex):
+ _version = 1
+
+ def __init__(self, *args, **kwargs):
+ kwargs['key_format'] = '32s'
+ super(ReleaseStatusIndex, self).__init__(*args, **kwargs)
+
+ def make_key(self, key):
+ return md5(key).hexdigest()
+
+ def make_key_value(self, data):
+ if data.get('_t') == 'release' and data.get('status'):
+ return md5(data.get('status')).hexdigest(), {'media_id': data.get('media_id')}
+
+
+class ReleaseIDIndex(HashIndex):
+ _version = 1
+
+ def __init__(self, *args, **kwargs):
+ kwargs['key_format'] = '32s'
+ super(ReleaseIDIndex, self).__init__(*args, **kwargs)
+
+ def make_key(self, key):
+ return md5(key).hexdigest()
+
+ def make_key_value(self, data):
+ if data.get('_t') == 'release' and data.get('identifier'):
+ return md5(data.get('identifier')).hexdigest(), {'media_id': data.get('media_id')}
+
+
+class ReleaseDownloadIndex(HashIndex):
+ _version = 2
+
+ def __init__(self, *args, **kwargs):
+ kwargs['key_format'] = '32s'
+ super(ReleaseDownloadIndex, self).__init__(*args, **kwargs)
+
+ def make_key(self, key):
+ return md5(key.lower()).hexdigest()
+
+ def make_key_value(self, data):
+ if data.get('_t') == 'release' and data.get('download_info') and data['download_info']['id'] and data['download_info']['downloader']:
+ return md5(('%s-%s' % (data['download_info']['downloader'], data['download_info']['id'])).lower()).hexdigest(), None
diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py
index 02843f855a..8320a247d5 100644
--- a/couchpotato/core/plugins/release/main.py
+++ b/couchpotato/core/plugins/release/main.py
@@ -1,24 +1,34 @@
-from couchpotato import get_session
+from inspect import ismethod, isfunction
+import os
+import time
+import traceback
+
+from CodernityDB.database import RecordDeleted, RecordNotFound
+from couchpotato import md5, get_db
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent
-from couchpotato.core.helpers.encoding import ss
-from couchpotato.core.helpers.request import getParam, jsonified
+from couchpotato.core.helpers.encoding import toUnicode, sp
+from couchpotato.core.helpers.variable import getTitle, tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
-from couchpotato.core.plugins.scanner.main import Scanner
-from couchpotato.core.settings.model import File, Release as Relea, Movie
-from sqlalchemy.sql.expression import and_, or_
-import os
+from .index import ReleaseIndex, ReleaseStatusIndex, ReleaseIDIndex, ReleaseDownloadIndex
+from couchpotato.environment import Env
+
log = CPLog(__name__)
class Release(Plugin):
- def __init__(self):
- addEvent('release.add', self.add)
+ _database = {
+ 'release': ReleaseIndex,
+ 'release_status': ReleaseStatusIndex,
+ 'release_identifier': ReleaseIDIndex,
+ 'release_download': ReleaseDownloadIndex
+ }
- addApiView('release.download', self.download, docs = {
+ def __init__(self):
+ addApiView('release.manual_download', self.manualDownload, docs = {
'desc': 'Send a release manually to the downloaders',
'params': {
'id': {'type': 'id', 'desc': 'ID of the release object in release-table'}
@@ -37,160 +47,522 @@ def __init__(self):
}
})
+ addEvent('release.add', self.add)
+ addEvent('release.download', self.download)
+ addEvent('release.try_download_result', self.tryDownloadResult)
+ addEvent('release.create_from_search', self.createFromSearch)
addEvent('release.delete', self.delete)
addEvent('release.clean', self.clean)
+ addEvent('release.update_status', self.updateStatus)
+ addEvent('release.with_status', self.withStatus)
+ addEvent('release.for_media', self.forMedia)
- def add(self, group):
- db = get_session()
-
- identifier = '%s.%s.%s' % (group['library']['identifier'], group['meta_data'].get('audio', 'unknown'), group['meta_data']['quality']['identifier'])
-
- # Add movie
- done_status = fireEvent('status.get', 'done', single = True)
- movie = db.query(Movie).filter_by(library_id = group['library'].get('id')).first()
- if not movie:
- movie = Movie(
- library_id = group['library'].get('id'),
- profile_id = 0,
- status_id = done_status.get('id')
- )
- db.add(movie)
- db.commit()
-
- # Add Release
- snatched_status = fireEvent('status.get', 'snatched', single = True)
- rel = db.query(Relea).filter(
- or_(
- Relea.identifier == identifier,
- and_(Relea.identifier.startswith(group['library']['identifier']), Relea.status_id == snatched_status.get('id'))
- )
- ).first()
- if not rel:
- rel = Relea(
- identifier = identifier,
- movie = movie,
- quality_id = group['meta_data']['quality'].get('id'),
- status_id = done_status.get('id')
- )
- db.add(rel)
- db.commit()
-
- # Add each file type
- for type in group['files']:
- for cur_file in group['files'][type]:
- added_file = self.saveFile(cur_file, type = type, include_media_info = type is 'movie')
- try:
- added_file = db.query(File).filter_by(id = added_file.get('id')).one()
- rel.files.append(added_file)
- db.commit()
- except Exception, e:
- log.debug('Failed to attach "%s" to release: %s', (cur_file, e))
+ # Clean releases that didn't have activity in the last week
+ addEvent('app.load', self.cleanDone, priority = 1000)
+ fireEvent('schedule.interval', 'movie.clean_releases', self.cleanDone, hours = 12)
- fireEvent('movie.restatus', movie.id)
+ def cleanDone(self):
+ log.debug('Removing releases from dashboard')
- return True
+ now = time.time()
+ week = 604800
+ db = get_db()
- def saveFile(self, filepath, type = 'unknown', include_media_info = False):
+ # Get (and remove) parentless releases
+ releases = db.all('release', with_doc = False)
+ media_exist = []
+ reindex = 0
+ for release in releases:
+ if release.get('key') in media_exist:
+ continue
- properties = {}
+ try:
- # Get media info for files
- if include_media_info:
- properties = {}
+ try:
+ doc = db.get('id', release.get('_id'))
+ except RecordDeleted:
+ reindex += 1
+ continue
- # Check database and update/insert if necessary
- return fireEvent('file.add', path = filepath, part = fireEvent('scanner.partnumber', file, single = True), type_tuple = Scanner.file_types.get(type), properties = properties, single = True)
+ db.get('id', release.get('key'))
+ media_exist.append(release.get('key'))
- def deleteView(self):
+ try:
+ if doc.get('status') == 'ignore':
+ doc['status'] = 'ignored'
+ db.update(doc)
+ except:
+ log.error('Failed fixing mis-status tag: %s', traceback.format_exc())
+ except ValueError:
+ fireEvent('database.delete_corrupted', release.get('key'), traceback_error = traceback.format_exc(0))
+ reindex += 1
+ except RecordDeleted:
+ db.delete(doc)
+ log.debug('Deleted orphaned release: %s', doc)
+ reindex += 1
+ except:
+ log.debug('Failed cleaning up orphaned releases: %s', traceback.format_exc())
+
+ if reindex > 0:
+ db.reindex()
+
+ del media_exist
+
+ # get movies last_edit more than a week ago
+ medias = fireEvent('media.with_status', ['done', 'active'], single = True)
+
+ for media in medias:
+ if media.get('last_edit', 0) > (now - week):
+ continue
+
+ for rel in self.forMedia(media['_id']):
+
+ # Remove all available releases
+ if rel['status'] in ['available']:
+ self.delete(rel['_id'])
+
+ # Set all snatched and downloaded releases to ignored to make sure they are ignored when re-adding the media
+ elif rel['status'] in ['snatched', 'downloaded']:
+ self.updateStatus(rel['_id'], status = 'ignored')
+
+ if 'recent' in media.get('tags', []):
+ fireEvent('media.untag', media.get('_id'), 'recent', single = True)
+
+ def add(self, group, update_info = True, update_id = None):
+
+ try:
+ db = get_db()
+
+ release_identifier = '%s.%s.%s' % (group['identifier'], group['meta_data'].get('audio', 'unknown'), group['meta_data']['quality']['identifier'])
+
+ # Add movie if it doesn't exist
+ try:
+ media = db.get('media', 'imdb-%s' % group['identifier'], with_doc = True)['doc']
+ except:
+ media = fireEvent('movie.add', params = {
+ 'identifier': group['identifier'],
+ 'profile_id': None,
+ }, search_after = False, update_after = update_info, notify_after = False, status = 'done', single = True)
+
+ release = None
+ if update_id:
+ try:
+ release = db.get('id', update_id)
+ release.update({
+ 'identifier': release_identifier,
+ 'last_edit': int(time.time()),
+ 'status': 'done',
+ })
+ except:
+ log.error('Failed updating existing release: %s', traceback.format_exc())
+ else:
+
+ # Add Release
+ if not release:
+ release = {
+ '_t': 'release',
+ 'media_id': media['_id'],
+ 'identifier': release_identifier,
+ 'quality': group['meta_data']['quality'].get('identifier'),
+ 'is_3d': group['meta_data']['quality'].get('is_3d', 0),
+ 'last_edit': int(time.time()),
+ 'status': 'done'
+ }
- release_id = getParam('id')
+ try:
+ r = db.get('release_identifier', release_identifier, with_doc = True)['doc']
+ r['media_id'] = media['_id']
+ except:
+ log.debug('Failed updating release by identifier "%s". Inserting new.', release_identifier)
+ r = db.insert(release)
- return jsonified({
- 'success': self.delete(release_id)
- })
+ # Update with ref and _id
+ release.update({
+ '_id': r['_id'],
+ '_rev': r['_rev'],
+ })
- def delete(self, id):
+ # Empty out empty file groups
+ release['files'] = dict((k, [toUnicode(x) for x in v]) for k, v in group['files'].items() if v)
+ db.update(release)
- db = get_session()
+ fireEvent('media.restatus', media['_id'], allowed_restatus = ['done'], single = True)
- rel = db.query(Relea).filter_by(id = id).first()
- if rel:
- rel.delete()
- db.commit()
return True
+ except:
+ log.error('Failed: %s', traceback.format_exc())
return False
- def clean(self, id):
+ def deleteView(self, id = None, **kwargs):
+
+ return {
+ 'success': self.delete(id)
+ }
- db = get_session()
+ def delete(self, release_id):
+
+ try:
+ db = get_db()
+ rel = db.get('id', release_id)
+ db.delete(rel)
+ return True
+ except RecordDeleted:
+ log.debug('Already deleted: %s', release_id)
+ return True
+ except:
+ log.error('Failed: %s', traceback.format_exc())
+
+ return False
- rel = db.query(Relea).filter_by(id = id).first()
- if rel:
- for release_file in rel.files:
- if not os.path.isfile(ss(release_file.path)):
- db.delete(release_file)
- db.commit()
+ def clean(self, release_id):
- if len(rel.files) == 0:
- self.delete(id)
+ try:
+ db = get_db()
+ rel = db.get('id', release_id)
+ raw_files = rel.get('files')
+
+ if len(raw_files) == 0:
+ self.delete(rel['_id'])
+ else:
+
+ files = {}
+ for file_type in raw_files:
+
+ for release_file in raw_files.get(file_type, []):
+ if os.path.isfile(sp(release_file)):
+ if file_type not in files:
+ files[file_type] = []
+ files[file_type].append(release_file)
+
+ rel['files'] = files
+ db.update(rel)
return True
+ except:
+ log.error('Failed: %s', traceback.format_exc())
return False
- def ignore(self):
+ def ignore(self, id = None, **kwargs):
- db = get_session()
- id = getParam('id')
+ db = get_db()
- rel = db.query(Relea).filter_by(id = id).first()
- if rel:
- ignored_status = fireEvent('status.get', 'ignored', single = True)
- available_status = fireEvent('status.get', 'available', single = True)
- rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id')
- db.commit()
+ try:
+ if id:
+ rel = db.get('id', id, with_doc = True)
+ self.updateStatus(id, 'available' if rel['status'] in ['ignored', 'failed'] else 'ignored')
- return jsonified({
- 'success': True
- })
+ return {
+ 'success': True
+ }
+ except:
+ log.error('Failed: %s', traceback.format_exc())
- def download(self):
+ return {
+ 'success': False
+ }
+
+ def manualDownload(self, id = None, **kwargs):
- db = get_session()
- id = getParam('id')
- status_snatched = fireEvent('status.add', 'snatched', single = True)
+ db = get_db()
- rel = db.query(Relea).filter_by(id = id).first()
- if rel:
- item = {}
- for info in rel.info:
- item[info.identifier] = info.value
+ try:
+ release = db.get('id', id)
+ item = release['info']
+ movie = db.get('id', release['media_id'])
+
+ fireEvent('notify.frontend', type = 'release.manual_download', data = True, message = 'Snatching "%s"' % item['name'])
# Get matching provider
provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True)
- if item['type'] != 'torrent_magnet':
- item['download'] = provider.download
+ if item.get('protocol') != 'torrent_magnet':
+ item['download'] = provider.loginDownload if provider.urls.get('login') else provider.download
- success = fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({
- 'profile': {'types': {'quality': {}}},
- 'releases': {'status': {}, 'quality': {}},
- 'library': {'titles': {}, 'files':{}},
- 'files': {}
- }), manual = True, single = True)
+ success = self.download(data = item, media = movie, manual = True)
if success:
- rel.status_id = status_snatched.get('id')
- db.commit()
+ fireEvent('notify.frontend', type = 'release.manual_download', data = True, message = 'Successfully snatched "%s"' % item['name'])
- return jsonified({
- 'success': success
- })
- else:
- log.error('Couldn\'t find release with id: %s', id)
+ return {
+ 'success': success == True
+ }
- return jsonified({
- 'success': False
- })
+ except:
+ log.error('Couldn\'t find release with id: %s: %s', (id, traceback.format_exc()))
+ return {
+ 'success': False
+ }
+
+ def download(self, data, media, manual = False):
+
+ # Test to see if any downloaders are enabled for this type
+ downloader_enabled = fireEvent('download.enabled', manual, data, single = True)
+ if not downloader_enabled:
+ log.info('Tried to download, but none of the "%s" downloaders are enabled or gave an error', data.get('protocol'))
+ return False
+
+ # Download NZB or torrent file
+ filedata = None
+ if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))):
+ try:
+ filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
+ except:
+ log.error('Tried to download, but the "%s" provider gave an error: %s', (data.get('protocol'), traceback.format_exc()))
+ return False
+
+ if filedata == 'try_next':
+ return filedata
+ elif not filedata:
+ return False
+
+ # Send NZB or torrent file to downloader
+ download_result = fireEvent('download', data = data, media = media, manual = manual, filedata = filedata, single = True)
+ if not download_result:
+ log.info('Tried to download, but the "%s" downloader gave an error', data.get('protocol'))
+ return False
+ log.debug('Downloader result: %s', download_result)
+
+ try:
+ db = get_db()
+
+ try:
+ rls = db.get('release_identifier', md5(data['url']), with_doc = True)['doc']
+ except:
+ log.error('No release found to store download information in')
+ return False
+
+ renamer_enabled = Env.setting('enabled', 'renamer')
+
+ # Save download-id info if returned
+ if isinstance(download_result, dict):
+ rls['download_info'] = download_result
+ db.update(rls)
+
+ log_movie = '%s (%s) in %s' % (getTitle(media), media['info'].get('year'), rls['quality'])
+ snatch_message = 'Snatched "%s": %s from %s' % (data.get('name'), log_movie, (data.get('provider', '') + data.get('provider_extra', '')))
+ log.info(snatch_message)
+ fireEvent('%s.snatched' % data['type'], message = snatch_message, data = media)
+
+ # Mark release as snatched
+ if renamer_enabled:
+ self.updateStatus(rls['_id'], status = 'snatched')
+
+ # If renamer isn't used, mark media done if finished or release downloaded
+ else:
+
+ if media['status'] == 'active':
+ profile = db.get('id', media['profile_id'])
+ if fireEvent('quality.isfinish', {'identifier': rls['quality'], 'is_3d': rls.get('is_3d', False)}, profile, single = True):
+ log.info('Renamer disabled, marking media as finished: %s', log_movie)
+
+ # Mark release done
+ self.updateStatus(rls['_id'], status = 'done')
+
+ # Mark media done
+ fireEvent('media.restatus', media['_id'], single = True)
+
+ return True
+
+ # Assume release downloaded
+ self.updateStatus(rls['_id'], status = 'downloaded')
+
+ except:
+ log.error('Failed storing download status: %s', traceback.format_exc())
+ return False
+
+ return True
+
+ def tryDownloadResult(self, results, media, quality_custom):
+
+ wait_for = False
+ let_through = False
+ filtered_results = []
+ minimum_seeders = tryInt(Env.setting('minimum_seeders', section = 'torrent', default = 1))
+
+ # Filter out ignored and other releases we don't want
+ for rel in results:
+
+ if rel['status'] in ['ignored', 'failed']:
+ log.info('Ignored: %s', rel['name'])
+ continue
+
+ if rel['score'] < quality_custom.get('minimum_score'):
+ log.info('Ignored, score "%s" to low, need at least "%s": %s', (rel['score'], quality_custom.get('minimum_score'), rel['name']))
+ continue
+
+ if rel['size'] <= 50:
+ log.info('Ignored, size "%sMB" to low: %s', (rel['size'], rel['name']))
+ continue
+
+ if 'seeders' in rel and rel.get('seeders') < minimum_seeders:
+ log.info('Ignored, not enough seeders, has %s needs %s: %s', (rel.get('seeders'), minimum_seeders, rel['name']))
+ continue
+
+ # If a single release comes through the "wait for", let through all
+ rel['wait_for'] = False
+ if quality_custom.get('index') != 0 and quality_custom.get('wait_for', 0) > 0 and rel.get('age') <= quality_custom.get('wait_for', 0):
+ rel['wait_for'] = True
+ else:
+ let_through = True
+
+ filtered_results.append(rel)
+
+ # Loop through filtered results
+ for rel in filtered_results:
+
+ # Only wait if not a single release is old enough
+ if rel.get('wait_for') and not let_through:
+ log.info('Ignored, waiting %s days: %s', (quality_custom.get('wait_for') - rel.get('age'), rel['name']))
+ wait_for = True
+ continue
+
+ downloaded = fireEvent('release.download', data = rel, media = media, single = True)
+ if downloaded is True:
+ return True
+ elif downloaded != 'try_next':
+ break
+
+ return wait_for
+
+ def createFromSearch(self, search_results, media, quality):
+
+ try:
+ db = get_db()
+
+ found_releases = []
+
+ is_3d = False
+ try: is_3d = quality['custom']['3d']
+ except: pass
+
+ for rel in search_results:
+
+ rel_identifier = md5(rel['url'])
+
+ release = {
+ '_t': 'release',
+ 'identifier': rel_identifier,
+ 'media_id': media.get('_id'),
+ 'quality': quality.get('identifier'),
+ 'is_3d': is_3d,
+ 'status': rel.get('status', 'available'),
+ 'last_edit': int(time.time()),
+ 'info': {}
+ }
+
+ # Add downloader info if provided
+ try:
+ release['download_info'] = rel['download_info']
+ del rel['download_info']
+ except:
+ pass
+
+ try:
+ rls = db.get('release_identifier', rel_identifier, with_doc = True)['doc']
+ except:
+ rls = db.insert(release)
+ rls.update(release)
+
+ # Update info, but filter out functions
+ for info in rel:
+ try:
+ if not isinstance(rel[info], (str, unicode, int, long, float)):
+ continue
+
+ rls['info'][info] = toUnicode(rel[info]) if isinstance(rel[info], (str, unicode)) else rel[info]
+ except:
+ log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc()))
+
+ db.update(rls)
+
+ # Update release in search_results
+ rel['status'] = rls.get('status')
+
+ if rel['status'] == 'available':
+ found_releases.append(rel_identifier)
+
+ return found_releases
+ except:
+ log.error('Failed: %s', traceback.format_exc())
+
+ return []
+
+ def updateStatus(self, release_id, status = None):
+ if not status: return False
+
+ try:
+ db = get_db()
+
+ rel = db.get('id', release_id)
+ if rel and rel.get('status') != status:
+
+ release_name = None
+ if rel.get('files'):
+ for file_type in rel.get('files', {}):
+ if file_type == 'movie':
+ for release_file in rel['files'][file_type]:
+ release_name = os.path.basename(release_file)
+ break
+
+ if not release_name and rel.get('info'):
+ release_name = rel['info'].get('name')
+
+ #update status in Db
+ log.debug('Marking release %s as %s', (release_name, status))
+ rel['status'] = status
+ rel['last_edit'] = int(time.time())
+
+ db.update(rel)
+
+ #Update all movie info as there is no release update function
+ fireEvent('notify.frontend', type = 'release.update_status', data = rel)
+
+ return True
+ except:
+ log.error('Failed: %s', traceback.format_exc())
+
+ return False
+
+ def withStatus(self, status, with_doc = True):
+
+ db = get_db()
+
+ status = list(status if isinstance(status, (list, tuple)) else [status])
+
+ for s in status:
+ for ms in db.get_many('release_status', s):
+ if with_doc:
+ try:
+ doc = db.get('id', ms['_id'])
+ yield doc
+ except RecordNotFound:
+ log.debug('Record not found, skipping: %s', ms['_id'])
+ else:
+ yield ms
+
+ def forMedia(self, media_id):
+
+ db = get_db()
+ raw_releases = db.get_many('release', media_id)
+
+ releases = []
+ for r in raw_releases:
+ try:
+ doc = db.get('id', r.get('_id'))
+ releases.append(doc)
+ except RecordDeleted:
+ pass
+ except (ValueError, EOFError):
+ fireEvent('database.delete_corrupted', r.get('_id'), traceback_error = traceback.format_exc(0))
+
+ releases = sorted(releases, key = lambda k: k.get('info', {}).get('score', 0), reverse = True)
+
+ # Sort based on preferred search method
+ download_preference = self.conf('preferred_method', section = 'searcher')
+ if download_preference != 'both':
+ releases = sorted(releases, key = lambda k: k.get('info', {}).get('protocol', '')[:3], reverse = (download_preference == 'torrent'))
+
+ return releases or []
diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py
new file mode 100755
index 0000000000..fd710d5a41
--- /dev/null
+++ b/couchpotato/core/plugins/renamer.py
@@ -0,0 +1,1488 @@
+import fnmatch
+import os
+import re
+import shutil
+import time
+import traceback
+
+from couchpotato import get_db
+from couchpotato.api import addApiView
+from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
+from couchpotato.core.helpers.encoding import toUnicode, ss, sp
+from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
+ getImdb, link, symlink, tryInt, splitString, fnEscape, isSubFolder, \
+ getIdentifier, randomString, getFreeSpace, getSize
+from couchpotato.core.logger import CPLog
+from couchpotato.core.plugins.base import Plugin
+from couchpotato.environment import Env
+from unrar2 import RarFile
+import six
+from six.moves import filter
+
+
+log = CPLog(__name__)
+
+autoload = 'Renamer'
+
+
+class Renamer(Plugin):
+
+ renaming_started = False
+ checking_snatched = False
+
+ def __init__(self):
+ addApiView('renamer.scan', self.scanView, docs = {
+ 'desc': 'For the renamer to check for new files to rename in a folder',
+ 'params': {
+ 'async': {'desc': 'Optional: Set to 1 if you dont want to fire the renamer.scan asynchronous.'},
+ 'to_folder': {'desc': 'Optional: The folder to move releases to. Leave empty for default folder.'},
+ 'media_folder': {'desc': 'Optional: The folder of the media to scan. Keep empty for default renamer folder.'},
+ 'files': {'desc': 'Optional: Provide the release files if more releases are in the same media_folder, delimited with a \'|\'. Note that no dedicated release folder is expected for releases with one file.'},
+ 'base_folder': {'desc': 'Optional: The folder to find releases in. Leave empty for default folder.'},
+ 'downloader': {'desc': 'Optional: The downloader the release has been downloaded with. \'download_id\' is required with this option.'},
+ 'download_id': {'desc': 'Optional: The nzb/torrent ID of the release in media_folder. \'downloader\' is required with this option.'},
+ 'status': {'desc': 'Optional: The status of the release: \'completed\' (default) or \'seeding\''},
+ },
+ })
+
+ addApiView('renamer.progress', self.getProgress, docs = {
+ 'desc': 'Get the progress of current renamer scan',
+ 'return': {'type': 'object', 'example': """{
+ 'progress': False || True,
+}"""},
+ })
+
+ addEvent('renamer.scan', self.scan)
+ addEvent('renamer.check_snatched', self.checkSnatched)
+
+ addEvent('app.load', self.scan)
+ addEvent('app.load', self.setCrons)
+
+ # Enable / disable interval
+ addEvent('setting.save.renamer.enabled.after', self.setCrons)
+ addEvent('setting.save.renamer.run_every.after', self.setCrons)
+ addEvent('setting.save.renamer.force_every.after', self.setCrons)
+
+ def setCrons(self):
+
+ fireEvent('schedule.remove', 'renamer.check_snatched')
+ if self.isEnabled() and self.conf('run_every') > 0:
+ fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'), single = True)
+
+ fireEvent('schedule.remove', 'renamer.check_snatched_forced')
+ if self.isEnabled() and self.conf('force_every') > 0:
+ fireEvent('schedule.interval', 'renamer.check_snatched_forced', self.scan, hours = self.conf('force_every'), single = True)
+
+ return True
+
+ def getProgress(self, **kwargs):
+ return {
+ 'progress': self.renaming_started
+ }
+
+ def scanView(self, **kwargs):
+
+ async = tryInt(kwargs.get('async', 0))
+ base_folder = kwargs.get('base_folder')
+ media_folder = sp(kwargs.get('media_folder'))
+ to_folder = kwargs.get('to_folder')
+
+ # Backwards compatibility, to be removed after a few versions :)
+ if not media_folder:
+ media_folder = sp(kwargs.get('movie_folder'))
+
+ downloader = kwargs.get('downloader')
+ download_id = kwargs.get('download_id')
+ files = [sp(filename) for filename in splitString(kwargs.get('files'), '|')]
+ status = kwargs.get('status', 'completed')
+
+ release_download = None
+ if not base_folder and media_folder:
+ release_download = {'folder': media_folder}
+
+ if download_id:
+ release_download.update({
+ 'id': download_id,
+ 'downloader': downloader,
+ 'status': status,
+ 'files': files
+ })
+
+ fire_handle = fireEvent if not async else fireEventAsync
+ fire_handle('renamer.scan', base_folder = base_folder, release_download = release_download, to_folder = to_folder)
+
+ return {
+ 'success': True
+ }
+
+ def scan(self, base_folder = None, release_download = None, to_folder = None):
+ if not release_download: release_download = {}
+
+ if self.isDisabled():
+ return
+
+ if self.renaming_started is True:
+ log.info('Renamer is already running, if you see this often, check the logs above for errors.')
+ return
+
+ if not base_folder:
+ base_folder = sp(self.conf('from'))
+
+ from_folder = sp(self.conf('from'))
+
+ if not to_folder:
+ to_folder = sp(self.conf('to'))
+
+ # Get media folder to process
+ media_folder = sp(release_download.get('folder'))
+
+ # Get all folders that should not be processed
+ no_process = [to_folder]
+ cat_list = fireEvent('category.all', single = True) or []
+ no_process.extend([item['destination'] for item in cat_list])
+
+ # Check to see if the no_process folders are inside the "from" folder.
+ if not os.path.isdir(base_folder) or not os.path.isdir(to_folder):
+ log.error('Both the "To" and "From" folder have to exist.')
+ return
+ else:
+ for item in no_process:
+ if isSubFolder(item, base_folder):
+ log.error('To protect your data, the media libraries can\'t be inside of or the same as the "from" folder. "%s" in "%s"', (item, base_folder))
+ return
+
+ # Check to see if the no_process folders are inside the provided media_folder
+ if media_folder and not os.path.isdir(media_folder):
+ log.debug('The provided media folder %s does not exist. Trying to find it in the \'from\' folder.', media_folder)
+
+ # Update to the from folder
+ if len(release_download.get('files', [])) == 1:
+ new_media_folder = sp(from_folder)
+ else:
+ new_media_folder = sp(os.path.join(from_folder, os.path.basename(media_folder)))
+
+ if not os.path.isdir(new_media_folder):
+ log.error('The provided media folder %s does not exist and could also not be found in the \'from\' folder.', media_folder)
+ return
+
+ # Update the files
+ new_files = [os.path.join(new_media_folder, os.path.relpath(filename, media_folder)) for filename in release_download.get('files', [])]
+ if new_files and not os.path.isfile(new_files[0]):
+ log.error('The provided media folder %s does not exist and its files could also not be found in the \'from\' folder.', media_folder)
+ return
+
+ # Update release_download info to the from folder
+ log.debug('Release %s found in the \'from\' folder.', media_folder)
+ release_download['folder'] = new_media_folder
+ release_download['files'] = new_files
+ media_folder = new_media_folder
+
+ if media_folder:
+ for item in no_process:
+ if isSubFolder(item, media_folder):
+ log.error('To protect your data, the media libraries can\'t be inside of or the same as the provided media folder. "%s" in "%s"', (item, media_folder))
+ return
+
+ # Make sure a checkSnatched marked all downloads/seeds as such
+ if not release_download and self.conf('run_every') > 0:
+ self.checkSnatched(fire_scan = False)
+
+ self.renaming_started = True
+
+ # make sure the media folder name is included in the search
+ folder = None
+ files = []
+ if media_folder:
+ log.info('Scanning media folder %s...', media_folder)
+ folder = os.path.dirname(media_folder)
+
+ release_files = release_download.get('files', [])
+ if release_files:
+ files = release_files
+
+ # If there is only one file in the torrent, the downloader did not create a subfolder
+ if len(release_files) == 1:
+ folder = media_folder
+ else:
+ # Get all files from the specified folder
+ try:
+ for root, folders, names in os.walk(media_folder):
+ files.extend([sp(os.path.join(root, name)) for name in names])
+ except:
+ log.error('Failed getting files from %s: %s', (media_folder, traceback.format_exc()))
+
+ db = get_db()
+
+ # Extend the download info with info stored in the downloaded release
+ keep_original = self.moveTypeIsLinked()
+ is_torrent = False
+ if release_download:
+ release_download = self.extendReleaseDownload(release_download)
+ is_torrent = self.downloadIsTorrent(release_download)
+ keep_original = True if is_torrent and self.conf('file_action') not in ['move'] else keep_original
+
+ # Unpack any archives
+ extr_files = None
+ if self.conf('unrar'):
+ folder, media_folder, files, extr_files = self.extractFiles(folder = folder, media_folder = media_folder, files = files,
+ cleanup = self.conf('cleanup') and not keep_original)
+
+ groups = fireEvent('scanner.scan', folder = folder if folder else base_folder,
+ files = files, release_download = release_download, return_ignored = False, single = True) or []
+
+ folder_name = self.conf('folder_name')
+ file_name = self.conf('file_name')
+ trailer_name = self.conf('trailer_name')
+ nfo_name = self.conf('nfo_name')
+ separator = self.conf('separator')
+
+ if len(file_name) == 0:
+ log.error('Please fill in the filename option under renamer settings. Forcing it on . to keep the same name as source file.')
+ file_name = '.'
+
+ cd_keys = ['','', '']
+ if not any(x in folder_name for x in cd_keys) and not any(x in file_name for x in cd_keys):
+ log.error('Missing `cd` or `cd_nr` in the renamer. This will cause multi-file releases of being renamed to the same file. '
+ 'Please add it in the renamer settings. Force adding it for now.')
+ file_name = '%s %s' % ('', file_name)
+
+ # Tag release folder as failed_rename in case no groups were found. This prevents check_snatched from removing the release from the downloader.
+ if not groups and self.statusInfoComplete(release_download):
+ self.tagRelease(release_download = release_download, tag = 'failed_rename')
+
+ for group_identifier in groups:
+
+ group = groups[group_identifier]
+ group['release_download'] = None
+ rename_files = {}
+ remove_files = []
+ remove_releases = []
+
+ media_title = getTitle(group)
+
+ # Add _UNKNOWN_ if no library item is connected
+ if not group.get('media') or not media_title:
+ self.tagRelease(group = group, tag = 'unknown')
+ continue
+ # Rename the files using the library data
+ else:
+
+ # Media not in library, add it first
+ if not group['media'].get('_id'):
+ group['media'] = fireEvent('movie.add', params = {
+ 'identifier': group['identifier'],
+ 'profile_id': None
+ }, search_after = False, status = 'done', single = True)
+ else:
+ group['media'] = fireEvent('movie.update', media_id = group['media'].get('_id'), single = True)
+
+ if not group['media'] or not group['media'].get('_id'):
+ log.error('Could not rename, no library item to work with: %s', group_identifier)
+ continue
+
+ media = group['media']
+ media_title = getTitle(media)
+
+ # Overwrite destination when set in category
+ destination = to_folder
+ category_label = ''
+
+ if media.get('category_id') and media.get('category_id') != '-1':
+ try:
+ category = db.get('id', media['category_id'])
+ category_label = category['label']
+
+ if category['destination'] and len(category['destination']) > 0 and category['destination'] != 'None':
+ destination = sp(category['destination'])
+ log.debug('Setting category destination for "%s": %s' % (media_title, destination))
+ else:
+ log.debug('No category destination found for "%s"' % media_title)
+ except:
+ log.error('Failed getting category label: %s', traceback.format_exc())
+
+
+ # Find subtitle for renaming
+ group['before_rename'] = []
+ fireEvent('renamer.before', group)
+
+ # Add extracted files to the before_rename list
+ if extr_files:
+ group['before_rename'].extend(extr_files)
+
+ # Remove weird chars from movie name
+ movie_name = re.sub(r"[\x00\/\\:\*\?\"<>\|]", '', media_title)
+
+ # Put 'The' at the end
+ name_the = movie_name
+ for prefix in ['the ', 'an ', 'a ']:
+ if prefix == movie_name[:len(prefix)].lower():
+ name_the = movie_name[len(prefix):] + ', ' + prefix.strip().capitalize()
+ break
+
+ replacements = {
+ 'ext': 'mkv',
+ 'namethe': name_the.strip(),
+ 'thename': movie_name.strip(),
+ 'year': media['info']['year'],
+ 'first': name_the[0].upper(),
+ 'quality': group['meta_data']['quality']['label'],
+ 'quality_type': group['meta_data']['quality_type'],
+ 'video': group['meta_data'].get('video'),
+ 'audio': group['meta_data'].get('audio'),
+ 'group': group['meta_data']['group'],
+ 'source': group['meta_data']['source'],
+ 'resolution_width': group['meta_data'].get('resolution_width'),
+ 'resolution_height': group['meta_data'].get('resolution_height'),
+ 'audio_channels': group['meta_data'].get('audio_channels'),
+ 'imdb_id': group['identifier'],
+ 'cd': '',
+ 'cd_nr': '',
+ 'mpaa': media['info'].get('mpaa', ''),
+ 'mpaa_only': media['info'].get('mpaa', ''),
+ 'category': category_label,
+ '3d': '3D' if group['meta_data']['quality'].get('is_3d', 0) else '',
+ '3d_type': group['meta_data'].get('3d_type'),
+ }
+
+ if replacements['mpaa_only'] not in ('G', 'PG', 'PG-13', 'R', 'NC-17'):
+ replacements['mpaa_only'] = 'Not Rated'
+
+ for file_type in group['files']:
+
+ # Move nfo depending on settings
+ if file_type is 'nfo' and not self.conf('rename_nfo'):
+ log.debug('Skipping, renaming of %s disabled', file_type)
+ for current_file in group['files'][file_type]:
+ if self.conf('cleanup') and (not keep_original or self.fileIsAdded(current_file, group)):
+ remove_files.append(current_file)
+ continue
+
+ # Subtitle extra
+ if file_type is 'subtitle_extra':
+ continue
+
+ # Move other files
+ multiple = len(group['files'][file_type]) > 1 and not group['is_dvd']
+ cd = 1 if multiple else 0
+
+ for current_file in sorted(list(group['files'][file_type])):
+ current_file = sp(current_file)
+
+ # Original filename
+ replacements['original'] = os.path.splitext(os.path.basename(current_file))[0]
+ replacements['original_folder'] = fireEvent('scanner.remove_cptag', group['dirname'], single = True)
+
+ if not replacements['original_folder'] or len(replacements['original_folder']) == 0:
+ replacements['original_folder'] = replacements['original']
+
+ # Extension
+ replacements['ext'] = getExt(current_file)
+
+ # cd #
+ replacements['cd'] = ' cd%d' % cd if multiple else ''
+ replacements['cd_nr'] = cd if multiple else ''
+
+ # Naming
+ final_folder_name = self.doReplace(folder_name, replacements, folder = True)
+ final_file_name = self.doReplace(file_name, replacements)
+ replacements['filename'] = final_file_name[:-(len(getExt(final_file_name)) + 1)]
+
+ # Meta naming
+ if file_type is 'trailer':
+ final_file_name = self.doReplace(trailer_name, replacements, remove_multiple = True)
+ elif file_type is 'nfo':
+ final_file_name = self.doReplace(nfo_name, replacements, remove_multiple = True)
+
+ # Move DVD files (no structure renaming)
+ if group['is_dvd'] and file_type is 'movie':
+ found = False
+ for top_dir in ['video_ts', 'audio_ts', 'bdmv', 'certificate']:
+ has_string = current_file.lower().find(os.path.sep + top_dir + os.path.sep)
+ if has_string >= 0:
+ structure_dir = current_file[has_string:].lstrip(os.path.sep)
+ rename_files[current_file] = os.path.join(destination, final_folder_name, structure_dir)
+ found = True
+ break
+
+ if not found:
+ log.error('Could not determine dvd structure for: %s', current_file)
+
+ # Do rename others
+ else:
+ if file_type is 'leftover':
+ if self.conf('move_leftover'):
+ rename_files[current_file] = os.path.join(destination, final_folder_name, os.path.basename(current_file))
+ elif file_type not in ['subtitle']:
+ rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
+
+ # Check for extra subtitle files
+ if file_type is 'subtitle':
+
+ remove_multiple = False
+ if len(group['files']['movie']) == 1:
+ remove_multiple = True
+
+ sub_langs = group['subtitle_language'].get(current_file, [])
+
+ # rename subtitles with or without language
+ sub_name = self.doReplace(file_name, replacements, remove_multiple = remove_multiple)
+ rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name)
+
+ rename_extras = self.getRenameExtras(
+ extra_type = 'subtitle_extra',
+ replacements = replacements,
+ folder_name = folder_name,
+ file_name = file_name,
+ destination = destination,
+ group = group,
+ current_file = current_file,
+ remove_multiple = remove_multiple,
+ )
+
+ # Don't add language if multiple languages in 1 subtitle file
+ if len(sub_langs) == 1:
+ sub_suffix = '%s.%s' % (sub_langs[0], replacements['ext'])
+
+ # Don't add language to subtitle file it it's already there
+ if not sub_name.endswith(sub_suffix):
+ sub_name = sub_name.replace(replacements['ext'], sub_suffix)
+ rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name)
+
+ rename_files = mergeDicts(rename_files, rename_extras)
+
+ # Filename without cd etc
+ elif file_type is 'movie':
+ rename_extras = self.getRenameExtras(
+ extra_type = 'movie_extra',
+ replacements = replacements,
+ folder_name = folder_name,
+ file_name = file_name,
+ destination = destination,
+ group = group,
+ current_file = current_file
+ )
+ rename_files = mergeDicts(rename_files, rename_extras)
+
+ group['filename'] = self.doReplace(file_name, replacements, remove_multiple = True)[:-(len(getExt(final_file_name)) + 1)]
+ group['destination_dir'] = os.path.join(destination, final_folder_name)
+
+ if multiple:
+ cd += 1
+
+ # Before renaming, remove the lower quality files
+ remove_leftovers = True
+
+ # Get media quality profile
+ profile = None
+ if media.get('profile_id'):
+ try:
+ profile = db.get('id', media['profile_id'])
+ except:
+ # Set profile to None as it does not exist anymore
+ mdia = db.get('id', media['_id'])
+ mdia['profile_id'] = None
+ db.update(mdia)
+ log.error('Error getting quality profile for %s: %s', (media_title, traceback.format_exc()))
+ else:
+ log.debug('Media has no quality profile: %s', media_title)
+
+ # Mark media for dashboard
+ mark_as_recent = False
+
+ # Go over current movie releases
+ for release in fireEvent('release.for_media', media['_id'], single = True):
+
+ # When a release already exists
+ if release.get('status') == 'done':
+
+ # This is where CP removes older, lesser quality releases or releases that are not wanted anymore
+ is_higher = fireEvent('quality.ishigher', \
+ group['meta_data']['quality'], {'identifier': release['quality'], 'is_3d': release.get('is_3d', False)}, profile, single = True)
+
+ if is_higher == 'higher':
+ log.info('Removing lesser or not wanted quality %s for %s.', (media_title, release.get('quality')))
+ for file_type in release.get('files', {}):
+ for release_file in release['files'][file_type]:
+ remove_files.append(release_file)
+ remove_releases.append(release)
+
+ # Same quality, but still downloaded, so maybe repack/proper/unrated/directors cut etc
+ elif is_higher == 'equal':
+ log.info('Same quality release already exists for %s, with quality %s. Assuming repack.', (media_title, release.get('quality')))
+ for file_type in release.get('files', {}):
+ for release_file in release['files'][file_type]:
+ remove_files.append(release_file)
+ remove_releases.append(release)
+
+ # Downloaded a lower quality, rename the newly downloaded files/folder to exclude them from scan
+ else:
+ log.info('Better quality release already exists for %s, with quality %s', (media_title, release.get('quality')))
+
+ # Add exists tag to the .ignore file
+ self.tagRelease(group = group, tag = 'exists')
+
+ # Notify on rename fail
+ download_message = 'Renaming of %s (%s) cancelled, exists in %s already.' % (media_title, group['meta_data']['quality']['label'], release.get('quality'))
+ fireEvent('movie.renaming.canceled', message = download_message, data = group)
+ remove_leftovers = False
+
+ break
+
+ elif release.get('status') in ['snatched', 'seeding']:
+ if release_download and release_download.get('release_id'):
+ if release_download['release_id'] == release['_id']:
+ if release_download['status'] == 'completed':
+ # Set the release to downloaded
+ fireEvent('release.update_status', release['_id'], status = 'downloaded', single = True)
+ group['release_download'] = release_download
+ mark_as_recent = True
+ elif release_download['status'] == 'seeding':
+ # Set the release to seeding
+ fireEvent('release.update_status', release['_id'], status = 'seeding', single = True)
+ mark_as_recent = True
+
+ elif release.get('quality') == group['meta_data']['quality']['identifier']:
+ # Set the release to downloaded
+ fireEvent('release.update_status', release['_id'], status = 'downloaded', single = True)
+ group['release_download'] = release_download
+ mark_as_recent = True
+
+ # Mark media for dashboard
+ if mark_as_recent:
+ fireEvent('media.tag', group['media'].get('_id'), 'recent', update_edited = True, single = True)
+
+ # Remove leftover files
+ if not remove_leftovers: # Don't remove anything
+ continue
+
+ log.debug('Removing leftover files')
+ for current_file in group['files']['leftover']:
+ if self.conf('cleanup') and not self.conf('move_leftover') and \
+ (not keep_original or self.fileIsAdded(current_file, group)):
+ remove_files.append(current_file)
+
+ if self.conf('check_space'):
+ total_space, available_space = getFreeSpace(destination)
+ renaming_size = getSize(rename_files.keys())
+ if renaming_size > available_space:
+ log.error('Not enough space left, need %s MB but only %s MB available', (renaming_size, available_space))
+ self.tagRelease(group = group, tag = 'not_enough_space')
+ continue
+
+ # Remove files
+ delete_folders = []
+ for src in remove_files:
+
+ if rename_files.get(src):
+ log.debug('Not removing file that will be renamed: %s', src)
+ continue
+
+ log.info('Removing "%s"', src)
+ try:
+ src = sp(src)
+ if os.path.isfile(src):
+ os.remove(src)
+
+ parent_dir = os.path.dirname(src)
+ if parent_dir not in delete_folders and os.path.isdir(parent_dir) and \
+ not isSubFolder(destination, parent_dir) and not isSubFolder(media_folder, parent_dir) and \
+ isSubFolder(parent_dir, base_folder):
+
+ delete_folders.append(parent_dir)
+
+ except:
+ log.error('Failed removing %s: %s', (src, traceback.format_exc()))
+ self.tagRelease(group = group, tag = 'failed_remove')
+
+ # Delete leftover folder from older releases
+ delete_folders = sorted(delete_folders, key = len, reverse = True)
+ for delete_folder in delete_folders:
+ try:
+ self.deleteEmptyFolder(delete_folder, show_error = False)
+ except Exception as e:
+ log.error('Failed to delete folder: %s %s', (e, traceback.format_exc()))
+
+ # Rename all files marked
+ group['renamed_files'] = []
+ failed_rename = False
+ for src in rename_files:
+ if rename_files[src]:
+ dst = rename_files[src]
+
+ if dst in group['renamed_files']:
+ log.error('File "%s" already renamed once, adding random string at the end to prevent data loss', dst)
+ dst = '%s.random-%s' % (dst, randomString())
+
+ # Create dir
+ self.makeDir(os.path.dirname(dst))
+
+ try:
+ self.moveFile(src, dst, use_default = not is_torrent or self.fileIsAdded(src, group))
+ group['renamed_files'].append(dst)
+ except:
+ log.error('Failed renaming the file "%s" : %s', (os.path.basename(src), traceback.format_exc()))
+ failed_rename = True
+ break
+
+ # If renaming failed tag the release folder as failed and continue with next group. Note that all old files have already been deleted.
+ if failed_rename:
+ self.tagRelease(group = group, tag = 'failed_rename')
+ continue
+ # If renaming succeeded, make sure it is not tagged as failed (scanner didn't return a group, but a download_ID was provided in an earlier attempt)
+ else:
+ self.untagRelease(group = group, tag = 'failed_rename')
+
+ # Tag folder if it is in the 'from' folder and it will not be removed because it is a torrent
+ if self.movieInFromFolder(media_folder) and keep_original:
+ self.tagRelease(group = group, tag = 'renamed_already')
+
+ # Remove matching releases
+ for release in remove_releases:
+ log.debug('Removing release %s', release.get('identifier'))
+ try:
+ db.delete(release)
+ except:
+ log.error('Failed removing %s: %s', (release, traceback.format_exc()))
+
+ if group['dirname'] and group['parentdir'] and not keep_original:
+ if media_folder:
+ # Delete the movie folder
+ group_folder = media_folder
+ else:
+ # Delete the first empty subfolder in the tree relative to the 'from' folder
+ group_folder = sp(os.path.join(base_folder, os.path.relpath(group['parentdir'], base_folder).split(os.path.sep)[0]))
+
+ try:
+ if self.conf('cleanup') or self.conf('move_leftover'):
+ log.info('Deleting folder: %s', group_folder)
+ self.deleteEmptyFolder(group_folder)
+ except:
+ log.error('Failed removing %s: %s', (group_folder, traceback.format_exc()))
+
+ # Notify on download, search for trailers etc
+ download_message = 'Downloaded %s (%s%s)' % (media_title, replacements['quality'], (' ' + replacements['3d']) if replacements['3d'] else '')
+ try:
+ fireEvent('renamer.after', message = download_message, group = group, in_order = True)
+ except:
+ log.error('Failed firing (some) of the renamer.after events: %s', traceback.format_exc())
+
+ # Break if CP wants to shut down
+ if self.shuttingDown():
+ break
+
+ self.renaming_started = False
+
+ def getRenameExtras(self, extra_type = '', replacements = None, folder_name = '', file_name = '', destination = '', group = None, current_file = '', remove_multiple = False):
+ if not group: group = {}
+ if not replacements: replacements = {}
+
+ replacements = replacements.copy()
+ rename_files = {}
+
+ def test(s):
+ return current_file[:-len(replacements['ext'])] in sp(s)
+
+ for extra in set(filter(test, group['files'][extra_type])):
+ replacements['ext'] = getExt(extra)
+
+ final_folder_name = self.doReplace(folder_name, replacements, remove_multiple = remove_multiple, folder = True)
+ final_file_name = self.doReplace(file_name, replacements, remove_multiple = remove_multiple)
+ rename_files[extra] = os.path.join(destination, final_folder_name, final_file_name)
+
+ return rename_files
+
+ # This adds a file to ignore / tag a release so it is ignored later
+ def tagRelease(self, tag, group = None, release_download = None):
+ if not tag:
+ return
+
+ text = """This file is from CouchPotato
+It has marked this release as "%s"
+This file hides the release from the renamer
+Remove it if you want it to be renamed (again, or at least let it try again)
+""" % tag
+
+ tag_files = []
+
+ # Tag movie files if they are known
+ if isinstance(group, dict):
+ tag_files = [sorted(list(group['files']['movie']))[0]]
+
+ elif isinstance(release_download, dict):
+ # Tag download_files if they are known
+ if release_download.get('files', []):
+ tag_files = [filename for filename in release_download.get('files', []) if os.path.exists(filename)]
+
+ # Tag all files in release folder
+ elif release_download['folder']:
+ for root, folders, names in os.walk(sp(release_download['folder'])):
+ tag_files.extend([os.path.join(root, name) for name in names])
+
+ for filename in tag_files:
+
+ # Don't tag .ignore files
+ if os.path.splitext(filename)[1] == '.ignore':
+ continue
+
+ tag_filename = '%s.%s.ignore' % (os.path.splitext(filename)[0], tag)
+ if not os.path.isfile(tag_filename):
+ self.createFile(tag_filename, text)
+
+ def untagRelease(self, group = None, release_download = None, tag = ''):
+ if not release_download:
+ return
+
+ tag_files = []
+ folder = None
+
+ # Tag movie files if they are known
+ if isinstance(group, dict):
+ tag_files = [sorted(list(group['files']['movie']))[0]]
+
+ folder = sp(group['parentdir'])
+ if not group.get('dirname') or not os.path.isdir(folder):
+ return False
+
+ elif isinstance(release_download, dict):
+
+ folder = sp(release_download['folder'])
+ if not os.path.isdir(folder):
+ return False
+
+ # Untag download_files if they are known
+ if release_download.get('files'):
+ tag_files = release_download.get('files', [])
+
+ # Untag all files in release folder
+ else:
+ for root, folders, names in os.walk(folder):
+ tag_files.extend([sp(os.path.join(root, name)) for name in names if not os.path.splitext(name)[1] == '.ignore'])
+
+ if not folder:
+ return False
+
+ # Find all .ignore files in folder
+ ignore_files = []
+ for root, dirnames, filenames in os.walk(folder):
+ ignore_files.extend(fnmatch.filter([sp(os.path.join(root, filename)) for filename in filenames], '*%s.ignore' % tag))
+
+ # Match all found ignore files with the tag_files and delete if found
+ for tag_file in tag_files:
+ ignore_file = fnmatch.filter(ignore_files, fnEscape('%s.%s.ignore' % (os.path.splitext(tag_file)[0], tag if tag else '*')))
+ for filename in ignore_file:
+ try:
+ os.remove(filename)
+ except:
+ log.debug('Unable to remove ignore file: %s. Error: %s.' % (filename, traceback.format_exc()))
+
+ def hastagRelease(self, release_download, tag = ''):
+ if not release_download:
+ return False
+
+ folder = sp(release_download['folder'])
+ if not os.path.isdir(folder):
+ return False
+
+ tag_files = []
+ ignore_files = []
+
+ # Find tag on download_files if they are known
+ if release_download.get('files'):
+ tag_files = release_download.get('files', [])
+
+ # Find tag on all files in release folder
+ else:
+ for root, folders, names in os.walk(folder):
+ tag_files.extend([sp(os.path.join(root, name)) for name in names if not os.path.splitext(name)[1] == '.ignore'])
+
+ # Find all .ignore files in folder
+ for root, dirnames, filenames in os.walk(folder):
+ ignore_files.extend(fnmatch.filter([sp(os.path.join(root, filename)) for filename in filenames], '*%s.ignore' % tag))
+
+ # Match all found ignore files with the tag_files and return True found
+ for tag_file in tag_files:
+ ignore_file = fnmatch.filter(ignore_files, fnEscape('%s.%s.ignore' % (os.path.splitext(tag_file)[0], tag if tag else '*')))
+ if ignore_file:
+ return True
+
+ return False
+
+ def moveFile(self, old, dest, use_default = False):
+ dest = sp(dest)
+ try:
+
+ if os.path.exists(dest) and os.path.isfile(dest):
+ raise Exception('Destination "%s" already exists' % dest)
+
+ move_type = self.conf('file_action')
+ if use_default:
+ move_type = self.conf('default_file_action')
+
+ if move_type not in ['copy', 'link']:
+ try:
+ log.info('Moving "%s" to "%s"', (old, dest))
+ shutil.move(old, dest)
+ except:
+ exists = os.path.exists(dest)
+ if exists and os.path.getsize(old) == os.path.getsize(dest):
+ log.error('Successfully moved file "%s", but something went wrong: %s', (dest, traceback.format_exc()))
+ os.unlink(old)
+ else:
+ # remove faultly copied file
+ if exists:
+ os.unlink(dest)
+ raise
+ elif move_type == 'copy':
+ log.info('Copying "%s" to "%s"', (old, dest))
+ shutil.copy(old, dest)
+ else:
+ log.info('Linking "%s" to "%s"', (old, dest))
+ # First try to hardlink
+ try:
+ log.debug('Hardlinking file "%s" to "%s"...', (old, dest))
+ link(old, dest)
+ except:
+ # Try to simlink next
+ log.debug('Couldn\'t hardlink file "%s" to "%s". Symlinking instead. Error: %s.', (old, dest, traceback.format_exc()))
+ shutil.copy(old, dest)
+ try:
+ old_link = '%s.link' % sp(old)
+ symlink(dest, old_link)
+ os.unlink(old)
+ os.rename(old_link, old)
+ except:
+ log.error('Couldn\'t symlink file "%s" to "%s". Copied instead. Error: %s. ', (old, dest, traceback.format_exc()))
+
+ try:
+ os.chmod(dest, Env.getPermission('file'))
+ if os.name == 'nt' and self.conf('ntfs_permission'):
+ os.popen('icacls "' + dest + '"* /reset /T')
+ except:
+ log.debug('Failed setting permissions for file: %s, %s', (dest, traceback.format_exc(1)))
+ except:
+ log.error('Couldn\'t move file "%s" to "%s": %s', (old, dest, traceback.format_exc()))
+ raise
+
+ return True
+
+ def doReplace(self, string, replacements, remove_multiple = False, folder = False):
+ """
+ replace confignames with the real thing
+ """
+
+ replacements = replacements.copy()
+ if remove_multiple:
+ replacements['cd'] = ''
+ replacements['cd_nr'] = ''
+
+ replaced = toUnicode(string)
+ for x, r in replacements.items():
+ if x in ['thename', 'namethe']:
+ continue
+ if r is not None:
+ replaced = replaced.replace(six.u('<%s>') % toUnicode(x), toUnicode(r))
+ else:
+ #If information is not available, we don't want the tag in the filename
+ replaced = replaced.replace('<' + x + '>', '')
+
+ if self.conf('replace_doubles'):
+ replaced = self.replaceDoubles(replaced.lstrip('. '))
+
+ for x, r in replacements.items():
+ if x in ['thename', 'namethe']:
+ replaced = replaced.replace(six.u('<%s>') % toUnicode(x), toUnicode(r))
+ replaced = re.sub(r"[\x00:\*\?\"<>\|]", '', replaced)
+
+ sep = self.conf('foldersep') if folder else self.conf('separator')
+ return ss(replaced.replace(' ', ' ' if not sep else sep))
+
+ def replaceDoubles(self, string):
+
+ replaces = [
+ ('\.+', '.'), ('_+', '_'), ('-+', '-'), ('\s+', ' '), (' \\\\', '\\\\'), (' /', '/'),
+ ('(\s\.)+', '.'), ('(-\.)+', '.'), ('(\s-)+', '-'),
+ ]
+
+ for r in replaces:
+ reg, replace_with = r
+ string = re.sub(reg, replace_with, string)
+
+ string = string.rstrip(',_-/\\ ')
+
+ return string
+
+ def checkSnatched(self, fire_scan = True):
+
+ if self.checking_snatched:
+ log.debug('Already checking snatched')
+ return False
+
+ self.checking_snatched = True
+
+ try:
+ db = get_db()
+
+ rels = list(fireEvent('release.with_status', ['snatched', 'seeding', 'missing'], single = True))
+
+ if not rels:
+ #No releases found that need status checking
+ self.checking_snatched = False
+ return True
+
+ # Collect all download information with the download IDs from the releases
+ download_ids = []
+ no_status_support = []
+ try:
+ for rel in rels:
+ if not rel.get('download_info'): continue
+
+ if rel['download_info'].get('id') and rel['download_info'].get('downloader'):
+ download_ids.append(rel['download_info'])
+
+ ds = rel['download_info'].get('status_support')
+ if ds is False or ds == 'False':
+ no_status_support.append(ss(rel['download_info'].get('downloader')))
+ except:
+ log.error('Error getting download IDs from database')
+ self.checking_snatched = False
+ return False
+
+ release_downloads = fireEvent('download.status', download_ids, merge = True) if download_ids else []
+
+ if len(no_status_support) > 0:
+ log.debug('Download status functionality is not implemented for one of the active downloaders: %s', list(set(no_status_support)))
+
+ if not release_downloads:
+ if fire_scan:
+ self.scan()
+
+ self.checking_snatched = False
+ return True
+
+ scan_releases = []
+ scan_required = False
+
+ log.debug('Checking status snatched releases...')
+
+ try:
+ for rel in rels:
+ if not rel.get('media_id'): continue
+ movie_dict = db.get('id', rel.get('media_id'))
+ download_info = rel.get('download_info')
+
+ if not isinstance(download_info, dict):
+ log.error('Faulty release found without any info, ignoring.')
+ fireEvent('release.update_status', rel.get('_id'), status = 'ignored', single = True)
+ continue
+
+ # Check if download ID is available
+ if not download_info.get('id') or not download_info.get('downloader'):
+ log.debug('Download status functionality is not implemented for downloader (%s) of release %s.', (download_info.get('downloader', 'unknown'), rel['info']['name']))
+ scan_required = True
+
+ # Continue with next release
+ continue
+
+ # Find release in downloaders
+ nzbname = self.createNzbName(rel['info'], movie_dict)
+
+ found_release = False
+ for release_download in release_downloads:
+ found_release = False
+ if download_info.get('id'):
+ if release_download['id'] == download_info['id'] and release_download['downloader'] == download_info['downloader']:
+ log.debug('Found release by id: %s', release_download['id'])
+ found_release = True
+ break
+ else:
+ if release_download['name'] == nzbname or rel['info']['name'] in release_download['name'] or getImdb(release_download['name']) == getIdentifier(movie_dict):
+ log.debug('Found release by release name or imdb ID: %s', release_download['name'])
+ found_release = True
+ break
+
+ if not found_release:
+ log.info('%s not found in downloaders', nzbname)
+
+ #Check status if already missing and for how long, if > 1 week, set to ignored else to missing
+ if rel.get('status') == 'missing':
+ if rel.get('last_edit') < int(time.time()) - 7 * 24 * 60 * 60:
+ fireEvent('release.update_status', rel.get('_id'), status = 'ignored', single = True)
+ else:
+ # Set the release to missing
+ fireEvent('release.update_status', rel.get('_id'), status = 'missing', single = True)
+
+ # Continue with next release
+ continue
+
+ # Log that we found the release
+ timeleft = 'N/A' if release_download['timeleft'] == -1 else release_download['timeleft']
+ log.debug('Found %s: %s, time to go: %s', (release_download['name'], release_download['status'].upper(), timeleft))
+
+ # Check status of release
+ if release_download['status'] == 'busy':
+ # Set the release to snatched if it was missing before
+ fireEvent('release.update_status', rel.get('_id'), status = 'snatched', single = True)
+
+ # Tag folder if it is in the 'from' folder and it will not be processed because it is still downloading
+ if self.movieInFromFolder(release_download['folder']):
+ self.tagRelease(release_download = release_download, tag = 'downloading')
+
+ elif release_download['status'] == 'seeding':
+ #If linking setting is enabled, process release
+ if self.conf('file_action') != 'move' and not rel.get('status') == 'seeding' and self.statusInfoComplete(release_download):
+ log.info('Download of %s completed! It is now being processed while leaving the original files alone for seeding. Current ratio: %s.', (release_download['name'], release_download['seed_ratio']))
+
+ # Remove the downloading tag
+ self.untagRelease(release_download = release_download, tag = 'downloading')
+
+ # Scan and set the torrent to paused if required
+ release_download.update({'pause': True, 'scan': True, 'process_complete': False})
+ scan_releases.append(release_download)
+ else:
+ #let it seed
+ log.debug('%s is seeding with ratio: %s', (release_download['name'], release_download['seed_ratio']))
+
+ # Set the release to seeding
+ fireEvent('release.update_status', rel.get('_id'), status = 'seeding', single = True)
+
+ elif release_download['status'] == 'failed':
+ # Set the release to failed
+ fireEvent('release.update_status', rel.get('_id'), status = 'failed', single = True)
+
+ fireEvent('download.remove_failed', release_download, single = True)
+
+ if self.conf('next_on_failed'):
+ fireEvent('movie.searcher.try_next_release', media_id = rel.get('media_id'))
+
+ elif release_download['status'] == 'completed':
+ log.info('Download of %s completed!', release_download['name'])
+
+ #Make sure the downloader sent over a path to look in
+ if self.statusInfoComplete(release_download):
+
+ # If the release has been seeding, process now the seeding is done
+ if rel.get('status') == 'seeding':
+ if self.conf('file_action') != 'move':
+ # Set the release to done as the movie has already been renamed
+ fireEvent('release.update_status', rel.get('_id'), status = 'downloaded', single = True)
+
+ # Allow the downloader to clean-up
+ release_download.update({'pause': False, 'scan': False, 'process_complete': True})
+ scan_releases.append(release_download)
+ else:
+ # Scan and Allow the downloader to clean-up
+ release_download.update({'pause': False, 'scan': True, 'process_complete': True})
+ scan_releases.append(release_download)
+
+ else:
+ # Set the release to snatched if it was missing before
+ fireEvent('release.update_status', rel.get('_id'), status = 'snatched', single = True)
+
+ # Remove the downloading tag
+ self.untagRelease(release_download = release_download, tag = 'downloading')
+
+ # Scan and Allow the downloader to clean-up
+ release_download.update({'pause': False, 'scan': True, 'process_complete': True})
+ scan_releases.append(release_download)
+ else:
+ scan_required = True
+
+ except:
+ log.error('Failed checking for release in downloader: %s', traceback.format_exc())
+
+ # The following can either be done here, or inside the scanner if we pass it scan_items in one go
+ for release_download in scan_releases:
+ # Ask the renamer to scan the item
+ if release_download['scan']:
+ if release_download['pause'] and self.conf('file_action') == 'link':
+ fireEvent('download.pause', release_download = release_download, pause = True, single = True)
+ self.scan(release_download = release_download)
+ if release_download['pause'] and self.conf('file_action') == 'link':
+ fireEvent('download.pause', release_download = release_download, pause = False, single = True)
+ if release_download['process_complete']:
+ # First make sure the files were successfully processed
+ if not self.hastagRelease(release_download = release_download, tag = 'failed_rename'):
+ # Remove the seeding tag if it exists
+ self.untagRelease(release_download = release_download, tag = 'renamed_already')
+ # Ask the downloader to process the item
+ fireEvent('download.process_complete', release_download = release_download, single = True)
+
+ if fire_scan and (scan_required or len(no_status_support) > 0):
+ self.scan()
+
+ self.checking_snatched = False
+ return True
+ except:
+ log.error('Failed checking snatched: %s', traceback.format_exc())
+
+ self.checking_snatched = False
+ return False
+
+ def extendReleaseDownload(self, release_download):
+
+ rls = None
+ db = get_db()
+
+ if release_download and release_download.get('id'):
+ try:
+ rls = db.get('release_download', '%s-%s' % (release_download.get('downloader'), release_download.get('id')), with_doc = True)['doc']
+ except:
+ log.error('Download ID %s from downloader %s not found in releases', (release_download.get('id'), release_download.get('downloader')))
+
+ if rls:
+ media = db.get('id', rls['media_id'])
+ release_download.update({
+ 'imdb_id': getIdentifier(media),
+ 'quality': rls['quality'],
+ 'is_3d': rls['is_3d'],
+ 'protocol': rls.get('info', {}).get('protocol') or rls.get('info', {}).get('type'),
+ 'release_id': rls['_id'],
+ })
+
+ return release_download
+
+ def downloadIsTorrent(self, release_download):
+ return release_download and release_download.get('protocol') in ['torrent', 'torrent_magnet']
+
+ def fileIsAdded(self, src, group):
+ if not group or not group.get('before_rename'):
+ return False
+ return src in group['before_rename']
+
+ def moveTypeIsLinked(self):
+ return self.conf('default_file_action') in ['copy', 'link']
+
+ def statusInfoComplete(self, release_download):
+ return release_download.get('id') and release_download.get('downloader') and release_download.get('folder')
+
+ def movieInFromFolder(self, media_folder):
+ return media_folder and isSubFolder(media_folder, sp(self.conf('from'))) or not media_folder
+
+ def extractFiles(self, folder = None, media_folder = None, files = None, cleanup = False):
+ if not files: files = []
+
+ # RegEx for finding rar files
+ archive_regex = '(?P^(?P(?:(?!\.part\d+\.rar$).)*)\.(?:(?:part0*1\.)?rar)$)'
+ restfile_regex = '(^%s\.(?:part(?!0*1\.rar$)\d+\.rar$|[rstuvw]\d+$))'
+ extr_files = []
+
+ from_folder = sp(self.conf('from'))
+
+ # Check input variables
+ if not folder:
+ folder = from_folder
+
+ check_file_date = True
+ if media_folder:
+ check_file_date = False
+
+ if not files:
+ for root, folders, names in os.walk(folder):
+ files.extend([sp(os.path.join(root, name)) for name in names])
+
+ # Find all archive files
+ archives = [re.search(archive_regex, name).groupdict() for name in files if re.search(archive_regex, name)]
+
+ #Extract all found archives
+ for archive in archives:
+ # Check if it has already been processed by CPS
+ if self.hastagRelease(release_download = {'folder': os.path.dirname(archive['file']), 'files': archive['file']}):
+ continue
+
+ # Find all related archive files
+ archive['files'] = [name for name in files if re.search(restfile_regex % re.escape(archive['base']), name)]
+ archive['files'].append(archive['file'])
+
+ # Check if archive is fresh and maybe still copying/moving/downloading, ignore files newer than 1 minute
+ if check_file_date:
+ files_too_new, time_string = self.checkFilesChanged(archive['files'])
+
+ if files_too_new:
+ log.info('Archive seems to be still copying/moving/downloading or just copied/moved/downloaded (created on %s), ignoring for now: %s', (time_string, os.path.basename(archive['file'])))
+ continue
+
+ log.info('Archive %s found. Extracting...', os.path.basename(archive['file']))
+ try:
+ unrar_path = self.conf('unrar_path')
+ unrar_path = unrar_path if unrar_path and (os.path.isfile(unrar_path) or re.match('^[a-zA-Z0-9_/\.\-]+$', unrar_path)) else None
+
+ rar_handle = RarFile(archive['file'], custom_path = unrar_path)
+ extr_path = os.path.join(from_folder, os.path.relpath(os.path.dirname(archive['file']), folder))
+ self.makeDir(extr_path)
+ for packedinfo in rar_handle.infolist():
+ extr_file_path = sp(os.path.join(extr_path, os.path.basename(packedinfo.filename)))
+ if not packedinfo.isdir and not os.path.isfile(extr_file_path):
+ log.debug('Extracting %s...', packedinfo.filename)
+ rar_handle.extract(condition = [packedinfo.index], path = extr_path, withSubpath = False, overwrite = False)
+ if self.conf('unrar_modify_date'):
+ try:
+ os.utime(extr_file_path, (os.path.getatime(archive['file']), os.path.getmtime(archive['file'])))
+ except:
+ log.error('Rar modify date enabled, but failed: %s', traceback.format_exc())
+ extr_files.append(extr_file_path)
+ del rar_handle
+ except Exception as e:
+ log.error('Failed to extract %s: %s %s', (archive['file'], e, traceback.format_exc()))
+ continue
+
+ # Delete the archive files
+ for filename in archive['files']:
+ if cleanup:
+ try:
+ os.remove(filename)
+ except Exception as e:
+ log.error('Failed to remove %s: %s %s', (filename, e, traceback.format_exc()))
+ continue
+ files.remove(filename)
+
+ # Move the rest of the files and folders if any files are extracted to the from folder (only if folder was provided)
+ if extr_files and folder != from_folder:
+ for leftoverfile in list(files):
+ move_to = os.path.join(from_folder, os.path.relpath(leftoverfile, folder))
+
+ try:
+ self.makeDir(os.path.dirname(move_to))
+ self.moveFile(leftoverfile, move_to, cleanup)
+ except Exception as e:
+ log.error('Failed moving left over file %s to %s: %s %s', (leftoverfile, move_to, e, traceback.format_exc()))
+ # As we probably tried to overwrite the nfo file, check if it exists and then remove the original
+ if os.path.isfile(move_to) and os.path.getsize(leftoverfile) == os.path.getsize(move_to):
+ if cleanup:
+ log.info('Deleting left over file %s instead...', leftoverfile)
+ os.unlink(leftoverfile)
+ else:
+ continue
+
+ files.remove(leftoverfile)
+ extr_files.append(move_to)
+
+ if cleanup:
+ # Remove all left over folders
+ log.debug('Removing old movie folder %s...', media_folder)
+ self.deleteEmptyFolder(media_folder)
+
+ media_folder = os.path.join(from_folder, os.path.relpath(media_folder, folder))
+ folder = from_folder
+
+ if extr_files:
+ files.extend(extr_files)
+
+ # Cleanup files and folder if media_folder was not provided
+ if not media_folder:
+ files = []
+ folder = None
+
+ return folder, media_folder, files, extr_files
+
+
+rename_options = {
+ 'pre': '<',
+ 'post': '>',
+ 'choices': {
+ 'ext': 'Extention (mkv)',
+ 'namethe': 'Moviename, The',
+ 'thename': 'The Moviename',
+ 'year': 'Year (2011)',
+ 'first': 'First letter (M)',
+ 'quality': 'Quality (720p)',
+ 'quality_type': '(HD) or (SD)',
+ '3d': '3D',
+ '3d_type': '3D Type (Full SBS)',
+ 'video': 'Video (x264)',
+ 'audio': 'Audio (DTS)',
+ 'group': 'Releasegroup name',
+ 'source': 'Source media (Bluray)',
+ 'resolution_width': 'resolution width (1280)',
+ 'resolution_height': 'resolution height (720)',
+ 'audio_channels': 'audio channels (7.1)',
+ 'original': 'Original filename',
+ 'original_folder': 'Original foldername',
+ 'imdb_id': 'IMDB id (tt0123456)',
+ 'cd': 'CD number (cd1)',
+ 'cd_nr': 'Just the cd nr. (1)',
+ 'mpaa': 'MPAA or other certification',
+ 'mpaa_only': 'MPAA only certification (G|PG|PG-13|R|NC-17|Not Rated)',
+ 'category': 'Category label',
+ },
+}
+
+config = [{
+ 'name': 'renamer',
+ 'order': 40,
+ 'description': 'Move and rename your downloaded movies to your movie directory.',
+ 'groups': [
+ {
+ 'tab': 'renamer',
+ 'name': 'renamer',
+ 'label': 'Rename downloaded movies',
+ 'wizard': True,
+ 'options': [
+ {
+ 'name': 'enabled',
+ 'default': False,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'from',
+ 'type': 'directory',
+ 'description': 'Folder where CP searches for movies.',
+ },
+ {
+ 'name': 'to',
+ 'type': 'directory',
+ 'description': 'Default folder where the movies are moved/copied/linked to.',
+ },
+ {
+ 'name': 'folder_name',
+ 'label': 'Folder naming',
+ 'description': 'Name of the folder. Keep empty for no folder.',
+ 'default': ' ()',
+ 'type': 'choice',
+ 'options': rename_options
+ },
+ {
+ 'name': 'file_name',
+ 'label': 'File naming',
+ 'description': 'Name of the file',
+ 'default': '.',
+ 'type': 'choice',
+ 'options': rename_options
+ },
+ {
+ 'advanced': True,
+ 'name': 'replace_doubles',
+ 'type': 'bool',
+ 'label': 'Clean Name',
+ 'description': ('Attempt to clean up double separaters due to missing data for fields.','Sometimes this eliminates wanted white space (see #2782).'),
+ 'default': True
+ },
+ {
+ 'name': 'unrar',
+ 'type': 'bool',
+ 'description': 'Extract rar files if found.',
+ 'default': False,
+ },
+ {
+ 'advanced': True,
+ 'name': 'unrar_path',
+ 'description': 'Custom path to unrar bin',
+ },
+ {
+ 'advanced': True,
+ 'name': 'unrar_modify_date',
+ 'type': 'bool',
+ 'description': ('Set modify date of unrar-ed files to the rar-file\'s date.', 'This will allow XBMC to recognize extracted files as recently added even if the movie was released some time ago.'),
+ 'default': False,
+ },
+ {
+ 'name': 'cleanup',
+ 'type': 'bool',
+ 'description': 'Cleanup leftover files after successful rename.',
+ 'default': False,
+ },
+ {
+ 'advanced': True,
+ 'name': 'run_every',
+ 'label': 'Run every',
+ 'default': 1,
+ 'type': 'int',
+ 'unit': 'min(s)',
+ 'description': ('Detect movie status every X minutes.', 'Will start the renamer if movie is completed or handle failed download if these options are enabled'),
+ },
+ {
+ 'advanced': True,
+ 'name': 'force_every',
+ 'label': 'Force every',
+ 'default': 2,
+ 'type': 'int',
+ 'unit': 'hour(s)',
+ 'description': 'Forces the renamer to scan every X hours',
+ },
+ {
+ 'advanced': True,
+ 'name': 'next_on_failed',
+ 'default': True,
+ 'type': 'bool',
+ 'description': 'Try the next best release for a movie after a download failed.',
+ },
+ {
+ 'name': 'move_leftover',
+ 'type': 'bool',
+ 'description': 'Move all leftover file after renaming, to the movie folder.',
+ 'default': False,
+ 'advanced': True,
+ },
+ {
+ 'advanced': True,
+ 'name': 'separator',
+ 'label': 'File-Separator',
+ 'description': ('Replace all the spaces with a character.', 'Example: ".", "-" (without quotes). Leave empty to use spaces.'),
+ },
+ {
+ 'advanced': True,
+ 'name': 'foldersep',
+ 'label': 'Folder-Separator',
+ 'description': ('Replace all the spaces with a character.', 'Example: ".", "-" (without quotes). Leave empty to use spaces.'),
+ },
+ {
+ 'name': 'check_space',
+ 'label': 'Check space',
+ 'default': True,
+ 'type': 'bool',
+ 'description': ('Check if there\'s enough available space to rename the files', 'Disable when the filesystem doesn\'t return the proper value'),
+ 'advanced': True,
+ },
+ {
+ 'name': 'default_file_action',
+ 'label': 'Default File Action',
+ 'default': 'move',
+ 'type': 'dropdown',
+ 'values': [('Link', 'link'), ('Copy', 'copy'), ('Move', 'move')],
+ 'description': ('Link, Copy or Move after download completed.',
+ 'Link first tries hard link, then sym link and falls back to Copy.'),
+ 'advanced': True,
+ },
+ {
+ 'name': 'file_action',
+ 'label': 'Torrent File Action',
+ 'default': 'link',
+ 'type': 'dropdown',
+ 'values': [('Link', 'link'), ('Copy', 'copy'), ('Move', 'move')],
+ 'description': 'See above. It is prefered to use link when downloading torrents as it will save you space, while still being able to seed.',
+ 'advanced': True,
+ },
+ {
+ 'advanced': True,
+ 'name': 'ntfs_permission',
+ 'label': 'NTFS Permission',
+ 'type': 'bool',
+ 'hidden': os.name != 'nt',
+ 'description': 'Set permission of moved files to that of destination folder (Windows NTFS only).',
+ 'default': False,
+ },
+ ],
+ }, {
+ 'tab': 'renamer',
+ 'name': 'meta_renamer',
+ 'label': 'Advanced renaming',
+ 'description': 'Meta data file renaming. Use <filename> to use the above "File naming" settings, without the file extention.',
+ 'advanced': True,
+ 'options': [
+ {
+ 'name': 'rename_nfo',
+ 'label': 'Rename .NFO',
+ 'description': 'Rename original .nfo file',
+ 'type': 'bool',
+ 'default': True,
+ },
+ {
+ 'name': 'nfo_name',
+ 'label': 'NFO naming',
+ 'default': '.orig.',
+ 'type': 'choice',
+ 'options': rename_options
+ },
+ ],
+ },
+ ],
+}]
diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py
deleted file mode 100644
index 6ce21922d5..0000000000
--- a/couchpotato/core/plugins/renamer/__init__.py
+++ /dev/null
@@ -1,149 +0,0 @@
-from couchpotato.core.plugins.renamer.main import Renamer
-import os
-
-def start():
- return Renamer()
-
-rename_options = {
- 'pre': '<',
- 'post': '>',
- 'choices': {
- 'ext': 'Extention (mkv)',
- 'namethe': 'Moviename, The',
- 'thename': 'The Moviename',
- 'year': 'Year (2011)',
- 'first': 'First letter (M)',
- 'quality': 'Quality (720P)',
- 'video': 'Video (x264)',
- 'audio': 'Audio (DTS)',
- 'group': 'Releasegroup name',
- 'source': 'Source media (Bluray)',
- 'original': 'Original filename',
- 'original_folder': 'Original foldername',
- 'imdb_id': 'IMDB id (tt0123456)',
- 'cd': 'CD number (cd1)',
- 'cd_nr': 'Just the cd nr. (1)',
- },
-}
-
-config = [{
- 'name': 'renamer',
- 'order': 40,
- 'description': 'Move and rename your downloaded movies to your movie directory.',
- 'groups': [
- {
- 'tab': 'renamer',
- 'name': 'renamer',
- 'label': 'Rename downloaded movies',
- 'wizard': True,
- 'options': [
- {
- 'name': 'enabled',
- 'default': False,
- 'type': 'enabler',
- },
- {
- 'name': 'from',
- 'type': 'directory',
- 'description': 'Folder where CP searches for movies.',
- },
- {
- 'name': 'to',
- 'type': 'directory',
- 'description': 'Folder where the movies should be moved to.',
- },
- {
- 'name': 'folder_name',
- 'label': 'Folder naming',
- 'description': 'Name of the folder. Keep empty for no folder.',
- 'default': ' ()',
- 'type': 'choice',
- 'options': rename_options
- },
- {
- 'name': 'file_name',
- 'label': 'File naming',
- 'description': 'Name of the file',
- 'default': '.',
- 'type': 'choice',
- 'options': rename_options
- },
- {
- 'name': 'cleanup',
- 'type': 'bool',
- 'description': 'Cleanup leftover files after successful rename.',
- 'default': False,
- },
- {
- 'advanced': True,
- 'name': 'run_every',
- 'label': 'Run every',
- 'default': 1,
- 'type': 'int',
- 'unit': 'min(s)',
- 'description': 'Detect movie status every X minutes. Will start the renamer if movie is completed or handle failed download if these options are enabled',
- },
- {
- 'advanced': True,
- 'name': 'force_every',
- 'label': 'Force every',
- 'default': 2,
- 'type': 'int',
- 'unit': 'hour(s)',
- 'description': 'Forces the renamer to scan every X hours',
- },
- {
- 'advanced': True,
- 'name': 'next_on_failed',
- 'default': True,
- 'type': 'bool',
- 'description': 'Try the next best release for a movie after a download failed.',
- },
- {
- 'name': 'move_leftover',
- 'type': 'bool',
- 'description': 'Move all leftover file after renaming, to the movie folder.',
- 'default': False,
- 'advanced': True,
- },
- {
- 'advanced': True,
- 'name': 'separator',
- 'label': 'Separator',
- 'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.',
- },
- {
- 'advanced': True,
- 'name': 'ntfs_permission',
- 'label': 'NTFS Permission',
- 'type': 'bool',
- 'hidden': os.name != 'nt',
- 'description': 'Set permission of moved files to that of destination folder (Windows NTFS only).',
- 'default': False,
- },
- ],
- }, {
- 'tab': 'renamer',
- 'name': 'meta_renamer',
- 'label': 'Advanced renaming',
- 'description': 'Meta data file renaming. Use <filename> to use the above "File naming" settings, without the file extention.',
- 'advanced': True,
- 'options': [
- {
- 'name': 'rename_nfo',
- 'label': 'Rename .NFO',
- 'description': 'Rename original .nfo file',
- 'type': 'bool',
- 'default': True,
- },
- {
- 'name': 'nfo_name',
- 'label': 'NFO naming',
- 'default': '.orig.',
- 'type': 'choice',
- 'options': rename_options
- },
- ],
- },
- ],
-}]
diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py
deleted file mode 100644
index 7df93b83bd..0000000000
--- a/couchpotato/core/plugins/renamer/main.py
+++ /dev/null
@@ -1,602 +0,0 @@
-from couchpotato import get_session
-from couchpotato.api import addApiView
-from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
-from couchpotato.core.helpers.encoding import toUnicode, ss
-from couchpotato.core.helpers.request import jsonified
-from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
- getImdb
-from couchpotato.core.logger import CPLog
-from couchpotato.core.plugins.base import Plugin
-from couchpotato.core.settings.model import Library, File, Profile, Release
-from couchpotato.environment import Env
-import errno
-import os
-import re
-import shutil
-import traceback
-
-log = CPLog(__name__)
-
-
-class Renamer(Plugin):
-
- renaming_started = False
- checking_snatched = False
-
- def __init__(self):
-
- addApiView('renamer.scan', self.scanView, docs = {
- 'desc': 'For the renamer to check for new files to rename',
- })
-
- addEvent('renamer.scan', self.scan)
- addEvent('renamer.check_snatched', self.checkSnatched)
-
- addEvent('app.load', self.scan)
- addEvent('app.load', self.checkSnatched)
-
- if self.conf('run_every') > 0:
- fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'))
-
- if self.conf('force_every') > 0:
- fireEvent('schedule.interval', 'renamer.check_snatched_forced', self.scan, hours = self.conf('force_every'))
-
- def scanView(self):
-
- fireEventAsync('renamer.scan')
-
- return jsonified({
- 'success': True
- })
-
- def scan(self):
-
- if self.isDisabled():
- return
-
- if self.renaming_started is True:
- log.info('Renamer is already running, if you see this often, check the logs above for errors.')
- return
-
- # Check to see if the "to" folder is inside the "from" folder.
- if not os.path.isdir(self.conf('from')) or not os.path.isdir(self.conf('to')):
- log.debug('"To" and "From" have to exist.')
- return
- elif self.conf('from') in self.conf('to'):
- log.error('The "to" can\'t be inside of the "from" folder. You\'ll get an infinite loop.')
- return
-
- groups = fireEvent('scanner.scan', folder = self.conf('from'), single = True)
-
- self.renaming_started = True
-
- destination = self.conf('to')
- folder_name = self.conf('folder_name')
- file_name = self.conf('file_name')
- trailer_name = self.conf('trailer_name')
- nfo_name = self.conf('nfo_name')
- separator = self.conf('separator')
-
- # Statusses
- done_status = fireEvent('status.get', 'done', single = True)
- active_status = fireEvent('status.get', 'active', single = True)
- downloaded_status = fireEvent('status.get', 'downloaded', single = True)
- snatched_status = fireEvent('status.get', 'snatched', single = True)
-
- db = get_session()
-
- for group_identifier in groups:
-
- group = groups[group_identifier]
- rename_files = {}
- remove_files = []
- remove_releases = []
-
- movie_title = getTitle(group['library'])
-
- # Add _UNKNOWN_ if no library item is connected
- if not group['library'] or not movie_title:
- self.tagDir(group, 'unknown')
- continue
- # Rename the files using the library data
- else:
- group['library'] = fireEvent('library.update', identifier = group['library']['identifier'], single = True)
- if not group['library']:
- log.error('Could not rename, no library item to work with: %s', group_identifier)
- continue
-
- library = group['library']
- movie_title = getTitle(library)
-
- # Find subtitle for renaming
- fireEvent('renamer.before', group)
-
- # Remove weird chars from moviename
- movie_name = re.sub(r"[\x00\/\\:\*\?\"<>\|]", '', movie_title)
-
- # Put 'The' at the end
- name_the = movie_name
- if movie_name[:4].lower() == 'the ':
- name_the = movie_name[4:] + ', The'
-
- replacements = {
- 'ext': 'mkv',
- 'namethe': name_the.strip(),
- 'thename': movie_name.strip(),
- 'year': library['year'],
- 'first': name_the[0].upper(),
- 'quality': group['meta_data']['quality']['label'],
- 'quality_type': group['meta_data']['quality_type'],
- 'video': group['meta_data'].get('video'),
- 'audio': group['meta_data'].get('audio'),
- 'group': group['meta_data']['group'],
- 'source': group['meta_data']['source'],
- 'resolution_width': group['meta_data'].get('resolution_width'),
- 'resolution_height': group['meta_data'].get('resolution_height'),
- 'imdb_id': library['identifier'],
- 'cd': '',
- 'cd_nr': '',
- }
-
- for file_type in group['files']:
-
- # Move nfo depending on settings
- if file_type is 'nfo' and not self.conf('rename_nfo'):
- log.debug('Skipping, renaming of %s disabled', file_type)
- if self.conf('cleanup'):
- for current_file in group['files'][file_type]:
- remove_files.append(current_file)
- continue
-
- # Subtitle extra
- if file_type is 'subtitle_extra':
- continue
-
- # Move other files
- multiple = len(group['files'][file_type]) > 1 and not group['is_dvd']
- cd = 1 if multiple else 0
-
- for current_file in sorted(list(group['files'][file_type])):
-
- # Original filename
- replacements['original'] = os.path.splitext(os.path.basename(current_file))[0]
- replacements['original_folder'] = fireEvent('scanner.remove_cptag', group['dirname'], single = True)
-
- # Extension
- replacements['ext'] = getExt(current_file)
-
- # cd #
- replacements['cd'] = ' cd%d' % cd if multiple else ''
- replacements['cd_nr'] = cd if multiple else ''
-
- # Naming
- final_folder_name = self.doReplace(folder_name, replacements).lstrip('. ')
- final_file_name = self.doReplace(file_name, replacements).lstrip('. ')
- replacements['filename'] = final_file_name[:-(len(getExt(final_file_name)) + 1)]
-
- # Meta naming
- if file_type is 'trailer':
- final_file_name = self.doReplace(trailer_name, replacements, remove_multiple = True).lstrip('. ')
- elif file_type is 'nfo':
- final_file_name = self.doReplace(nfo_name, replacements, remove_multiple = True).lstrip('. ')
-
- # Seperator replace
- if separator:
- final_file_name = final_file_name.replace(' ', separator)
-
- # Move DVD files (no structure renaming)
- if group['is_dvd'] and file_type is 'movie':
- found = False
- for top_dir in ['video_ts', 'audio_ts', 'bdmv', 'certificate']:
- has_string = current_file.lower().find(os.path.sep + top_dir + os.path.sep)
- if has_string >= 0:
- structure_dir = current_file[has_string:].lstrip(os.path.sep)
- rename_files[current_file] = os.path.join(destination, final_folder_name, structure_dir)
- found = True
- break
-
- if not found:
- log.error('Could not determine dvd structure for: %s', current_file)
-
- # Do rename others
- else:
- if file_type is 'leftover':
- if self.conf('move_leftover'):
- rename_files[current_file] = os.path.join(destination, final_folder_name, os.path.basename(current_file))
- elif file_type not in ['subtitle']:
- rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
-
- # Check for extra subtitle files
- if file_type is 'subtitle':
-
- remove_multiple = False
- if len(group['files']['movie']) == 1:
- remove_multiple = True
-
- sub_langs = group['subtitle_language'].get(current_file, [])
-
- # rename subtitles with or without language
- sub_name = self.doReplace(file_name, replacements, remove_multiple = remove_multiple)
- rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name)
-
- rename_extras = self.getRenameExtras(
- extra_type = 'subtitle_extra',
- replacements = replacements,
- folder_name = folder_name,
- file_name = file_name,
- destination = destination,
- group = group,
- current_file = current_file,
- remove_multiple = remove_multiple,
- )
-
- # Don't add language if multiple languages in 1 subtitle file
- if len(sub_langs) == 1:
- sub_name = final_file_name.replace(replacements['ext'], '%s.%s' % (sub_langs[0], replacements['ext']))
- rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name)
-
- rename_files = mergeDicts(rename_files, rename_extras)
-
- # Filename without cd etc
- elif file_type is 'movie':
- rename_extras = self.getRenameExtras(
- extra_type = 'movie_extra',
- replacements = replacements,
- folder_name = folder_name,
- file_name = file_name,
- destination = destination,
- group = group,
- current_file = current_file
- )
- rename_files = mergeDicts(rename_files, rename_extras)
-
- group['filename'] = self.doReplace(file_name, replacements, remove_multiple = True)[:-(len(getExt(final_file_name)) + 1)]
- group['destination_dir'] = os.path.join(destination, final_folder_name)
-
- if multiple:
- cd += 1
-
- # Before renaming, remove the lower quality files
- library = db.query(Library).filter_by(identifier = group['library']['identifier']).first()
- remove_leftovers = True
-
- # Add it to the wanted list before we continue
- if len(library.movies) == 0:
- profile = db.query(Profile).filter_by(core = True, label = group['meta_data']['quality']['label']).first()
- fireEvent('movie.add', params = {'identifier': group['library']['identifier'], 'profile_id': profile.id}, search_after = False)
- db.expire_all()
- library = db.query(Library).filter_by(identifier = group['library']['identifier']).first()
-
- for movie in library.movies:
-
- # Mark movie "done" onces it found the quality with the finish check
- try:
- if movie.status_id == active_status.get('id') and movie.profile:
- for profile_type in movie.profile.types:
- if profile_type.quality_id == group['meta_data']['quality']['id'] and profile_type.finish:
- movie.status_id = done_status.get('id')
- db.commit()
- except Exception, e:
- log.error('Failed marking movie finished: %s %s', (e, traceback.format_exc()))
-
- # Go over current movie releases
- for release in movie.releases:
-
- # When a release already exists
- if release.status_id is done_status.get('id'):
-
- # This is where CP removes older, lesser quality releases
- if release.quality.order > group['meta_data']['quality']['order']:
- log.info('Removing lesser quality %s for %s.', (movie.library.titles[0].title, release.quality.label))
- for current_file in release.files:
- remove_files.append(current_file)
- remove_releases.append(release)
- # Same quality, but still downloaded, so maybe repack/proper/unrated/directors cut etc
- elif release.quality.order is group['meta_data']['quality']['order']:
- log.info('Same quality release already exists for %s, with quality %s. Assuming repack.', (movie.library.titles[0].title, release.quality.label))
- for current_file in release.files:
- remove_files.append(current_file)
- remove_releases.append(release)
-
- # Downloaded a lower quality, rename the newly downloaded files/folder to exclude them from scan
- else:
- log.info('Better quality release already exists for %s, with quality %s', (movie.library.titles[0].title, release.quality.label))
-
- # Add _EXISTS_ to the parent dir
- self.tagDir(group, 'exists')
-
- # Notify on rename fail
- download_message = 'Renaming of %s (%s) canceled, exists in %s already.' % (movie.library.titles[0].title, group['meta_data']['quality']['label'], release.quality.label)
- fireEvent('movie.renaming.canceled', message = download_message, data = group)
- remove_leftovers = False
-
- break
- elif release.status_id is snatched_status.get('id'):
- if release.quality.id is group['meta_data']['quality']['id']:
- log.debug('Marking release as downloaded')
- try:
- release.status_id = downloaded_status.get('id')
- except Exception, e:
- log.error('Failed marking release as finished: %s %s', (e, traceback.format_exc()))
- db.commit()
-
- # Remove leftover files
- if self.conf('cleanup') and not self.conf('move_leftover') and remove_leftovers:
- log.debug('Removing leftover files')
- for current_file in group['files']['leftover']:
- remove_files.append(current_file)
- elif not remove_leftovers: # Don't remove anything
- break
-
- # Remove files
- delete_folders = []
- for src in remove_files:
-
- if isinstance(src, File):
- src = src.path
-
- if rename_files.get(src):
- log.debug('Not removing file that will be renamed: %s', src)
- continue
-
- log.info('Removing "%s"', src)
- try:
- src = ss(src)
- if os.path.isfile(src):
- os.remove(src)
-
- parent_dir = os.path.normpath(os.path.dirname(src))
- if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and destination != parent_dir:
- delete_folders.append(parent_dir)
-
- except:
- log.error('Failed removing %s: %s', (src, traceback.format_exc()))
- self.tagDir(group, 'failed_remove')
-
- # Delete leftover folder from older releases
- for delete_folder in delete_folders:
- try:
- self.deleteEmptyFolder(delete_folder, show_error = False)
- except Exception, e:
- log.error('Failed to delete folder: %s %s', (e, traceback.format_exc()))
-
- # Rename all files marked
- group['renamed_files'] = []
- for src in rename_files:
- if rename_files[src]:
- dst = rename_files[src]
- log.info('Renaming "%s" to "%s"', (src, dst))
-
- # Create dir
- self.makeDir(os.path.dirname(dst))
-
- try:
- self.moveFile(src, dst)
- group['renamed_files'].append(dst)
- except:
- log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc()))
- self.tagDir(group, 'failed_rename')
-
- # Remove matching releases
- for release in remove_releases:
- log.debug('Removing release %s', release.identifier)
- try:
- db.delete(release)
- except:
- log.error('Failed removing %s: %s', (release.identifier, traceback.format_exc()))
-
- if group['dirname'] and group['parentdir']:
- try:
- log.info('Deleting folder: %s', group['parentdir'])
- self.deleteEmptyFolder(group['parentdir'])
- except:
- log.error('Failed removing %s: %s', (group['parentdir'], traceback.format_exc()))
-
- # Notify on download, search for trailers etc
- download_message = 'Downloaded %s (%s)' % (movie_title, replacements['quality'])
- try:
- fireEvent('renamer.after', message = download_message, group = group, in_order = True)
- except:
- log.error('Failed firing (some) of the renamer.after events: %s', traceback.format_exc())
-
- # Break if CP wants to shut down
- if self.shuttingDown():
- break
-
- self.renaming_started = False
-
- def getRenameExtras(self, extra_type = '', replacements = {}, folder_name = '', file_name = '', destination = '', group = {}, current_file = '', remove_multiple = False):
-
- replacements = replacements.copy()
- rename_files = {}
-
- def test(s):
- return current_file[:-len(replacements['ext'])] in s
-
- for extra in set(filter(test, group['files'][extra_type])):
- replacements['ext'] = getExt(extra)
-
- final_folder_name = self.doReplace(folder_name, replacements, remove_multiple = remove_multiple)
- final_file_name = self.doReplace(file_name, replacements, remove_multiple = remove_multiple)
- rename_files[extra] = os.path.join(destination, final_folder_name, final_file_name)
-
- return rename_files
-
- def tagDir(self, group, tag):
-
- rename_files = {}
-
- if group['dirname']:
- rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_%s_%s' % (tag.upper(), group['dirname']))
- else: # Add it to filename
- for file_type in group['files']:
- for rename_me in group['files'][file_type]:
- filename = os.path.basename(rename_me)
- rename_files[rename_me] = rename_me.replace(filename, '_%s_%s' % (tag.upper(), filename))
-
- for src in rename_files:
- if rename_files[src]:
- dst = rename_files[src]
- log.info('Renaming "%s" to "%s"', (src, dst))
-
- # Create dir
- self.makeDir(os.path.dirname(dst))
-
- try:
- self.moveFile(src, dst)
- except:
- log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc()))
- raise
-
- def moveFile(self, old, dest):
- dest = ss(dest)
- try:
- shutil.move(old, dest)
-
- try:
- os.chmod(dest, Env.getPermission('file'))
- if os.name == 'nt' and self.conf('ntfs_permission'):
- os.popen('icacls "' + dest + '"* /reset /T')
- except:
- log.error('Failed setting permissions for file: %s, %s', (dest, traceback.format_exc(1)))
-
- except OSError, err:
- # Copying from a filesystem with octal permission to an NTFS file system causes a permission error. In this case ignore it.
- if not hasattr(os, 'chmod') or err.errno != errno.EPERM:
- raise
- else:
- if os.path.exists(dest):
- os.unlink(old)
-
- except:
- log.error('Couldn\'t move file "%s" to "%s": %s', (old, dest, traceback.format_exc()))
- raise
-
- return True
-
- def doReplace(self, string, replacements, remove_multiple = False):
- '''
- replace confignames with the real thing
- '''
-
- replacements = replacements.copy()
- if remove_multiple:
- replacements['cd'] = ''
- replacements['cd_nr'] = ''
-
- replaced = toUnicode(string)
- for x, r in replacements.iteritems():
- if r is not None:
- replaced = replaced.replace(u'<%s>' % toUnicode(x), toUnicode(r))
- else:
- #If information is not available, we don't want the tag in the filename
- replaced = replaced.replace('<' + x + '>', '')
-
- replaced = re.sub(r"[\x00:\*\?\"<>\|]", '', replaced)
-
- sep = self.conf('separator')
- return self.replaceDoubles(replaced).replace(' ', ' ' if not sep else sep)
-
- def replaceDoubles(self, string):
- return string.replace(' ', ' ').replace(' .', '.')
-
- def deleteEmptyFolder(self, folder, show_error = True):
- folder = ss(folder)
-
- loge = log.error if show_error else log.debug
- for root, dirs, files in os.walk(folder):
-
- for dir_name in dirs:
- full_path = os.path.join(root, dir_name)
- if len(os.listdir(full_path)) == 0:
- try:
- os.rmdir(full_path)
- except:
- loge('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc()))
-
- try:
- os.rmdir(folder)
- except:
- loge('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc()))
-
- def checkSnatched(self):
- if self.checking_snatched:
- log.debug('Already checking snatched')
-
- self.checking_snatched = True
-
- snatched_status = fireEvent('status.get', 'snatched', single = True)
- ignored_status = fireEvent('status.get', 'ignored', single = True)
- failed_status = fireEvent('status.get', 'failed', single = True)
-
- done_status = fireEvent('status.get', 'done', single = True)
-
- db = get_session()
- rels = db.query(Release).filter_by(status_id = snatched_status.get('id')).all()
-
- scan_required = False
-
- if rels:
- self.checking_snatched = True
- log.debug('Checking status snatched releases...')
-
- statuses = fireEvent('download.status', merge = True)
- if not statuses:
- log.debug('Download status functionality is not implemented for active downloaders.')
- scan_required = True
- else:
- try:
- for rel in rels:
- rel_dict = rel.to_dict({'info': {}})
-
- # Get current selected title
- default_title = getTitle(rel.movie.library)
-
- # Check if movie has already completed and is manage tab (legacy db correction)
- if rel.movie.status_id == done_status.get('id'):
- log.debug('Found a completed movie with a snatched release : %s. Setting release status to ignored...' , default_title)
- rel.status_id = ignored_status.get('id')
- db.commit()
- continue
-
- movie_dict = fireEvent('movie.get', rel.movie_id, single = True)
-
- # check status
- nzbname = self.createNzbName(rel_dict['info'], movie_dict)
-
- found = False
- for item in statuses:
- if item['name'] == nzbname or rel_dict['info']['name'] in item['name'] or getImdb(item['name']) == movie_dict['library']['identifier']:
-
- timeleft = 'N/A' if item['timeleft'] == -1 else item['timeleft']
- log.debug('Found %s: %s, time to go: %s', (item['name'], item['status'].upper(), timeleft))
-
- if item['status'] == 'busy':
- pass
- elif item['status'] == 'failed':
- fireEvent('download.remove_failed', item, single = True)
-
- if self.conf('next_on_failed'):
- fireEvent('searcher.try_next_release', movie_id = rel.movie_id)
- else:
- rel.status_id = failed_status.get('id')
- db.commit()
- elif item['status'] == 'completed':
- log.info('Download of %s completed!', item['name'])
- scan_required = True
-
- found = True
- break
-
- if not found:
- log.info('%s not found in downloaders', nzbname)
-
- except:
- log.error('Failed checking for release in downloader: %s', traceback.format_exc())
-
- if scan_required:
- fireEvent('renamer.scan')
-
- self.checking_snatched = False
-
- return True
diff --git a/couchpotato/core/plugins/scanner.py b/couchpotato/core/plugins/scanner.py
new file mode 100644
index 0000000000..04b90a4833
--- /dev/null
+++ b/couchpotato/core/plugins/scanner.py
@@ -0,0 +1,972 @@
+import os
+import re
+import threading
+import time
+import traceback
+
+from couchpotato import get_db
+from couchpotato.core.event import fireEvent, addEvent
+from couchpotato.core.helpers.encoding import toUnicode, simplifyString, sp, ss
+from couchpotato.core.helpers.variable import getExt, getImdb, tryInt, \
+ splitString, getIdentifier
+from couchpotato.core.logger import CPLog
+from couchpotato.core.plugins.base import Plugin
+from guessit import guess_movie_info
+from subliminal.videos import Video
+import enzyme
+from six.moves import filter, map, zip
+
+
+log = CPLog(__name__)
+
+autoload = 'Scanner'
+
+
+class Scanner(Plugin):
+
+ ignored_in_path = [os.path.sep + 'extracted' + os.path.sep, 'extracting', '_unpack', '_failed_', '_unknown_', '_exists_', '_failed_remove_',
+ '_failed_rename_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo',
+ 'thumbs.db', 'ehthumbs.db', 'desktop.ini'] # unpacking, smb-crap, hidden files
+ ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate']
+ extensions = {
+ 'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img', 'mdf', 'ts', 'm4v', 'flv'],
+ 'movie_extra': ['mds'],
+ 'dvd': ['vts_*', 'vob'],
+ 'nfo': ['nfo', 'txt', 'tag'],
+ 'subtitle': ['sub', 'srt', 'ssa', 'ass'],
+ 'subtitle_extra': ['idx'],
+ 'trailer': ['mov', 'mp4', 'flv']
+ }
+
+ threed_types = {
+ 'Half SBS': [('half', 'sbs'), ('h', 'sbs'), 'hsbs'],
+ 'Full SBS': [('full', 'sbs'), ('f', 'sbs'), 'fsbs'],
+ 'SBS': ['sbs'],
+ 'Half OU': [('half', 'ou'), ('h', 'ou'), 'hou'],
+ 'Full OU': [('full', 'ou'), ('h', 'ou'), 'fou'],
+ 'OU': ['ou'],
+ 'Frame Packed': ['mvc', ('complete', 'bluray')],
+ '3D': ['3d']
+ }
+
+ file_types = {
+ 'subtitle': ('subtitle', 'subtitle'),
+ 'subtitle_extra': ('subtitle', 'subtitle_extra'),
+ 'trailer': ('video', 'trailer'),
+ 'nfo': ('nfo', 'nfo'),
+ 'movie': ('video', 'movie'),
+ 'movie_extra': ('movie', 'movie_extra'),
+ 'backdrop': ('image', 'backdrop'),
+ 'poster': ('image', 'poster'),
+ 'thumbnail': ('image', 'thumbnail'),
+ 'leftover': ('leftover', 'leftover'),
+ }
+
+ file_sizes = { # in MB
+ 'movie': {'min': 200},
+ 'trailer': {'min': 2, 'max': 199},
+ 'backdrop': {'min': 0, 'max': 5},
+ }
+
+ codecs = {
+ 'audio': ['DTS', 'AC3', 'AC3D', 'MP3'],
+ 'video': ['x264', 'H264', 'DivX', 'Xvid']
+ }
+
+ resolutions = {
+ '1080p': {'resolution_width': 1920, 'resolution_height': 1080, 'aspect': 1.78},
+ '1080i': {'resolution_width': 1920, 'resolution_height': 1080, 'aspect': 1.78},
+ '720p': {'resolution_width': 1280, 'resolution_height': 720, 'aspect': 1.78},
+ '720i': {'resolution_width': 1280, 'resolution_height': 720, 'aspect': 1.78},
+ '480p': {'resolution_width': 640, 'resolution_height': 480, 'aspect': 1.33},
+ '480i': {'resolution_width': 640, 'resolution_height': 480, 'aspect': 1.33},
+ 'default': {'resolution_width': 0, 'resolution_height': 0, 'aspect': 1},
+ }
+
+ audio_codec_map = {
+ 0x2000: 'AC3',
+ 0x2001: 'DTS',
+ 0x0055: 'MP3',
+ 0x0050: 'MP2',
+ 0x0001: 'PCM',
+ 0x003: 'WAV',
+ 0x77a1: 'TTA1',
+ 0x5756: 'WAV',
+ 0x6750: 'Vorbis',
+ 0xF1AC: 'FLAC',
+ 0x00ff: 'AAC',
+ }
+
+ source_media = {
+ 'Blu-ray': ['bluray', 'blu-ray', 'brrip', 'br-rip'],
+ 'HD DVD': ['hddvd', 'hd-dvd'],
+ 'DVD': ['dvd'],
+ 'HDTV': ['hdtv']
+ }
+
+ clean = '([ _\,\.\(\)\[\]\-]|^)(3d|hsbs|sbs|half.sbs|full.sbs|ou|half.ou|full.ou|extended|extended.cut|directors.cut|french|fr|swedisch|sw|danish|dutch|nl|swesub|subs|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip' \
+ '|hdtvrip|webdl|web.dl|webrip|web.rip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|video_ts|audio_ts|480p|480i|576p|576i|720p|720i|1080p|1080i|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|hc|\[.*\])(?=[ _\,\.\(\)\[\]\-]|$)'
+ multipart_regex = [
+ '[ _\.-]+cd[ _\.-]*([0-9a-d]+)', #*cd1
+ '[ _\.-]+dvd[ _\.-]*([0-9a-d]+)', #*dvd1
+ '[ _\.-]+part[ _\.-]*([0-9a-d]+)', #*part1
+ '[ _\.-]+dis[ck][ _\.-]*([0-9a-d]+)', #*disk1
+ 'cd[ _\.-]*([0-9a-d]+)$', #cd1.ext
+ 'dvd[ _\.-]*([0-9a-d]+)$', #dvd1.ext
+ 'part[ _\.-]*([0-9a-d]+)$', #part1.mkv
+ 'dis[ck][ _\.-]*([0-9a-d]+)$', #disk1.mkv
+ '()[ _\.-]+([0-9]*[abcd]+)(\.....?)$',
+ '([a-z])([0-9]+)(\.....?)$',
+ '()([ab])(\.....?)$' #*a.mkv
+ ]
+
+ cp_imdb = '\.cp\((?Ptt[0-9]+),?\s?(?P[A-Za-z0-9]+)?\)'
+
+ def __init__(self):
+
+ addEvent('scanner.create_file_identifier', self.createStringIdentifier)
+ addEvent('scanner.remove_cptag', self.removeCPTag)
+
+ addEvent('scanner.scan', self.scan)
+ addEvent('scanner.name_year', self.getReleaseNameYear)
+ addEvent('scanner.partnumber', self.getPartNumber)
+
+ def scan(self, folder = None, files = None, release_download = None, simple = False, newer_than = 0, return_ignored = True, check_file_date = True, on_found = None):
+
+ folder = sp(folder)
+
+ if not folder or not os.path.isdir(folder):
+ log.error('Folder doesn\'t exists: %s', folder)
+ return {}
+
+ # Get movie "master" files
+ movie_files = {}
+ leftovers = []
+
+ # Scan all files of the folder if no files are set
+ if not files:
+ try:
+ files = []
+ for root, dirs, walk_files in os.walk(folder, followlinks=True):
+ files.extend([sp(os.path.join(sp(root), ss(filename))) for filename in walk_files])
+
+ # Break if CP wants to shut down
+ if self.shuttingDown():
+ break
+
+ except:
+ log.error('Failed getting files from %s: %s', (folder, traceback.format_exc()))
+
+ log.debug('Found %s files to scan and group in %s', (len(files), folder))
+ else:
+ check_file_date = False
+ files = [sp(x) for x in files]
+
+ for file_path in files:
+
+ if not os.path.exists(file_path):
+ continue
+
+ # Remove ignored files
+ if self.isSampleFile(file_path):
+ leftovers.append(file_path)
+ continue
+ elif not self.keepFile(file_path):
+ continue
+
+ is_dvd_file = self.isDVDFile(file_path)
+ if self.filesizeBetween(file_path, self.file_sizes['movie']) or is_dvd_file: # Minimal 300MB files or is DVD file
+
+ # Normal identifier
+ identifier = self.createStringIdentifier(file_path, folder, exclude_filename = is_dvd_file)
+ identifiers = [identifier]
+
+ # Identifier with quality
+ quality = fireEvent('quality.guess', files = [file_path], size = self.getFileSize(file_path), single = True) if not is_dvd_file else {'identifier':'dvdr'}
+ if quality:
+ identifier_with_quality = '%s %s' % (identifier, quality.get('identifier', ''))
+ identifiers = [identifier_with_quality, identifier]
+
+ if not movie_files.get(identifier):
+ movie_files[identifier] = {
+ 'unsorted_files': [],
+ 'identifiers': identifiers,
+ 'is_dvd': is_dvd_file,
+ }
+
+ movie_files[identifier]['unsorted_files'].append(file_path)
+ else:
+ leftovers.append(file_path)
+
+ # Break if CP wants to shut down
+ if self.shuttingDown():
+ break
+
+ # Cleanup
+ del files
+
+ # Sort reverse, this prevents "Iron man 2" from getting grouped with "Iron man" as the "Iron Man 2"
+ # files will be grouped first.
+ leftovers = set(sorted(leftovers, reverse = True))
+
+ # Group files minus extension
+ ignored_identifiers = []
+ for identifier, group in movie_files.items():
+ if identifier not in group['identifiers'] and len(identifier) > 0: group['identifiers'].append(identifier)
+
+ log.debug('Grouping files: %s', identifier)
+
+ has_ignored = 0
+ for file_path in list(group['unsorted_files']):
+ ext = getExt(file_path)
+ wo_ext = file_path[:-(len(ext) + 1)]
+ found_files = set([i for i in leftovers if wo_ext in i])
+ group['unsorted_files'].extend(found_files)
+ leftovers = leftovers - found_files
+
+ has_ignored += 1 if ext == 'ignore' else 0
+
+ if has_ignored == 0:
+ for file_path in list(group['unsorted_files']):
+ ext = getExt(file_path)
+ has_ignored += 1 if ext == 'ignore' else 0
+
+ if has_ignored > 0:
+ ignored_identifiers.append(identifier)
+
+ # Break if CP wants to shut down
+ if self.shuttingDown():
+ break
+
+
+ # Create identifiers for all leftover files
+ path_identifiers = {}
+ for file_path in leftovers:
+ identifier = self.createStringIdentifier(file_path, folder)
+
+ if not path_identifiers.get(identifier):
+ path_identifiers[identifier] = []
+
+ path_identifiers[identifier].append(file_path)
+
+
+ # Group the files based on the identifier
+ delete_identifiers = []
+ for identifier, found_files in path_identifiers.items():
+ log.debug('Grouping files on identifier: %s', identifier)
+
+ group = movie_files.get(identifier)
+ if group:
+ group['unsorted_files'].extend(found_files)
+ delete_identifiers.append(identifier)
+
+ # Remove the found files from the leftover stack
+ leftovers = leftovers - set(found_files)
+
+ # Break if CP wants to shut down
+ if self.shuttingDown():
+ break
+
+ # Cleaning up used
+ for identifier in delete_identifiers:
+ if path_identifiers.get(identifier):
+ del path_identifiers[identifier]
+ del delete_identifiers
+
+ # Group based on folder
+ delete_identifiers = []
+ for identifier, found_files in path_identifiers.items():
+ log.debug('Grouping files on foldername: %s', identifier)
+
+ for ff in found_files:
+ new_identifier = self.createStringIdentifier(os.path.dirname(ff), folder)
+
+ group = movie_files.get(new_identifier)
+ if group:
+ group['unsorted_files'].extend([ff])
+ delete_identifiers.append(identifier)
+
+ # Remove the found files from the leftover stack
+ leftovers -= leftovers - set([ff])
+
+ # Break if CP wants to shut down
+ if self.shuttingDown():
+ break
+
+ # leftovers should be empty
+ if leftovers:
+ log.debug('Some files are still left over: %s', leftovers)
+
+ # Cleaning up used
+ for identifier in delete_identifiers:
+ if path_identifiers.get(identifier):
+ del path_identifiers[identifier]
+ del delete_identifiers
+
+ # Make sure we remove older / still extracting files
+ valid_files = {}
+ while True and not self.shuttingDown():
+ try:
+ identifier, group = movie_files.popitem()
+ except:
+ break
+
+ # Check if movie is fresh and maybe still unpacking, ignore files newer than 1 minute
+ if check_file_date:
+ files_too_new, time_string = self.checkFilesChanged(group['unsorted_files'])
+ if files_too_new:
+ log.info('Files seem to be still unpacking or just unpacked (created on %s), ignoring for now: %s', (time_string, identifier))
+
+ # Delete the unsorted list
+ del group['unsorted_files']
+
+ continue
+
+ # Only process movies newer than x
+ if newer_than and newer_than > 0:
+ has_new_files = False
+ for cur_file in group['unsorted_files']:
+ file_time = self.getFileTimes(cur_file)
+ if file_time[0] > newer_than or file_time[1] > newer_than:
+ has_new_files = True
+ break
+
+ if not has_new_files:
+ log.debug('None of the files have changed since %s for %s, skipping.', (time.ctime(newer_than), identifier))
+
+ # Delete the unsorted list
+ del group['unsorted_files']
+
+ continue
+
+ valid_files[identifier] = group
+
+ del movie_files
+
+ total_found = len(valid_files)
+
+ # Make sure only one movie was found if a download ID is provided
+ if release_download and total_found == 0:
+ log.info('Download ID provided (%s), but no groups found! Make sure the download contains valid media files (fully extracted).', release_download.get('imdb_id'))
+ elif release_download and total_found > 1:
+ log.info('Download ID provided (%s), but more than one group found (%s). Ignoring Download ID...', (release_download.get('imdb_id'), len(valid_files)))
+ release_download = None
+
+ # Determine file types
+ processed_movies = {}
+ while True and not self.shuttingDown():
+ try:
+ identifier, group = valid_files.popitem()
+ except:
+ break
+
+ if return_ignored is False and identifier in ignored_identifiers:
+ log.debug('Ignore file found, ignoring release: %s', identifier)
+ total_found -= 1
+ continue
+
+ # Group extra (and easy) files first
+ group['files'] = {
+ 'movie_extra': self.getMovieExtras(group['unsorted_files']),
+ 'subtitle': self.getSubtitles(group['unsorted_files']),
+ 'subtitle_extra': self.getSubtitlesExtras(group['unsorted_files']),
+ 'nfo': self.getNfo(group['unsorted_files']),
+ 'trailer': self.getTrailers(group['unsorted_files']),
+ 'leftover': set(group['unsorted_files']),
+ }
+
+ # Media files
+ if group['is_dvd']:
+ group['files']['movie'] = self.getDVDFiles(group['unsorted_files'])
+ else:
+ group['files']['movie'] = self.getMediaFiles(group['unsorted_files'])
+
+ if len(group['files']['movie']) == 0:
+ log.error('Couldn\'t find any movie files for %s', identifier)
+ total_found -= 1
+ continue
+
+ log.debug('Getting metadata for %s', identifier)
+ group['meta_data'] = self.getMetaData(group, folder = folder, release_download = release_download)
+
+ # Subtitle meta
+ group['subtitle_language'] = self.getSubtitleLanguage(group) if not simple else {}
+
+ # Get parent dir from movie files
+ for movie_file in group['files']['movie']:
+ group['parentdir'] = os.path.dirname(movie_file)
+ group['dirname'] = None
+
+ folder_names = group['parentdir'].replace(folder, '').split(os.path.sep)
+ folder_names.reverse()
+
+ # Try and get a proper dirname, so no "A", "Movie", "Download" etc
+ for folder_name in folder_names:
+ if folder_name.lower() not in self.ignore_names and len(folder_name) > 2:
+ group['dirname'] = folder_name
+ break
+
+ break
+
+ # Leftover "sorted" files
+ for file_type in group['files']:
+ if not file_type is 'leftover':
+ group['files']['leftover'] -= set(group['files'][file_type])
+ group['files'][file_type] = list(group['files'][file_type])
+ group['files']['leftover'] = list(group['files']['leftover'])
+
+ # Delete the unsorted list
+ del group['unsorted_files']
+
+ # Determine movie
+ group['media'] = self.determineMedia(group, release_download = release_download)
+ if not group['media']:
+ log.error('Unable to determine media: %s', group['identifiers'])
+ else:
+ group['identifier'] = getIdentifier(group['media']) or group['media']['info'].get('imdb')
+
+ processed_movies[identifier] = group
+
+ # Notify parent & progress on something found
+ if on_found:
+ on_found(group, total_found, len(valid_files))
+
+ # Wait for all the async events calm down a bit
+ while threading.activeCount() > 100 and not self.shuttingDown():
+ log.debug('Too many threads active, waiting a few seconds')
+ time.sleep(10)
+
+ if len(processed_movies) > 0:
+ log.info('Found %s movies in the folder %s', (len(processed_movies), folder))
+ else:
+ log.debug('Found no movies in the folder %s', folder)
+
+ return processed_movies
+
+ def getMetaData(self, group, folder = '', release_download = None):
+
+ data = {}
+ files = list(group['files']['movie'])
+
+ for cur_file in files:
+ if not self.filesizeBetween(cur_file, self.file_sizes['movie']): continue # Ignore smaller files
+
+ if not data.get('audio'): # Only get metadata from first media file
+ meta = self.getMeta(cur_file)
+
+ try:
+ data['titles'] = meta.get('titles', [])
+ data['video'] = meta.get('video', self.getCodec(cur_file, self.codecs['video']))
+ data['audio'] = meta.get('audio', self.getCodec(cur_file, self.codecs['audio']))
+ data['audio_channels'] = meta.get('audio_channels', 2.0)
+ if meta.get('resolution_width'):
+ data['resolution_width'] = meta.get('resolution_width')
+ data['resolution_height'] = meta.get('resolution_height')
+ data['aspect'] = round(float(meta.get('resolution_width')) / meta.get('resolution_height', 1), 2)
+ else:
+ data.update(self.getResolution(cur_file))
+ except:
+ log.debug('Error parsing metadata: %s %s', (cur_file, traceback.format_exc()))
+ pass
+
+ data['size'] = data.get('size', 0) + self.getFileSize(cur_file)
+
+ data['quality'] = None
+ quality = fireEvent('quality.guess', size = data.get('size'), files = files, extra = data, single = True)
+
+ # Use the quality that we snatched but check if it matches our guess
+ if release_download and release_download.get('quality'):
+ data['quality'] = fireEvent('quality.single', release_download.get('quality'), single = True)
+ data['quality']['is_3d'] = release_download.get('is_3d', 0)
+ if data['quality']['identifier'] != quality['identifier']:
+ log.info('Different quality snatched than detected for %s: %s vs. %s. Assuming snatched quality is correct.', (files[0], data['quality']['identifier'], quality['identifier']))
+ if data['quality']['is_3d'] != quality['is_3d']:
+ log.info('Different 3d snatched than detected for %s: %s vs. %s. Assuming snatched 3d is correct.', (files[0], data['quality']['is_3d'], quality['is_3d']))
+
+ if not data['quality']:
+ data['quality'] = quality
+
+ if not data['quality']:
+ data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True)
+
+ data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 or data['quality'].get('hd') else 'SD'
+
+ filename = re.sub(self.cp_imdb, '', files[0])
+ data['group'] = self.getGroup(filename[len(folder):])
+ data['source'] = self.getSourceMedia(filename)
+ if data['quality'].get('is_3d', 0):
+ data['3d_type'] = self.get3dType(filename)
+ return data
+
+ def get3dType(self, filename):
+ filename = ss(filename)
+
+ words = re.split('\W+', filename.lower())
+
+ for key in self.threed_types:
+ tags = self.threed_types.get(key, [])
+
+ for tag in tags:
+ if (isinstance(tag, tuple) and '.'.join(tag) in '.'.join(words)) or (isinstance(tag, (str, unicode)) and ss(tag.lower()) in words):
+ log.debug('Found %s in %s', (tag, filename))
+ return key
+
+ return ''
+
+ def getMeta(self, filename):
+
+ try:
+ p = enzyme.parse(filename)
+
+ # Video codec
+ vc = ('H264' if p.video[0].codec == 'AVC1' else p.video[0].codec)
+
+ # Audio codec
+ ac = p.audio[0].codec
+ try: ac = self.audio_codec_map.get(p.audio[0].codec)
+ except: pass
+
+ # Find title in video headers
+ titles = []
+
+ try:
+ if p.title and self.findYear(p.title):
+ titles.append(ss(p.title))
+ except:
+ log.error('Failed getting title from meta: %s', traceback.format_exc())
+
+ for video in p.video:
+ try:
+ if video.title and self.findYear(video.title):
+ titles.append(ss(video.title))
+ except:
+ log.error('Failed getting title from meta: %s', traceback.format_exc())
+
+ return {
+ 'titles': list(set(titles)),
+ 'video': vc,
+ 'audio': ac,
+ 'resolution_width': tryInt(p.video[0].width),
+ 'resolution_height': tryInt(p.video[0].height),
+ 'audio_channels': p.audio[0].channels,
+ }
+ except enzyme.exceptions.ParseError:
+ log.debug('Failed to parse meta for %s', filename)
+ except enzyme.exceptions.NoParserError:
+ log.debug('No parser found for %s', filename)
+ except:
+ log.debug('Failed parsing %s', filename)
+
+ return {}
+
+ def getSubtitleLanguage(self, group):
+ detected_languages = {}
+
+ # Subliminal scanner
+ paths = None
+ try:
+ paths = group['files']['movie']
+ scan_result = []
+ for p in paths:
+ if not group['is_dvd']:
+ video = Video.from_path(sp(p))
+ video_result = [(video, video.scan())]
+ scan_result.extend(video_result)
+
+ for video, detected_subtitles in scan_result:
+ for s in detected_subtitles:
+ if s.language and s.path not in paths:
+ detected_languages[s.path] = [s.language]
+ except:
+ log.debug('Failed parsing subtitle languages for %s: %s', (paths, traceback.format_exc()))
+
+ # IDX
+ for extra in group['files']['subtitle_extra']:
+ try:
+ if os.path.isfile(extra):
+ output = open(extra, 'r')
+ txt = output.read()
+ output.close()
+
+ idx_langs = re.findall('\nid: (\w+)', txt)
+
+ sub_file = '%s.sub' % os.path.splitext(extra)[0]
+ if len(idx_langs) > 0 and os.path.isfile(sub_file):
+ detected_languages[sub_file] = idx_langs
+ except:
+ log.error('Failed parsing subtitle idx for %s: %s', (extra, traceback.format_exc()))
+
+ return detected_languages
+
+ def determineMedia(self, group, release_download = None):
+
+ # Get imdb id from downloader
+ imdb_id = release_download and release_download.get('imdb_id')
+ if imdb_id:
+ log.debug('Found movie via imdb id from it\'s download id: %s', release_download.get('imdb_id'))
+
+ files = group['files']
+
+ # Check for CP(imdb_id) string in the file paths
+ if not imdb_id:
+ for cur_file in files['movie']:
+ imdb_id = self.getCPImdb(cur_file)
+ if imdb_id:
+ log.debug('Found movie via CP tag: %s', cur_file)
+ break
+
+ # Check and see if nfo contains the imdb-id
+ nfo_file = None
+ if not imdb_id:
+ try:
+ for nf in files['nfo']:
+ imdb_id = getImdb(nf, check_inside = True)
+ if imdb_id:
+ log.debug('Found movie via nfo file: %s', nf)
+ nfo_file = nf
+ break
+ except:
+ pass
+
+ # Check and see if filenames contains the imdb-id
+ if not imdb_id:
+ try:
+ for filetype in files:
+ for filetype_file in files[filetype]:
+ imdb_id = getImdb(filetype_file)
+ if imdb_id:
+ log.debug('Found movie via imdb in filename: %s', nfo_file)
+ break
+ except:
+ pass
+
+ # Search based on identifiers
+ if not imdb_id:
+ for identifier in group['identifiers']:
+
+ if len(identifier) > 2:
+ try: filename = list(group['files'].get('movie'))[0]
+ except: filename = None
+
+ name_year = self.getReleaseNameYear(identifier, file_name = filename if not group['is_dvd'] else None)
+ if name_year.get('name') and name_year.get('year'):
+ search_q = '%(name)s %(year)s' % name_year
+ movie = fireEvent('movie.search', q = search_q, merge = True, limit = 1)
+
+ # Try with other
+ if len(movie) == 0 and name_year.get('other') and name_year['other'].get('name') and name_year['other'].get('year'):
+ search_q2 = '%(name)s %(year)s' % name_year.get('other')
+ if search_q2 != search_q:
+ movie = fireEvent('movie.search', q = search_q2, merge = True, limit = 1)
+
+ if len(movie) > 0:
+ imdb_id = movie[0].get('imdb')
+ log.debug('Found movie via search: %s', identifier)
+ if imdb_id: break
+ else:
+ log.debug('Identifier to short to use for search: %s', identifier)
+
+ if imdb_id:
+ try:
+ db = get_db()
+ return db.get('media', 'imdb-%s' % imdb_id, with_doc = True)['doc']
+ except:
+ log.debug('Movie "%s" not in library, just getting info', imdb_id)
+ return {
+ 'identifier': imdb_id,
+ 'info': fireEvent('movie.info', identifier = imdb_id, merge = True, extended = False)
+ }
+
+ log.error('No imdb_id found for %s. Add a NFO file with IMDB id or add the year to the filename.', group['identifiers'])
+ return {}
+
+ def getCPImdb(self, string):
+
+ try:
+ m = re.search(self.cp_imdb, string.lower())
+ id = m.group('id')
+ if id: return id
+ except AttributeError:
+ pass
+
+ return False
+
+ def removeCPTag(self, name):
+ try:
+ return re.sub(self.cp_imdb, '', name).strip()
+ except:
+ pass
+ return name
+
+ def getSamples(self, files):
+ return set(filter(lambda s: self.isSampleFile(s), files))
+
+ def getMediaFiles(self, files):
+
+ def test(s):
+ return self.filesizeBetween(s, self.file_sizes['movie']) and getExt(s.lower()) in self.extensions['movie'] and not self.isSampleFile(s)
+
+ return set(filter(test, files))
+
+ def getMovieExtras(self, files):
+ return set(filter(lambda s: getExt(s.lower()) in self.extensions['movie_extra'], files))
+
+ def getDVDFiles(self, files):
+ def test(s):
+ return self.isDVDFile(s)
+
+ return set(filter(test, files))
+
+ def getSubtitles(self, files):
+ return set(filter(lambda s: getExt(s.lower()) in self.extensions['subtitle'], files))
+
+ def getSubtitlesExtras(self, files):
+ return set(filter(lambda s: getExt(s.lower()) in self.extensions['subtitle_extra'], files))
+
+ def getNfo(self, files):
+ return set(filter(lambda s: getExt(s.lower()) in self.extensions['nfo'], files))
+
+ def getTrailers(self, files):
+
+ def test(s):
+ return re.search('(^|[\W_])trailer\d*[\W_]', s.lower()) and self.filesizeBetween(s, self.file_sizes['trailer'])
+
+ return set(filter(test, files))
+
+ def getImages(self, files):
+
+ def test(s):
+ return getExt(s.lower()) in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tbn']
+ files = set(filter(test, files))
+
+ images = {
+ 'backdrop': set(filter(lambda s: re.search('(^|[\W_])fanart|backdrop\d*[\W_]', s.lower()) and self.filesizeBetween(s, self.file_sizes['backdrop']), files))
+ }
+
+ # Rest
+ images['rest'] = files - images['backdrop']
+
+ return images
+
+
+ def isDVDFile(self, file_name):
+
+ if list(set(file_name.lower().split(os.path.sep)) & set(['video_ts', 'audio_ts'])):
+ return True
+
+ for needle in ['vts_', 'video_ts', 'audio_ts', 'bdmv', 'certificate']:
+ if needle in file_name.lower():
+ return True
+
+ return False
+
+ def keepFile(self, filename):
+
+ # ignoredpaths
+ for i in self.ignored_in_path:
+ if i in filename.lower():
+ log.debug('Ignored "%s" contains "%s".', (filename, i))
+ return False
+
+ # All is OK
+ return True
+
+ def isSampleFile(self, filename):
+ is_sample = re.search('(^|[\W_])sample\d*[\W_]', filename.lower())
+ if is_sample: log.debug('Is sample file: %s', filename)
+ return is_sample
+
+ def filesizeBetween(self, file, file_size = None):
+ if not file_size: file_size = []
+
+ try:
+ return file_size.get('min', 0) < self.getFileSize(file) < file_size.get('max', 100000)
+ except:
+ log.error('Couldn\'t get filesize of %s.', file)
+
+ return False
+
+ def getFileSize(self, file):
+ try:
+ return os.path.getsize(file) / 1024 / 1024
+ except:
+ return None
+
+ def createStringIdentifier(self, file_path, folder = '', exclude_filename = False):
+
+ identifier = file_path.replace(folder, '').lstrip(os.path.sep) # root folder
+ identifier = os.path.splitext(identifier)[0] # ext
+
+ # Exclude file name path if needed (f.e. for DVD files)
+ if exclude_filename:
+ identifier = identifier[:len(identifier) - len(os.path.split(identifier)[-1])]
+
+ # Make sure the identifier is lower case as all regex is with lower case tags
+ identifier = identifier.lower()
+
+ try:
+ path_split = splitString(identifier, os.path.sep)
+ identifier = path_split[-2] if len(path_split) > 1 and len(path_split[-2]) > len(path_split[-1]) else path_split[-1] # Only get filename
+ except: pass
+
+ # multipart
+ identifier = self.removeMultipart(identifier)
+
+ # remove cptag
+ identifier = self.removeCPTag(identifier)
+
+ # simplify the string
+ identifier = simplifyString(identifier)
+
+ year = self.findYear(file_path)
+
+ # groups, release tags, scenename cleaner
+ identifier = re.sub(self.clean, '::', identifier).strip(':')
+
+ # Year
+ if year and identifier[:4] != year:
+ split_by = ':::' if ':::' in identifier else year
+ identifier = '%s %s' % (identifier.split(split_by)[0].strip(), year)
+ else:
+ identifier = identifier.split('::')[0]
+
+ # Remove duplicates
+ out = []
+ for word in identifier.split():
+ if not word in out:
+ out.append(word)
+
+ identifier = ' '.join(out)
+
+ return simplifyString(identifier)
+
+
+ def removeMultipart(self, name):
+ for regex in self.multipart_regex:
+ try:
+ found = re.sub(regex, '', name)
+ if found != name:
+ name = found
+ except:
+ pass
+ return name
+
+ def getPartNumber(self, name):
+ for regex in self.multipart_regex:
+ try:
+ found = re.search(regex, name)
+ if found:
+ return found.group(1)
+ return 1
+ except:
+ pass
+ return 1
+
+ def getCodec(self, filename, codecs):
+ codecs = map(re.escape, codecs)
+ try:
+ codec = re.search('[^A-Z0-9](?P' + '|'.join(codecs) + ')[^A-Z0-9]', filename, re.I)
+ return (codec and codec.group('codec')) or ''
+ except:
+ return ''
+
+ def getResolution(self, filename):
+ try:
+ for key in self.resolutions:
+ if key in filename.lower() and key != 'default':
+ return self.resolutions[key]
+ except:
+ pass
+
+ return self.resolutions['default']
+
+ def getGroup(self, file):
+ try:
+ match = re.findall('\-([A-Z0-9]+)[\.\/]', file, re.I)
+ return match[-1] or ''
+ except:
+ return ''
+
+ def getSourceMedia(self, file):
+ for media in self.source_media:
+ for alias in self.source_media[media]:
+ if alias in file.lower():
+ return media
+
+ return None
+
+ def findYear(self, text):
+
+ # Search year inside () or [] first
+ matches = re.findall('(\(|\[)(?P19[0-9]{2}|20[0-9]{2})(\]|\))', text)
+ if matches:
+ return matches[-1][1]
+
+ # Search normal
+ matches = re.findall('(?P19[0-9]{2}|20[0-9]{2})', text)
+ if matches:
+ return matches[-1]
+
+ return ''
+
+ def getReleaseNameYear(self, release_name, file_name = None):
+
+ release_name = release_name.strip(' .-_')
+
+ # Use guessit first
+ guess = {}
+ if file_name:
+ try:
+ guessit = guess_movie_info(toUnicode(file_name))
+ if guessit.get('title') and guessit.get('year'):
+ guess = {
+ 'name': guessit.get('title'),
+ 'year': guessit.get('year'),
+ }
+ except:
+ log.debug('Could not detect via guessit "%s": %s', (file_name, traceback.format_exc()))
+
+ # Backup to simple
+ release_name = os.path.basename(release_name.replace('\\', '/'))
+ cleaned = ' '.join(re.split('\W+', simplifyString(release_name)))
+ cleaned = re.sub(self.clean, ' ', cleaned)
+
+ year = None
+ for year_str in [file_name, release_name, cleaned]:
+ if not year_str: continue
+ year = self.findYear(year_str)
+ if year:
+ break
+
+ cp_guess = {}
+
+ if year: # Split name on year
+ try:
+ movie_name = cleaned.rsplit(year, 1).pop(0).strip()
+ if movie_name:
+ cp_guess = {
+ 'name': movie_name,
+ 'year': int(year),
+ }
+ except:
+ pass
+
+ if not cp_guess: # Split name on multiple spaces
+ try:
+ movie_name = cleaned.split(' ').pop(0).strip()
+ cp_guess = {
+ 'name': movie_name,
+ 'year': int(year) if movie_name[:4] != year else 0,
+ }
+ except:
+ pass
+
+ if cp_guess.get('year') == guess.get('year') and len(cp_guess.get('name', '')) > len(guess.get('name', '')):
+ cp_guess['other'] = guess
+ return cp_guess
+ elif guess == {}:
+ cp_guess['other'] = guess
+ return cp_guess
+
+ guess['other'] = cp_guess
+ return guess
diff --git a/couchpotato/core/plugins/scanner/__init__.py b/couchpotato/core/plugins/scanner/__init__.py
deleted file mode 100644
index 3d640465bd..0000000000
--- a/couchpotato/core/plugins/scanner/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from .main import Scanner
-
-def start():
- return Scanner()
-
-config = []
diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py
deleted file mode 100644
index b822bc0f9a..0000000000
--- a/couchpotato/core/plugins/scanner/main.py
+++ /dev/null
@@ -1,829 +0,0 @@
-from couchpotato import get_session
-from couchpotato.core.event import fireEvent, addEvent
-from couchpotato.core.helpers.encoding import toUnicode, simplifyString, ss
-from couchpotato.core.helpers.variable import getExt, getImdb, tryInt
-from couchpotato.core.logger import CPLog
-from couchpotato.core.plugins.base import Plugin
-from couchpotato.core.settings.model import File, Movie
-from enzyme.exceptions import NoParserError, ParseError
-from guessit import guess_movie_info
-from subliminal.videos import Video
-import enzyme
-import os
-import re
-import time
-import traceback
-
-log = CPLog(__name__)
-
-
-class Scanner(Plugin):
-
- minimal_filesize = {
- 'media': 314572800, # 300MB
- 'trailer': 1048576, # 1MB
- }
- ignored_in_path = [os.path.sep + 'extracted' + os.path.sep, 'extracting', '_unpack', '_failed_', '_unknown_', '_exists_', '_failed_remove_', '_failed_rename_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files
- ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate']
- extensions = {
- 'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img', 'mdf', 'ts', 'm4v'],
- 'movie_extra': ['mds'],
- 'dvd': ['vts_*', 'vob'],
- 'nfo': ['nfo', 'txt', 'tag'],
- 'subtitle': ['sub', 'srt', 'ssa', 'ass'],
- 'subtitle_extra': ['idx'],
- 'trailer': ['mov', 'mp4', 'flv']
- }
-
- file_types = {
- 'subtitle': ('subtitle', 'subtitle'),
- 'subtitle_extra': ('subtitle', 'subtitle_extra'),
- 'trailer': ('video', 'trailer'),
- 'nfo': ('nfo', 'nfo'),
- 'movie': ('video', 'movie'),
- 'movie_extra': ('movie', 'movie_extra'),
- 'backdrop': ('image', 'backdrop'),
- 'poster': ('image', 'poster'),
- 'thumbnail': ('image', 'thumbnail'),
- 'leftover': ('leftover', 'leftover'),
- }
-
- codecs = {
- 'audio': ['dts', 'ac3', 'ac3d', 'mp3'],
- 'video': ['x264', 'h264', 'divx', 'xvid']
- }
-
- audio_codec_map = {
- 0x2000: 'ac3',
- 0x2001: 'dts',
- 0x0055: 'mp3',
- 0x0050: 'mp2',
- 0x0001: 'pcm',
- 0x003: 'pcm',
- 0x77a1: 'tta1',
- 0x5756: 'wav',
- 0x6750: 'vorbis',
- 0xF1AC: 'flac',
- 0x00ff: 'aac',
- }
-
- source_media = {
- 'bluray': ['bluray', 'blu-ray', 'brrip', 'br-rip'],
- 'hddvd': ['hddvd', 'hd-dvd'],
- 'dvd': ['dvd'],
- 'hdtv': ['hdtv']
- }
-
- clean = '[ _\,\.\(\)\[\]\-](french|swedisch|danish|dutch|swesub|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|video_ts|audio_ts|480p|480i|576p|576i|720p|720i|1080p|1080i|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|cd[1-9]|\[.*\])([ _\,\.\(\)\[\]\-]|$)'
- multipart_regex = [
- '[ _\.-]+cd[ _\.-]*([0-9a-d]+)', #*cd1
- '[ _\.-]+dvd[ _\.-]*([0-9a-d]+)', #*dvd1
- '[ _\.-]+part[ _\.-]*([0-9a-d]+)', #*part1
- '[ _\.-]+dis[ck][ _\.-]*([0-9a-d]+)', #*disk1
- 'cd[ _\.-]*([0-9a-d]+)$', #cd1.ext
- 'dvd[ _\.-]*([0-9a-d]+)$', #dvd1.ext
- 'part[ _\.-]*([0-9a-d]+)$', #part1.mkv
- 'dis[ck][ _\.-]*([0-9a-d]+)$', #disk1.mkv
- '()[ _\.-]+([0-9]*[abcd]+)(\.....?)$',
- '([a-z])([0-9]+)(\.....?)$',
- '()([ab])(\.....?)$' #*a.mkv
- ]
-
- cp_imdb = '(.cp.(?Ptt[0-9{7}]+).)'
-
- def __init__(self):
-
- addEvent('scanner.create_file_identifier', self.createStringIdentifier)
- addEvent('scanner.remove_cptag', self.removeCPTag)
-
- addEvent('scanner.scan', self.scan)
- addEvent('scanner.name_year', self.getReleaseNameYear)
- addEvent('scanner.partnumber', self.getPartNumber)
-
- def scan(self, folder = None, files = None, simple = False, newer_than = 0, on_found = None):
-
- folder = ss(os.path.normpath(folder))
-
- if not folder or not os.path.isdir(folder):
- log.error('Folder doesn\'t exists: %s', folder)
- return {}
-
- # Get movie "master" files
- movie_files = {}
- leftovers = []
-
- # Scan all files of the folder if no files are set
- if not files:
- check_file_date = True
- try:
- files = []
- for root, dirs, walk_files in os.walk(folder):
- for filename in walk_files:
- files.append(os.path.join(root, filename))
- except:
- log.error('Failed getting files from %s: %s', (folder, traceback.format_exc()))
- else:
- check_file_date = False
- files = [ss(x) for x in files]
-
- db = get_session()
-
- for file_path in files:
-
- if not os.path.exists(file_path):
- continue
-
- # Remove ignored files
- if self.isSampleFile(file_path):
- leftovers.append(file_path)
- continue
- elif not self.keepFile(file_path):
- continue
-
- is_dvd_file = self.isDVDFile(file_path)
- if os.path.getsize(file_path) > self.minimal_filesize['media'] or is_dvd_file: # Minimal 300MB files or is DVD file
-
- # Normal identifier
- identifier = self.createStringIdentifier(file_path, folder, exclude_filename = is_dvd_file)
- identifiers = [identifier]
-
- # Identifier with quality
- quality = fireEvent('quality.guess', [file_path], single = True) if not is_dvd_file else {'identifier':'dvdr'}
- if quality:
- identifier_with_quality = '%s %s' % (identifier, quality.get('identifier', ''))
- identifiers = [identifier_with_quality, identifier]
-
- if not movie_files.get(identifier):
- movie_files[identifier] = {
- 'unsorted_files': [],
- 'identifiers': identifiers,
- 'is_dvd': is_dvd_file,
- }
-
- movie_files[identifier]['unsorted_files'].append(file_path)
- else:
- leftovers.append(file_path)
-
- # Break if CP wants to shut down
- if self.shuttingDown():
- break
-
- # Cleanup
- del files
-
- # Sort reverse, this prevents "Iron man 2" from getting grouped with "Iron man" as the "Iron Man 2"
- # files will be grouped first.
- leftovers = set(sorted(leftovers, reverse = True))
-
-
- # Group files minus extension
- for identifier, group in movie_files.iteritems():
- if identifier not in group['identifiers'] and len(identifier) > 0: group['identifiers'].append(identifier)
-
- log.debug('Grouping files: %s', identifier)
-
- for file_path in group['unsorted_files']:
- wo_ext = file_path[:-(len(getExt(file_path)) + 1)]
- found_files = set([i for i in leftovers if wo_ext in i])
- group['unsorted_files'].extend(found_files)
- leftovers = leftovers - found_files
-
- # Break if CP wants to shut down
- if self.shuttingDown():
- break
-
-
- # Create identifiers for all leftover files
- path_identifiers = {}
- for file_path in leftovers:
- identifier = self.createStringIdentifier(file_path, folder)
-
- if not path_identifiers.get(identifier):
- path_identifiers[identifier] = []
-
- path_identifiers[identifier].append(file_path)
-
-
- # Group the files based on the identifier
- delete_identifiers = []
- for identifier, found_files in path_identifiers.iteritems():
- log.debug('Grouping files on identifier: %s', identifier)
-
- group = movie_files.get(identifier)
- if group:
- group['unsorted_files'].extend(found_files)
- delete_identifiers.append(identifier)
-
- # Remove the found files from the leftover stack
- leftovers = leftovers - set(found_files)
-
- # Break if CP wants to shut down
- if self.shuttingDown():
- break
-
- # Cleaning up used
- for identifier in delete_identifiers:
- if path_identifiers.get(identifier):
- del path_identifiers[identifier]
- del delete_identifiers
-
- # Group based on folder
- delete_identifiers = []
- for identifier, found_files in path_identifiers.iteritems():
- log.debug('Grouping files on foldername: %s', identifier)
-
- for ff in found_files:
- new_identifier = self.createStringIdentifier(os.path.dirname(ff), folder)
-
- group = movie_files.get(new_identifier)
- if group:
- group['unsorted_files'].extend([ff])
- delete_identifiers.append(identifier)
-
- # Remove the found files from the leftover stack
- leftovers = leftovers - set([ff])
-
- # Break if CP wants to shut down
- if self.shuttingDown():
- break
-
- # Cleaning up used
- for identifier in delete_identifiers:
- if path_identifiers.get(identifier):
- del path_identifiers[identifier]
- del delete_identifiers
-
- # Make sure we remove older / still extracting files
- valid_files = {}
- while True and not self.shuttingDown():
- try:
- identifier, group = movie_files.popitem()
- except:
- break
-
- # Check if movie is fresh and maybe still unpacking, ignore files new then 1 minute
- file_too_new = False
- for cur_file in group['unsorted_files']:
- if not os.path.isfile(cur_file):
- file_too_new = time.time()
- break
- file_time = [os.path.getmtime(cur_file), os.path.getctime(cur_file)]
- for t in file_time:
- if t > time.time() - 60:
- file_too_new = tryInt(time.time() - t)
- break
-
- if file_too_new:
- break
-
- if check_file_date and file_too_new:
- try:
- time_string = time.ctime(file_time[0])
- except:
- try:
- time_string = time.ctime(file_time[1])
- except:
- time_string = 'unknown'
-
- log.info('Files seem to be still unpacking or just unpacked (created on %s), ignoring for now: %s', (time_string, identifier))
-
- # Delete the unsorted list
- del group['unsorted_files']
-
- continue
-
- # Only process movies newer than x
- if newer_than and newer_than > 0:
- has_new_files = False
- for cur_file in group['unsorted_files']:
- file_time = [os.path.getmtime(cur_file), os.path.getctime(cur_file)]
- if file_time[0] > newer_than or file_time[1] > newer_than:
- has_new_files = True
- break
-
- if not has_new_files:
- log.debug('None of the files have changed since %s for %s, skipping.', (time.ctime(newer_than), identifier))
-
- # Delete the unsorted list
- del group['unsorted_files']
-
- continue
-
- valid_files[identifier] = group
-
- del movie_files
-
- # Determine file types
- processed_movies = {}
- total_found = len(valid_files)
- while True and not self.shuttingDown():
- try:
- identifier, group = valid_files.popitem()
- except:
- break
-
- # Group extra (and easy) files first
- # images = self.getImages(group['unsorted_files'])
- group['files'] = {
- 'movie_extra': self.getMovieExtras(group['unsorted_files']),
- 'subtitle': self.getSubtitles(group['unsorted_files']),
- 'subtitle_extra': self.getSubtitlesExtras(group['unsorted_files']),
- 'nfo': self.getNfo(group['unsorted_files']),
- 'trailer': self.getTrailers(group['unsorted_files']),
- #'backdrop': images['backdrop'],
- 'leftover': set(group['unsorted_files']),
- }
-
- # Media files
- if group['is_dvd']:
- group['files']['movie'] = self.getDVDFiles(group['unsorted_files'])
- else:
- group['files']['movie'] = self.getMediaFiles(group['unsorted_files'])
-
- if len(group['files']['movie']) == 0:
- log.error('Couldn\'t find any movie files for %s', identifier)
- continue
-
- log.debug('Getting metadata for %s', identifier)
- group['meta_data'] = self.getMetaData(group, folder = folder)
-
- # Subtitle meta
- group['subtitle_language'] = self.getSubtitleLanguage(group) if not simple else {}
-
- # Get parent dir from movie files
- for movie_file in group['files']['movie']:
- group['parentdir'] = os.path.dirname(movie_file)
- group['dirname'] = None
-
- folder_names = group['parentdir'].replace(folder, '').split(os.path.sep)
- folder_names.reverse()
-
- # Try and get a proper dirname, so no "A", "Movie", "Download" etc
- for folder_name in folder_names:
- if folder_name.lower() not in self.ignore_names and len(folder_name) > 2:
- group['dirname'] = folder_name
- break
-
- break
-
- # Leftover "sorted" files
- for file_type in group['files']:
- if not file_type is 'leftover':
- group['files']['leftover'] -= set(group['files'][file_type])
-
- # Delete the unsorted list
- del group['unsorted_files']
-
- # Determine movie
- group['library'] = self.determineMovie(group)
- if not group['library']:
- log.error('Unable to determine movie: %s', group['identifiers'])
- else:
- movie = db.query(Movie).filter_by(library_id = group['library']['id']).first()
- group['movie_id'] = None if not movie else movie.id
-
- processed_movies[identifier] = group
-
- # Notify parent & progress on something found
- if on_found:
- on_found(group, total_found, total_found - len(processed_movies))
-
- if len(processed_movies) > 0:
- log.info('Found %s movies in the folder %s', (len(processed_movies), folder))
- else:
- log.debug('Found no movies in the folder %s', (folder))
-
- return processed_movies
-
- def getMetaData(self, group, folder = ''):
-
- data = {}
- files = list(group['files']['movie'])
-
- for cur_file in files:
- if os.path.getsize(cur_file) < self.minimal_filesize['media']: continue # Ignore smaller files
-
- meta = self.getMeta(cur_file)
-
- try:
- data['video'] = meta.get('video', self.getCodec(cur_file, self.codecs['video']))
- data['audio'] = meta.get('audio', self.getCodec(cur_file, self.codecs['audio']))
- data['resolution_width'] = meta.get('resolution_width', 720)
- data['resolution_height'] = meta.get('resolution_height', 480)
- data['aspect'] = meta.get('resolution_width', 720) / meta.get('resolution_height', 480)
- except:
- log.debug('Error parsing metadata: %s %s', (cur_file, traceback.format_exc()))
- pass
-
- if data.get('audio'): break
-
- data['quality'] = fireEvent('quality.guess', files = files, extra = data, single = True)
- if not data['quality']:
- data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True)
-
- data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 or data['quality'].get('hd') else 'SD'
-
- filename = re.sub('(.cp\(tt[0-9{7}]+\))', '', files[0])
- data['group'] = self.getGroup(filename[len(folder):])
- data['source'] = self.getSourceMedia(filename)
-
- return data
-
- def getMeta(self, filename):
-
- try:
- p = enzyme.parse(filename)
-
- # Video codec
- vc = ('h264' if p.video[0].codec == 'AVC1' else p.video[0].codec).lower()
-
- # Audio codec
- ac = p.audio[0].codec
- try: ac = self.audio_codec_map.get(p.audio[0].codec)
- except: pass
-
- return {
- 'video': vc,
- 'audio': ac,
- 'resolution_width': tryInt(p.video[0].width),
- 'resolution_height': tryInt(p.video[0].height),
- }
- except ParseError:
- log.debug('Failed to parse meta for %s', filename)
- except NoParserError:
- log.debug('No parser found for %s', filename)
- except:
- log.debug('Failed parsing %s', filename)
-
- return {}
-
- def getSubtitleLanguage(self, group):
- detected_languages = {}
-
- # Subliminal scanner
- try:
- paths = group['files']['movie']
- scan_result = []
- for p in paths:
- if not group['is_dvd']:
- video = Video.from_path(toUnicode(p))
- video_result = [(video, video.scan())]
- scan_result.extend(video_result)
-
- for video, detected_subtitles in scan_result:
- for s in detected_subtitles:
- if s.language and s.path not in paths:
- detected_languages[s.path] = [s.language]
- except:
- log.debug('Failed parsing subtitle languages for %s: %s', (paths, traceback.format_exc()))
-
- # IDX
- for extra in group['files']['subtitle_extra']:
- try:
- if os.path.isfile(extra):
- output = open(extra, 'r')
- txt = output.read()
- output.close()
-
- idx_langs = re.findall('\nid: (\w+)', txt)
-
- sub_file = '%s.sub' % os.path.splitext(extra)[0]
- if len(idx_langs) > 0 and os.path.isfile(sub_file):
- detected_languages[sub_file] = idx_langs
- except:
- log.error('Failed parsing subtitle idx for %s: %s', (extra, traceback.format_exc()))
-
- return detected_languages
-
- def determineMovie(self, group):
- imdb_id = None
-
- files = group['files']
-
- # Check for CP(imdb_id) string in the file paths
- for cur_file in files['movie']:
- imdb_id = self.getCPImdb(cur_file)
- if imdb_id:
- log.debug('Found movie via CP tag: %s', cur_file)
- break
-
- # Check and see if nfo contains the imdb-id
- if not imdb_id:
- try:
- for nfo_file in files['nfo']:
- imdb_id = getImdb(nfo_file)
- if imdb_id:
- log.debug('Found movie via nfo file: %s', nfo_file)
- break
- except:
- pass
-
- # Check and see if filenames contains the imdb-id
- if not imdb_id:
- try:
- for filetype in files:
- for filetype_file in files[filetype]:
- imdb_id = getImdb(filetype_file, check_inside = False)
- if imdb_id:
- log.debug('Found movie via imdb in filename: %s', nfo_file)
- break
- except:
- pass
-
- # Check if path is already in db
- if not imdb_id:
- db = get_session()
- for cur_file in files['movie']:
- f = db.query(File).filter_by(path = toUnicode(cur_file)).first()
- try:
- imdb_id = f.library[0].identifier
- log.debug('Found movie via database: %s', cur_file)
- break
- except:
- pass
-
- # Search based on OpenSubtitleHash
- if not imdb_id and not group['is_dvd']:
- for cur_file in files['movie']:
- movie = fireEvent('movie.by_hash', file = cur_file, merge = True)
-
- if len(movie) > 0:
- imdb_id = movie[0]['imdb']
- if imdb_id:
- log.debug('Found movie via OpenSubtitleHash: %s', cur_file)
- break
-
- # Search based on identifiers
- if not imdb_id:
- for identifier in group['identifiers']:
-
- if len(identifier) > 2:
- try: filename = list(group['files'].get('movie'))[0]
- except: filename = None
-
- name_year = self.getReleaseNameYear(identifier, file_name = filename if not group['is_dvd'] else None)
- if name_year.get('name') and name_year.get('year'):
- movie = fireEvent('movie.search', q = '%(name)s %(year)s' % name_year, merge = True, limit = 1)
-
- if len(movie) > 0:
- imdb_id = movie[0]['imdb']
- log.debug('Found movie via search: %s', cur_file)
- if imdb_id: break
- else:
- log.debug('Identifier to short to use for search: %s', identifier)
-
- if imdb_id:
- return fireEvent('library.add', attrs = {
- 'identifier': imdb_id
- }, update_after = False, single = True)
-
- log.error('No imdb_id found for %s. Add a NFO file with IMDB id or add the year to the filename.', group['identifiers'])
- return {}
-
- def getCPImdb(self, string):
-
- try:
- m = re.search(self.cp_imdb, string.lower())
- id = m.group('id')
- if id: return id
- except AttributeError:
- pass
-
- return False
-
- def removeCPTag(self, name):
- try:
- return re.sub(self.cp_imdb, '', name)
- except:
- pass
- return name
-
- def getSamples(self, files):
- return set(filter(lambda s: self.isSampleFile(s), files))
-
- def getMediaFiles(self, files):
-
- def test(s):
- return self.filesizeBetween(s, 300, 100000) and getExt(s.lower()) in self.extensions['movie'] and not self.isSampleFile(s)
-
- return set(filter(test, files))
-
- def getMovieExtras(self, files):
- return set(filter(lambda s: getExt(s.lower()) in self.extensions['movie_extra'], files))
-
- def getDVDFiles(self, files):
- def test(s):
- return self.isDVDFile(s)
-
- return set(filter(test, files))
-
- def getSubtitles(self, files):
- return set(filter(lambda s: getExt(s.lower()) in self.extensions['subtitle'], files))
-
- def getSubtitlesExtras(self, files):
- return set(filter(lambda s: getExt(s.lower()) in self.extensions['subtitle_extra'], files))
-
- def getNfo(self, files):
- return set(filter(lambda s: getExt(s.lower()) in self.extensions['nfo'], files))
-
- def getTrailers(self, files):
-
- def test(s):
- return re.search('(^|[\W_])trailer\d*[\W_]', s.lower()) and self.filesizeBetween(s, 2, 250)
-
- return set(filter(test, files))
-
- def getImages(self, files):
-
- def test(s):
- return getExt(s.lower()) in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tbn']
- files = set(filter(test, files))
-
- images = {}
-
- # Fanart
- images['backdrop'] = set(filter(lambda s: re.search('(^|[\W_])fanart|backdrop\d*[\W_]', s.lower()) and self.filesizeBetween(s, 0, 5), files))
-
- # Rest
- images['rest'] = files - images['backdrop']
-
- return images
-
-
- def isDVDFile(self, file_name):
-
- if list(set(file_name.lower().split(os.path.sep)) & set(['video_ts', 'audio_ts'])):
- return True
-
- for needle in ['vts_', 'video_ts', 'audio_ts', 'bdmv', 'certificate']:
- if needle in file_name.lower():
- return True
-
- return False
-
- def keepFile(self, filename):
-
- # ignoredpaths
- for i in self.ignored_in_path:
- if i in filename.lower():
- log.debug('Ignored "%s" contains "%s".', (filename, i))
- return False
-
- # Sample file
- if self.isSampleFile(filename):
- log.debug('Is sample file "%s".', filename)
- return False
-
- # Minimal size
- if self.filesizeBetween(filename, self.minimal_filesize['media']):
- log.debug('File to small: %s', filename)
- return False
-
- # All is OK
- return True
-
- def isSampleFile(self, filename):
- is_sample = re.search('(^|[\W_])sample\d*[\W_]', filename.lower())
- if is_sample: log.debug('Is sample file: %s', filename)
- return is_sample
-
- def filesizeBetween(self, file, min = 0, max = 100000):
- try:
- return (min * 1048576) < os.path.getsize(file) < (max * 1048576)
- except:
- log.error('Couldn\'t get filesize of %s.', file)
-
- return False
-
- def createStringIdentifier(self, file_path, folder = '', exclude_filename = False):
-
- identifier = file_path.replace(folder, '') # root folder
- identifier = os.path.splitext(identifier)[0] # ext
-
- if exclude_filename:
- identifier = identifier[:len(identifier) - len(os.path.split(identifier)[-1])]
-
- # multipart
- identifier = self.removeMultipart(identifier)
-
- # remove cptag
- identifier = self.removeCPTag(identifier)
-
- # groups, release tags, scenename cleaner, regex isn't correct
- identifier = re.sub(self.clean, '::', simplifyString(identifier)).strip(':')
-
- # Year
- year = self.findYear(identifier)
- if year:
- identifier = '%s %s' % (identifier.split(year)[0].strip(), year)
- else:
- identifier = identifier.split('::')[0]
-
- # Remove duplicates
- out = []
- for word in identifier.split():
- if not word in out:
- out.append(word)
-
- identifier = ' '.join(out)
-
- return simplifyString(identifier)
-
-
- def removeMultipart(self, name):
- for regex in self.multipart_regex:
- try:
- found = re.sub(regex, '', name)
- if found != name:
- name = found
- except:
- pass
- return name
-
- def getPartNumber(self, name):
- for regex in self.multipart_regex:
- try:
- found = re.search(regex, name)
- if found:
- return found.group(1)
- return 1
- except:
- pass
- return 1
-
- def getCodec(self, filename, codecs):
- codecs = map(re.escape, codecs)
- try:
- codec = re.search('[^A-Z0-9](?P' + '|'.join(codecs) + ')[^A-Z0-9]', filename, re.I)
- return (codec and codec.group('codec')) or ''
- except:
- return ''
-
- def getGroup(self, file):
- try:
- match = re.findall('\-([A-Z0-9]+)[\.\/]', file, re.I)
- return match[-1] or ''
- except:
- return ''
-
- def getSourceMedia(self, file):
- for media in self.source_media:
- for alias in self.source_media[media]:
- if alias in file.lower():
- return media
-
- return None
-
- def findYear(self, text):
- matches = re.search('(?P19[0-9]{2}|20[0-9]{2})', text)
- if matches:
- return matches.group('year')
-
- return ''
-
- def getReleaseNameYear(self, release_name, file_name = None):
-
- # Use guessit first
- guess = {}
- if file_name:
- try:
- guess = guess_movie_info(toUnicode(file_name))
- if guess.get('title') and guess.get('year'):
- guess = {
- 'name': guess.get('title'),
- 'year': guess.get('year'),
- }
- except:
- log.debug('Could not detect via guessit "%s": %s', (file_name, traceback.format_exc()))
-
- # Backup to simple
- cleaned = ' '.join(re.split('\W+', simplifyString(release_name)))
- cleaned = re.sub(self.clean, ' ', cleaned)
- year = self.findYear(cleaned)
- cp_guess = {}
-
- if year: # Split name on year
- try:
- movie_name = cleaned.split(year).pop(0).strip()
- cp_guess = {
- 'name': movie_name,
- 'year': int(year),
- }
- except:
- pass
- else: # Split name on multiple spaces
- try:
- movie_name = cleaned.split(' ').pop(0).strip()
- cp_guess = {
- 'name': movie_name,
- 'year': int(year),
- }
- except:
- pass
-
- if cp_guess.get('year') == guess.get('year') and len(cp_guess.get('name', '')) > len(guess.get('name', '')):
- return cp_guess
- elif guess == {}:
- return cp_guess
-
- return guess
diff --git a/couchpotato/core/plugins/score/__init__.py b/couchpotato/core/plugins/score/__init__.py
index 2c367f896e..65cadd9919 100644
--- a/couchpotato/core/plugins/score/__init__.py
+++ b/couchpotato/core/plugins/score/__init__.py
@@ -1,6 +1,5 @@
from .main import Score
-def start():
- return Score()
-config = []
+def autoload():
+ return Score()
diff --git a/couchpotato/core/plugins/score/main.py b/couchpotato/core/plugins/score/main.py
index f853be95ad..e6fef25324 100644
--- a/couchpotato/core/plugins/score/main.py
+++ b/couchpotato/core/plugins/score/main.py
@@ -1,11 +1,12 @@
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode
-from couchpotato.core.helpers.variable import getTitle
+from couchpotato.core.helpers.variable import getTitle, splitString, removeDuplicate
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.plugins.score.scores import nameScore, nameRatioScore, \
sizeScore, providerScore, duplicateScore, partialIgnoredScore, namePositionScore, \
- halfMultipartScore
+ halfMultipartScore, sceneScore
+from couchpotato.environment import Env
log = CPLog(__name__)
@@ -16,21 +17,26 @@ def __init__(self):
addEvent('score.calculate', self.calculate)
def calculate(self, nzb, movie):
- ''' Calculate the score of a NZB, used for sorting later '''
+ """ Calculate the score of a NZB, used for sorting later """
- score = nameScore(toUnicode(nzb['name']), movie['library']['year'])
+ # Merge global and category
+ preferred_words = splitString(Env.setting('preferred_words', section = 'searcher').lower())
+ try: preferred_words = removeDuplicate(preferred_words + splitString(movie['category']['preferred'].lower()))
+ except: pass
- for movie_title in movie['library']['titles']:
- score += nameRatioScore(toUnicode(nzb['name']), toUnicode(movie_title['title']))
- score += namePositionScore(toUnicode(nzb['name']), toUnicode(movie_title['title']))
+ score = nameScore(toUnicode(nzb['name']), movie['info']['year'], preferred_words)
+
+ for movie_title in movie['info']['titles']:
+ score += nameRatioScore(toUnicode(nzb['name']), toUnicode(movie_title))
+ score += namePositionScore(toUnicode(nzb['name']), toUnicode(movie_title))
score += sizeScore(nzb['size'])
# Torrents only
if nzb.get('seeders'):
try:
- score += nzb.get('seeders') / 5
- score += nzb.get('leechers') / 10
+ score += nzb.get('seeders') * 100 / 15
+ score += nzb.get('leechers') * 100 / 30
except:
pass
@@ -38,10 +44,15 @@ def calculate(self, nzb, movie):
score += providerScore(nzb['provider'])
# Duplicates in name
- score += duplicateScore(nzb['name'], getTitle(movie['library']))
+ score += duplicateScore(nzb['name'], getTitle(movie))
+
+ # Merge global and category
+ ignored_words = splitString(Env.setting('ignored_words', section = 'searcher').lower())
+ try: ignored_words = removeDuplicate(ignored_words + splitString(movie['category']['ignored'].lower()))
+ except: pass
# Partial ignored words
- score += partialIgnoredScore(nzb['name'], getTitle(movie['library']))
+ score += partialIgnoredScore(nzb['name'], getTitle(movie), ignored_words)
# Ignore single downloads from multipart
score += halfMultipartScore(nzb['name'])
@@ -51,4 +62,7 @@ def calculate(self, nzb, movie):
if extra_score:
score += extra_score(nzb)
+ # Scene / Nuke scoring
+ score += sceneScore(nzb['name'])
+
return score
diff --git a/couchpotato/core/plugins/score/scores.py b/couchpotato/core/plugins/score/scores.py
index 3d5a9e69b6..f53f69a31c 100644
--- a/couchpotato/core/plugins/score/scores.py
+++ b/couchpotato/core/plugins/score/scores.py
@@ -1,8 +1,15 @@
+import re
+import traceback
+
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import simplifyString
from couchpotato.core.helpers.variable import tryInt
+from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
-import re
+
+
+log = CPLog(__name__)
+
name_scores = [
# Tags
@@ -23,39 +30,46 @@
]
-def nameScore(name, year):
- ''' Calculate score for words in the NZB name '''
+def nameScore(name, year, preferred_words):
+ """ Calculate score for words in the NZB name """
- score = 0
- name = name.lower()
-
- # give points for the cool stuff
- for value in name_scores:
- v = value.split(':')
- add = int(v.pop())
- if v.pop() in name:
- score = score + add
-
- # points if the year is correct
- if str(year) in name:
- score = score + 5
-
- # Contains preferred word
- nzb_words = re.split('\W+', simplifyString(name))
- preferred_words = [x.strip() for x in Env.setting('preferred_words', section = 'searcher').split(',')]
- for word in preferred_words:
- if word.strip() and word.strip().lower() in nzb_words:
- score = score + 100
+ try:
+ score = 0
+ name = name.lower()
- return score
+ # give points for the cool stuff
+ for value in name_scores:
+ v = value.split(':')
+ add = int(v.pop())
+ if v.pop() in name:
+ score += add
+
+ # points if the year is correct
+ if str(year) in name:
+ score += 5
+
+ # Contains preferred word
+ nzb_words = re.split('\W+', simplifyString(name))
+ score += 100 * len(list(set(nzb_words) & set(preferred_words)))
+
+ return score
+ except:
+ log.error('Failed doing nameScore: %s', traceback.format_exc())
+
+ return 0
def nameRatioScore(nzb_name, movie_name):
- nzb_words = re.split('\W+', fireEvent('scanner.create_file_identifier', nzb_name, single = True))
- movie_words = re.split('\W+', simplifyString(movie_name))
+ try:
+ nzb_words = re.split('\W+', fireEvent('scanner.create_file_identifier', nzb_name, single = True))
+ movie_words = re.split('\W+', simplifyString(movie_name))
+
+ left_over = set(nzb_words) - set(movie_words)
+ return 10 - len(left_over)
+ except:
+ log.error('Failed doing nameRatioScore: %s', traceback.format_exc())
- left_over = set(nzb_words) - set(movie_words)
- return 10 - len(left_over)
+ return 0
def namePositionScore(nzb_name, movie_name):
@@ -72,9 +86,12 @@ def namePositionScore(nzb_name, movie_name):
name_year = fireEvent('scanner.name_year', nzb_name, single = True)
# Give points for movies beginning with the correct name
- name_split = simplifyString(nzb_name).split(simplifyString(movie_name))
- if name_split[0].strip() == '':
- score += 10
+ split_by = simplifyString(movie_name)
+ name_split = []
+ if len(split_by) > 0:
+ name_split = simplifyString(nzb_name).split(split_by)
+ if name_split[0].strip() == '':
+ score += 10
# If year is second in line, give more points
if len(name_split) > 1 and name_year:
@@ -116,49 +133,98 @@ def sizeScore(size):
def providerScore(provider):
- if provider in ['OMGWTFNZBs', 'PassThePopcorn', 'SceneAccess', 'TorrentLeech']:
- return 20
- if provider in ['Newznab']:
- return 10
+ try:
+ score = tryInt(Env.setting('extra_score', section = provider.lower(), default = 0))
+ except:
+ score = 0
- return 0
+ return score
def duplicateScore(nzb_name, movie_name):
- nzb_words = re.split('\W+', simplifyString(nzb_name))
- movie_words = re.split('\W+', simplifyString(movie_name))
+ try:
+ nzb_words = re.split('\W+', simplifyString(nzb_name))
+ movie_words = re.split('\W+', simplifyString(movie_name))
- # minus for duplicates
- duplicates = [x for i, x in enumerate(nzb_words) if nzb_words[i:].count(x) > 1]
+ # minus for duplicates
+ duplicates = [x for i, x in enumerate(nzb_words) if nzb_words[i:].count(x) > 1]
- return len(list(set(duplicates) - set(movie_words))) * -4
+ return len(list(set(duplicates) - set(movie_words))) * -4
+ except:
+ log.error('Failed doing duplicateScore: %s', traceback.format_exc())
+ return 0
-def partialIgnoredScore(nzb_name, movie_name):
- nzb_name = nzb_name.lower()
- movie_name = movie_name.lower()
+def partialIgnoredScore(nzb_name, movie_name, ignored_words):
- ignored_words = [x.strip().lower() for x in Env.setting('ignored_words', section = 'searcher').split(',')]
+ try:
+ nzb_name = nzb_name.lower()
+ movie_name = movie_name.lower()
- score = 0
- for ignored_word in ignored_words:
- if ignored_word in nzb_name and ignored_word not in movie_name:
- score -= 5
+ score = 0
+ for ignored_word in ignored_words:
+ if ignored_word in nzb_name and ignored_word not in movie_name:
+ score -= 5
+
+ return score
+ except:
+ log.error('Failed doing partialIgnoredScore: %s', traceback.format_exc())
+
+ return 0
- return score
def halfMultipartScore(nzb_name):
- wrong_found = 0
- for nr in [1, 2, 3, 4, 5, 'i', 'ii', 'iii', 'iv', 'v', 'a', 'b', 'c', 'd', 'e']:
- for wrong in ['cd', 'part', 'dis', 'disc', 'dvd']:
- if '%s%s' % (wrong, nr) in nzb_name.lower():
- wrong_found += 1
+ try:
+ wrong_found = 0
+ for nr in [1, 2, 3, 4, 5, 'i', 'ii', 'iii', 'iv', 'v', 'a', 'b', 'c', 'd', 'e']:
+ for wrong in ['cd', 'part', 'dis', 'disc', 'dvd']:
+ if '%s%s' % (wrong, nr) in nzb_name.lower():
+ wrong_found += 1
+
+ if wrong_found == 1:
+ return -30
+
+ return 0
+ except:
+ log.error('Failed doing halfMultipartScore: %s', traceback.format_exc())
+
+ return 0
+
+
+def sceneScore(nzb_name):
+
+ check_names = [nzb_name]
+
+ # Match names between "
+ try: check_names.append(re.search(r'([\'"])[^\1]*\1', nzb_name).group(0))
+ except: pass
+
+ # Match longest name between []
+ try: check_names.append(max(re.findall(r'[^[]*\[([^]]*)\]', nzb_name), key = len).strip())
+ except: pass
+
+ for name in check_names:
+
+ # Strip twice, remove possible file extensions
+ name = name.lower().strip(' "\'\.-_\[\]')
+ name = re.sub('\.([a-z0-9]{0,4})$', '', name)
+ name = name.strip(' "\'\.-_\[\]')
+
+ # Make sure year and groupname is in there
+ year = re.findall('(?P19[0-9]{2}|20[0-9]{2})', name)
+ group = re.findall('\-([a-z0-9]+)$', name)
- if wrong_found == 1:
- return -30
+ if len(year) > 0 and len(group) > 0:
+ try:
+ validate = fireEvent('release.validate', name, single = True)
+ if validate and tryInt(validate.get('score')) != 0:
+ log.debug('Release "%s" scored %s, reason: %s', (nzb_name, validate['score'], validate['reasons']))
+ return tryInt(validate.get('score'))
+ except:
+ log.error('Failed scoring scene: %s', traceback.format_exc())
return 0
diff --git a/couchpotato/core/plugins/searcher/__init__.py b/couchpotato/core/plugins/searcher/__init__.py
deleted file mode 100644
index a6dd6913ab..0000000000
--- a/couchpotato/core/plugins/searcher/__init__.py
+++ /dev/null
@@ -1,96 +0,0 @@
-from .main import Searcher
-import random
-
-def start():
- return Searcher()
-
-config = [{
- 'name': 'searcher',
- 'order': 20,
- 'groups': [
- {
- 'tab': 'searcher',
- 'name': 'searcher',
- 'label': 'Search',
- 'description': 'Options for the searchers',
- 'options': [
- {
- 'name': 'preferred_words',
- 'label': 'Preferred words',
- 'default': '',
- 'description': 'These words will give the releases a higher score.'
- },
- {
- 'name': 'required_words',
- 'label': 'Required words',
- 'default': '',
- 'placeholder': 'Example: DTS, AC3 & English',
- 'description': 'Ignore releases that don\'t contain at least one set of words. Sets are separated by "," and each word within a set must be separated with "&"'
- },
- {
- 'name': 'ignored_words',
- 'label': 'Ignored words',
- 'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs',
- },
- {
- 'name': 'preferred_method',
- 'label': 'First search',
- 'description': 'Which of the methods do you prefer',
- 'default': 'both',
- 'type': 'dropdown',
- 'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrents', 'torrent')],
- },
- ],
- }, {
- 'tab': 'searcher',
- 'name': 'cronjob',
- 'label': 'Cronjob',
- 'advanced': True,
- 'description': 'Cron settings for the searcher see: APScheduler for details.',
- 'options': [
- {
- 'name': 'cron_day',
- 'label': 'Day',
- 'advanced': True,
- 'default': '*',
- 'type': 'string',
- 'description': '*: Every day, */2: Every 2 days, 1: Every first of the month.',
- },
- {
- 'name': 'cron_hour',
- 'label': 'Hour',
- 'advanced': True,
- 'default': random.randint(0, 23),
- 'type': 'string',
- 'description': '*: Every hour, */8: Every 8 hours, 3: At 3, midnight.',
- },
- {
- 'name': 'cron_minute',
- 'label': 'Minute',
- 'advanced': True,
- 'default': random.randint(0, 59),
- 'type': 'string',
- 'description': "Just keep it random, so the providers don't get DDOSed by every CP user on a 'full' hour."
- },
- ],
- },
- ],
-}, {
- 'name': 'nzb',
- 'groups': [
- {
- 'tab': 'searcher',
- 'name': 'nzb',
- 'label': 'NZB',
- 'wizard': True,
- 'options': [
- {
- 'name': 'retention',
- 'default': 1000,
- 'type': 'int',
- 'unit': 'days'
- },
- ],
- },
- ],
-}]
diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py
deleted file mode 100644
index c5ed13800d..0000000000
--- a/couchpotato/core/plugins/searcher/main.py
+++ /dev/null
@@ -1,605 +0,0 @@
-from couchpotato import get_session
-from couchpotato.api import addApiView
-from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
-from couchpotato.core.helpers.encoding import simplifyString, toUnicode
-from couchpotato.core.helpers.request import jsonified, getParam
-from couchpotato.core.helpers.variable import md5, getTitle, splitString, \
- possibleTitles
-from couchpotato.core.logger import CPLog
-from couchpotato.core.plugins.base import Plugin
-from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
-from couchpotato.environment import Env
-from inspect import ismethod, isfunction
-from sqlalchemy.exc import InterfaceError
-import datetime
-import random
-import re
-import time
-import traceback
-
-log = CPLog(__name__)
-
-
-class Searcher(Plugin):
-
- in_progress = False
-
- def __init__(self):
- addEvent('searcher.all', self.allMovies)
- addEvent('searcher.single', self.single)
- addEvent('searcher.correct_movie', self.correctMovie)
- addEvent('searcher.download', self.download)
- addEvent('searcher.try_next_release', self.tryNextRelease)
- addEvent('searcher.could_be_released', self.couldBeReleased)
-
- addApiView('searcher.try_next', self.tryNextReleaseView, docs = {
- 'desc': 'Marks the snatched results as ignored and try the next best release',
- 'params': {
- 'id': {'desc': 'The id of the movie'},
- },
- })
-
- addApiView('searcher.full_search', self.allMoviesView, docs = {
- 'desc': 'Starts a full search for all wanted movies',
- })
-
- addApiView('searcher.progress', self.getProgress, docs = {
- 'desc': 'Get the progress of current full search',
- 'return': {'type': 'object', 'example': """{
- 'progress': False || object, total & to_go,
-}"""},
- })
-
- # Schedule cronjob
- fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
-
- def allMoviesView(self):
-
- in_progress = self.in_progress
- if not in_progress:
- fireEventAsync('searcher.all')
- fireEvent('notify.frontend', type = 'searcher.started', data = True, message = 'Full search started')
- else:
- fireEvent('notify.frontend', type = 'searcher.already_started', data = True, message = 'Full search already in progress')
-
- return jsonified({
- 'success': not in_progress
- })
-
- def getProgress(self):
-
- return jsonified({
- 'progress': self.in_progress
- })
-
- def allMovies(self):
-
- if self.in_progress:
- log.info('Search already in progress')
- return
-
- self.in_progress = True
-
- db = get_session()
-
- movies = db.query(Movie).filter(
- Movie.status.has(identifier = 'active')
- ).all()
- random.shuffle(movies)
-
- self.in_progress = {
- 'total': len(movies),
- 'to_go': len(movies),
- }
-
- try:
- search_types = self.getSearchTypes()
-
- for movie in movies:
- movie_dict = movie.to_dict({
- 'profile': {'types': {'quality': {}}},
- 'releases': {'status': {}, 'quality': {}},
- 'library': {'titles': {}, 'files':{}},
- 'files': {}
- })
-
- try:
- self.single(movie_dict, search_types)
- except IndexError:
- log.error('Forcing library update for %s, if you see this often, please report: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
- fireEvent('library.update', movie_dict['library']['identifier'], force = True)
- except:
- log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
-
- self.in_progress['to_go'] -= 1
-
- # Break if CP wants to shut down
- if self.shuttingDown():
- break
-
- except SearchSetupError:
- pass
-
- self.in_progress = False
-
- def single(self, movie, search_types = None):
-
- # Find out search type
- try:
- if not search_types:
- search_types = self.getSearchTypes()
- except SearchSetupError:
- return
-
- done_status = fireEvent('status.get', 'done', single = True)
-
- if not movie['profile'] or movie['status_id'] == done_status.get('id'):
- log.debug('Movie doesn\'t have a profile or already done, assuming in manage tab.')
- return
-
- db = get_session()
-
- pre_releases = fireEvent('quality.pre_releases', single = True)
- release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True)
- available_status = fireEvent('status.get', 'available', single = True)
- ignored_status = fireEvent('status.get', 'ignored', single = True)
-
- found_releases = []
-
- default_title = getTitle(movie['library'])
- if not default_title:
- log.error('No proper info found for movie, removing it from library to cause it from having more issues.')
- fireEvent('movie.delete', movie['id'], single = True)
- return
-
- fireEvent('notify.frontend', type = 'searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title)
-
-
- ret = False
- for quality_type in movie['profile']['types']:
- if not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates):
- log.info('Too early to search for %s, %s', (quality_type['quality']['identifier'], default_title))
- continue
-
- has_better_quality = 0
-
- # See if better quality is available
- for release in movie['releases']:
- if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id')]:
- has_better_quality += 1
-
- # Don't search for quality lower then already available.
- if has_better_quality is 0:
-
- log.info('Search for %s in %s', (default_title, quality_type['quality']['label']))
- quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True)
-
- results = []
- for search_type in search_types:
- type_results = fireEvent('%s.search' % search_type, movie, quality, merge = True)
- if type_results:
- results += type_results
-
- sorted_results = sorted(results, key = lambda k: k['score'], reverse = True)
- if len(sorted_results) == 0:
- log.debug('Nothing found for %s in %s', (default_title, quality_type['quality']['label']))
-
- download_preference = self.conf('preferred_method')
- if download_preference != 'both':
- sorted_results = sorted(sorted_results, key = lambda k: k['type'], reverse = (download_preference == 'torrent'))
-
- # Check if movie isn't deleted while searching
- if not db.query(Movie).filter_by(id = movie.get('id')).first():
- break
-
- # Add them to this movie releases list
- for nzb in sorted_results:
-
- nzb_identifier = md5(nzb['url'])
- found_releases.append(nzb_identifier)
-
- rls = db.query(Release).filter_by(identifier = nzb_identifier).first()
- if not rls:
- rls = Release(
- identifier = nzb_identifier,
- movie_id = movie.get('id'),
- quality_id = quality_type.get('quality_id'),
- status_id = available_status.get('id')
- )
- db.add(rls)
- else:
- [db.delete(old_info) for old_info in rls.info]
-
- db.commit()
-
- for info in nzb:
- try:
- if not isinstance(nzb[info], (str, unicode, int, long, float)):
- continue
-
- rls_info = ReleaseInfo(
- identifier = info,
- value = toUnicode(nzb[info])
- )
- rls.info.append(rls_info)
- except InterfaceError:
- log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc()))
-
- db.commit()
-
- nzb['status_id'] = rls.status_id
-
-
- for nzb in sorted_results:
- if not quality_type.get('finish', False) and quality_type.get('wait_for', 0) > 0 and nzb.get('age') <= quality_type.get('wait_for', 0):
- log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name']))
- continue
-
- if nzb['status_id'] == ignored_status.get('id'):
- log.info('Ignored: %s', nzb['name'])
- continue
-
- if nzb['score'] <= 0:
- log.info('Ignored, score to low: %s', nzb['name'])
- continue
-
- downloaded = self.download(data = nzb, movie = movie)
- if downloaded is True:
- ret = True
- break
- elif downloaded != 'try_next':
- break
-
- # Remove releases that aren't found anymore
- for release in movie.get('releases', []):
- if release.get('status_id') == available_status.get('id') and release.get('identifier') not in found_releases:
- fireEvent('release.delete', release.get('id'), single = True)
-
- else:
- log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title))
- fireEvent('movie.restatus', movie['id'])
- break
-
- # Break if CP wants to shut down
- if self.shuttingDown() or ret:
- break
-
- fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True)
-
- return ret
-
- def download(self, data, movie, manual = False):
-
- # Test to see if any downloaders are enabled for this type
- downloader_enabled = fireEvent('download.enabled', manual, data, single = True)
-
- if downloader_enabled:
-
- snatched_status = fireEvent('status.get', 'snatched', single = True)
-
- # Download movie to temp
- filedata = None
- if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))):
- filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
- if filedata == 'try_next':
- return filedata
-
- successful = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
-
- if successful:
-
- try:
- # Mark release as snatched
- db = get_session()
- rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
- if rls:
- rls.status_id = snatched_status.get('id')
- db.commit()
-
- log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
- snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
- log.info(snatch_message)
- fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict())
-
- # If renamer isn't used, mark movie done
- if not Env.setting('enabled', 'renamer'):
- active_status = fireEvent('status.get', 'active', single = True)
- done_status = fireEvent('status.get', 'done', single = True)
- try:
- if movie['status_id'] == active_status.get('id'):
- for profile_type in movie['profile']['types']:
- if rls and profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
- log.info('Renamer disabled, marking movie as finished: %s', log_movie)
-
- # Mark release done
- rls.status_id = done_status.get('id')
- db.commit()
-
- # Mark movie done
- mvie = db.query(Movie).filter_by(id = movie['id']).first()
- mvie.status_id = done_status.get('id')
- db.commit()
- except:
- log.error('Failed marking movie finished, renamer disabled: %s', traceback.format_exc())
-
- except:
- log.error('Failed marking movie finished: %s', traceback.format_exc())
-
- return True
-
- log.info('Tried to download, but none of the "%s" downloaders are enabled', (data.get('type', '')))
-
- return False
-
- def getSearchTypes(self):
-
- download_types = fireEvent('download.enabled_types', merge = True)
- provider_types = fireEvent('provider.enabled_types', merge = True)
-
- if download_types and len(list(set(provider_types) & set(download_types))) == 0:
- log.error('There aren\'t any providers enabled for your downloader (%s). Check your settings.', ','.join(download_types))
- raise NoProviders
-
- for useless_provider in list(set(provider_types) - set(download_types)):
- log.debug('Provider for "%s" enabled, but no downloader.', useless_provider)
-
- search_types = download_types
-
- if len(search_types) == 0:
- log.error('There aren\'t any downloaders enabled. Please pick one in settings.')
- raise NoDownloaders
-
- return search_types
-
- def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs):
-
- imdb_results = kwargs.get('imdb_results', False)
- retention = Env.setting('retention', section = 'nzb')
-
- if nzb.get('seeders') is None and 0 < retention < nzb.get('age', 0):
- log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name']))
- return False
-
- movie_name = getTitle(movie['library'])
- movie_words = re.split('\W+', simplifyString(movie_name))
- nzb_name = simplifyString(nzb['name'])
- nzb_words = re.split('\W+', nzb_name)
- required_words = splitString(self.conf('required_words').lower())
-
- req_match = 0
- for req_set in required_words:
- req = splitString(req_set, '&')
- req_match += len(list(set(nzb_words) & set(req))) == len(req)
-
- if self.conf('required_words') and req_match == 0:
- log.info2("Wrong: Required word missing: %s" % nzb['name'])
- return False
-
- ignored_words = splitString(self.conf('ignored_words').lower())
- blacklisted = list(set(nzb_words) & set(ignored_words) - set(movie_words))
- if self.conf('ignored_words') and blacklisted:
- log.info2("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted)))
- return False
-
- pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic']
- pron_words = list(set(nzb_words) & set(pron_tags) - set(movie_words))
- if pron_words:
- log.info('Wrong: %s, probably pr0n', (nzb['name']))
- return False
-
- #qualities = fireEvent('quality.all', single = True)
- preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True)
-
- # Contains lower quality string
- if self.containsOtherQuality(nzb, movie_year = movie['library']['year'], preferred_quality = preferred_quality):
- log.info2('Wrong: %s, looking for %s', (nzb['name'], quality['label']))
- return False
-
-
- # File to small
- if nzb['size'] and preferred_quality['size_min'] > nzb['size']:
- log.info2('Wrong: "%s" is too small to be %s. %sMB instead of the minimal of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_min']))
- return False
-
- # File to large
- if nzb['size'] and preferred_quality.get('size_max') < nzb['size']:
- log.info2('Wrong: "%s" is too large to be %s. %sMB instead of the maximum of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_max']))
- return False
-
-
- # Provider specific functions
- get_more = nzb.get('get_more_info')
- if get_more:
- get_more(nzb)
-
- extra_check = nzb.get('extra_check')
- if extra_check and not extra_check(nzb):
- return False
-
-
- if imdb_results:
- return True
-
- # Check if nzb contains imdb link
- if self.checkIMDB([nzb.get('description', '')], movie['library']['identifier']):
- return True
-
- for raw_title in movie['library']['titles']:
- for movie_title in possibleTitles(raw_title['title']):
- movie_words = re.split('\W+', simplifyString(movie_title))
-
- if self.correctName(nzb['name'], movie_title):
- # if no IMDB link, at least check year range 1
- if len(movie_words) > 2 and self.correctYear([nzb['name']], movie['library']['year'], 1):
- return True
-
- # if no IMDB link, at least check year
- if len(movie_words) <= 2 and self.correctYear([nzb['name']], movie['library']['year'], 0):
- return True
-
- log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie_name, movie['library']['year']))
- return False
-
- def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = {}):
-
- name = nzb['name']
- size = nzb.get('size', 0)
- nzb_words = re.split('\W+', simplifyString(name))
-
- qualities = fireEvent('quality.all', single = True)
-
- found = {}
- for quality in qualities:
- # Main in words
- if quality['identifier'] in nzb_words:
- found[quality['identifier']] = True
-
- # Alt in words
- if list(set(nzb_words) & set(quality['alternative'])):
- found[quality['identifier']] = True
-
- # Try guessing via quality tags
- guess = fireEvent('quality.guess', [nzb.get('name')], single = True)
- if guess:
- found[guess['identifier']] = True
-
- # Hack for older movies that don't contain quality tag
- year_name = fireEvent('scanner.name_year', name, single = True)
- if len(found) == 0 and movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None):
- if size > 3000: # Assume dvdr
- log.info('Quality was missing in name, assuming it\'s a DVD-R based on the size: %s', (size))
- found['dvdr'] = True
- else: # Assume dvdrip
- log.info('Quality was missing in name, assuming it\'s a DVD-Rip based on the size: %s', (size))
- found['dvdrip'] = True
-
- # Allow other qualities
- for allowed in preferred_quality.get('allow'):
- if found.get(allowed):
- del found[allowed]
-
- return not (found.get(preferred_quality['identifier']) and len(found) == 1)
-
- def checkIMDB(self, haystack, imdbId):
-
- for string in haystack:
- if 'imdb.com/title/' + imdbId in string:
- return True
-
- return False
-
- def correctYear(self, haystack, year, year_range):
-
- for string in haystack:
-
- year_name = fireEvent('scanner.name_year', string, single = True)
-
- if year_name and ((year - year_range) <= year_name.get('year') <= (year + year_range)):
- log.debug('Movie year matches range: %s looking for %s', (year_name.get('year'), year))
- return True
-
- log.debug('Movie year doesn\'t matche range: %s looking for %s', (year_name.get('year'), year))
- return False
-
- def correctName(self, check_name, movie_name):
-
- check_names = [check_name]
-
- # Match names between "
- try: check_names.append(re.search(r'([\'"])[^\1]*\1', check_name).group(0))
- except: pass
-
- # Match longest name between []
- try: check_names.append(max(check_name.split('['), key = len))
- except: pass
-
- for check_name in list(set(check_names)):
- check_movie = fireEvent('scanner.name_year', check_name, single = True)
-
- try:
- check_words = filter(None, re.split('\W+', check_movie.get('name', '')))
- movie_words = filter(None, re.split('\W+', simplifyString(movie_name)))
-
- if len(check_words) > 0 and len(movie_words) > 0 and len(list(set(check_words) - set(movie_words))) == 0:
- return True
- except:
- pass
-
- return False
-
- def couldBeReleased(self, is_pre_release, dates):
-
- now = int(time.time())
-
- if not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0):
- return True
- else:
-
- # For movies before 1972
- if dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0:
- return True
-
- if is_pre_release:
- # Prerelease 1 week before theaters
- if dates.get('theater') - 604800 < now:
- return True
- else:
- # 12 weeks after theater release
- if dates.get('theater') > 0 and dates.get('theater') + 7257600 < now:
- return True
-
- if dates.get('dvd') > 0:
-
- # 4 weeks before dvd release
- if dates.get('dvd') - 2419200 < now:
- return True
-
- # Dvd should be released
- if dates.get('dvd') < now:
- return True
-
-
- return False
-
- def tryNextReleaseView(self):
-
- trynext = self.tryNextRelease(getParam('id'))
-
- return jsonified({
- 'success': trynext
- })
-
- def tryNextRelease(self, movie_id, manual = False):
-
- snatched_status = fireEvent('status.get', 'snatched', single = True)
- ignored_status = fireEvent('status.get', 'ignored', single = True)
-
- try:
- db = get_session()
- rels = db.query(Release).filter_by(
- status_id = snatched_status.get('id'),
- movie_id = movie_id
- ).all()
-
- for rel in rels:
- rel.status_id = ignored_status.get('id')
- db.commit()
-
- movie_dict = fireEvent('movie.get', movie_id, single = True)
- log.info('Trying next release for: %s', getTitle(movie_dict['library']))
- fireEvent('searcher.single', movie_dict)
-
- return True
-
- except:
- log.error('Failed searching for next release: %s', traceback.format_exc())
- return False
-
-class SearchSetupError(Exception):
- pass
-
-class NoDownloaders(SearchSetupError):
- pass
-
-class NoProviders(SearchSetupError):
- pass
diff --git a/couchpotato/core/plugins/status/__init__.py b/couchpotato/core/plugins/status/__init__.py
deleted file mode 100644
index fb5b4cc79b..0000000000
--- a/couchpotato/core/plugins/status/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from .main import StatusPlugin
-
-def start():
- return StatusPlugin()
-
-config = []
diff --git a/couchpotato/core/plugins/status/main.py b/couchpotato/core/plugins/status/main.py
deleted file mode 100644
index c01caef597..0000000000
--- a/couchpotato/core/plugins/status/main.py
+++ /dev/null
@@ -1,109 +0,0 @@
-from couchpotato import get_session
-from couchpotato.api import addApiView
-from couchpotato.core.event import addEvent
-from couchpotato.core.helpers.encoding import toUnicode
-from couchpotato.core.helpers.request import jsonified
-from couchpotato.core.logger import CPLog
-from couchpotato.core.plugins.base import Plugin
-from couchpotato.core.settings.model import Status
-
-log = CPLog(__name__)
-
-
-class StatusPlugin(Plugin):
-
- statuses = {
- 'needs_update': 'Needs update',
- 'active': 'Active',
- 'done': 'Done',
- 'downloaded': 'Downloaded',
- 'wanted': 'Wanted',
- 'snatched': 'Snatched',
- 'failed': 'Failed',
- 'deleted': 'Deleted',
- 'ignored': 'Ignored',
- 'available': 'Available',
- 'suggest': 'Suggest',
- }
-
- def __init__(self):
- addEvent('status.add', self.add)
- addEvent('status.get', self.add) # Alias for .add
- addEvent('status.get_by_id', self.getById)
- addEvent('status.all', self.all)
- addEvent('app.initialize', self.fill)
-
- addApiView('status.list', self.list, docs = {
- 'desc': 'Check for available update',
- 'return': {'type': 'object', 'example': """{
- 'success': True,
- 'list': array, statuses
-}"""}
- })
-
- def list(self):
-
- return jsonified({
- 'success': True,
- 'list': self.all()
- })
-
- def getById(self, id):
- db = get_session()
- status = db.query(Status).filter_by(id = id).first()
- status_dict = status.to_dict()
- #db.close()
-
- return status_dict
-
- def all(self):
-
- db = get_session()
-
- statuses = db.query(Status).all()
-
- temp = []
- for status in statuses:
- s = status.to_dict()
- temp.append(s)
-
- #db.close()
- return temp
-
- def add(self, identifier):
-
- db = get_session()
-
- s = db.query(Status).filter_by(identifier = identifier).first()
- if not s:
- s = Status(
- identifier = identifier,
- label = toUnicode(identifier.capitalize())
- )
- db.add(s)
- db.commit()
-
- status_dict = s.to_dict()
-
- #db.close()
- return status_dict
-
- def fill(self):
-
- db = get_session()
-
- for identifier, label in self.statuses.iteritems():
- s = db.query(Status).filter_by(identifier = identifier).first()
- if not s:
- log.info('Creating status: %s', label)
- s = Status(
- identifier = identifier,
- label = toUnicode(label)
- )
- db.add(s)
-
- s.label = toUnicode(label)
- db.commit()
-
- #db.close()
-
diff --git a/couchpotato/core/plugins/status/static/status.js b/couchpotato/core/plugins/status/static/status.js
deleted file mode 100644
index 2b8d30f348..0000000000
--- a/couchpotato/core/plugins/status/static/status.js
+++ /dev/null
@@ -1,17 +0,0 @@
-var StatusBase = new Class({
-
- setup: function(statuses){
- var self = this;
-
- self.statuses = statuses;
-
- },
-
- get: function(id){
- return this.statuses.filter(function(status){
- return status.id == id
- }).pick()
- },
-
-});
-window.Status = new StatusBase();
diff --git a/couchpotato/core/plugins/subtitle.py b/couchpotato/core/plugins/subtitle.py
new file mode 100644
index 0000000000..e8baef0db8
--- /dev/null
+++ b/couchpotato/core/plugins/subtitle.py
@@ -0,0 +1,86 @@
+import traceback
+
+from couchpotato.core.event import addEvent
+from couchpotato.core.helpers.encoding import toUnicode, sp
+from couchpotato.core.helpers.variable import splitString
+from couchpotato.core.logger import CPLog
+from couchpotato.core.plugins.base import Plugin
+from couchpotato.environment import Env
+import subliminal
+
+
+log = CPLog(__name__)
+
+autoload = 'Subtitle'
+
+
+class Subtitle(Plugin):
+
+ services = ['opensubtitles', 'thesubdb', 'subswiki', 'subscenter']
+
+ def __init__(self):
+ addEvent('renamer.before', self.searchSingle)
+
+ def searchSingle(self, group):
+ if self.isDisabled(): return
+
+ try:
+ available_languages = sum(group['subtitle_language'].values(), [])
+ downloaded = []
+ files = [toUnicode(x) for x in group['files']['movie']]
+ log.debug('Searching for subtitles for: %s', files)
+
+ for lang in self.getLanguages():
+ if lang not in available_languages:
+ download = subliminal.download_subtitles(files, multi = True, force = self.conf('force'), languages = [lang], services = self.services, cache_dir = Env.get('cache_dir'))
+ for subtitle in download:
+ downloaded.extend(download[subtitle])
+
+ for d_sub in downloaded:
+ log.info('Found subtitle (%s): %s', (d_sub.language.alpha2, files))
+ group['files']['subtitle'].append(sp(d_sub.path))
+ group['before_rename'].append(sp(d_sub.path))
+ group['subtitle_language'][sp(d_sub.path)] = [d_sub.language.alpha2]
+
+ return True
+
+ except:
+ log.error('Failed searching for subtitle: %s', (traceback.format_exc()))
+
+ return False
+
+ def getLanguages(self):
+ return splitString(self.conf('languages'))
+
+
+config = [{
+ 'name': 'subtitle',
+ 'groups': [
+ {
+ 'tab': 'renamer',
+ 'name': 'subtitle',
+ 'label': 'Download subtitles',
+ 'description': 'after rename',
+ 'options': [
+ {
+ 'name': 'enabled',
+ 'label': 'Search and download subtitles',
+ 'default': False,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'languages',
+ 'description': ('Comma separated, 2 letter country code.', 'Example: en, nl. See the codes at on Wikipedia'),
+ },
+ {
+ 'advanced': True,
+ 'name': 'force',
+ 'label': 'Force',
+ 'description': ('Force download all languages (including embedded).', 'This will also overwrite all existing subtitles.'),
+ 'default': False,
+ 'type': 'bool',
+ },
+ ],
+ },
+ ],
+}]
diff --git a/couchpotato/core/plugins/subtitle/__init__.py b/couchpotato/core/plugins/subtitle/__init__.py
deleted file mode 100644
index 686d385e6c..0000000000
--- a/couchpotato/core/plugins/subtitle/__init__.py
+++ /dev/null
@@ -1,34 +0,0 @@
-from .main import Subtitle
-
-def start():
- return Subtitle()
-
-config = [{
- 'name': 'subtitle',
- 'groups': [
- {
- 'tab': 'renamer',
- 'name': 'subtitle',
- 'label': 'Download subtitles',
- 'description': 'after rename',
- 'options': [
- {
- 'name': 'enabled',
- 'label': 'Search and download subtitles',
- 'default': False,
- 'type': 'enabler',
- },
- {
- 'name': 'languages',
- 'description': 'Comma separated, 2 letter country code. Example: en, nl',
- },
-# {
-# 'name': 'automatic',
-# 'default': True,
-# 'type': 'bool',
-# 'description': 'Automaticly search & download for movies in library',
-# },
- ],
- },
- ],
-}]
diff --git a/couchpotato/core/plugins/subtitle/main.py b/couchpotato/core/plugins/subtitle/main.py
deleted file mode 100644
index 73ead0871f..0000000000
--- a/couchpotato/core/plugins/subtitle/main.py
+++ /dev/null
@@ -1,73 +0,0 @@
-from couchpotato import get_session
-from couchpotato.core.event import addEvent, fireEvent
-from couchpotato.core.helpers.encoding import toUnicode
-from couchpotato.core.helpers.variable import splitString
-from couchpotato.core.logger import CPLog
-from couchpotato.core.plugins.base import Plugin
-from couchpotato.core.settings.model import Library, FileType
-from couchpotato.environment import Env
-import subliminal
-import traceback
-
-log = CPLog(__name__)
-
-
-class Subtitle(Plugin):
-
- services = ['opensubtitles', 'thesubdb', 'subswiki', 'podnapisi']
-
- def __init__(self):
- addEvent('renamer.before', self.searchSingle)
-
- def searchLibrary(self):
-
- # Get all active and online movies
- db = get_session()
-
- library = db.query(Library).all()
- done_status = fireEvent('status.get', 'done', single = True)
-
- for movie in library.movies:
-
- for release in movie.releases:
-
- # get releases and their movie files
- if release.status_id is done_status.get('id'):
-
- files = []
- for file in release.files.filter(FileType.status.has(identifier = 'movie')).all():
- files.append(file.path);
-
- # get subtitles for those files
- subliminal.list_subtitles(files, cache_dir = Env.get('cache_dir'), multi = True, languages = self.getLanguages(), services = self.services)
-
- def searchSingle(self, group):
-
- if self.isDisabled(): return
-
- try:
- available_languages = sum(group['subtitle_language'].itervalues(), [])
- downloaded = []
- files = [toUnicode(x) for x in group['files']['movie']]
- log.debug('Searching for subtitles for: %s', files)
-
- for lang in self.getLanguages():
- if lang not in available_languages:
- download = subliminal.download_subtitles(files, multi = True, force = False, languages = [lang], services = self.services, cache_dir = Env.get('cache_dir'))
- for subtitle in download:
- downloaded.extend(download[subtitle])
-
- for d_sub in downloaded:
- log.info('Found subtitle (%s): %s', (d_sub.language.alpha2, files))
- group['files']['subtitle'].add(d_sub.path)
- group['subtitle_language'][d_sub.path] = [d_sub.language.alpha2]
-
- return True
-
- except:
- log.error('Failed searching for subtitle: %s', (traceback.format_exc()))
-
- return False
-
- def getLanguages(self):
- return splitString(self.conf('languages'))
diff --git a/couchpotato/core/plugins/suggestion/__init__.py b/couchpotato/core/plugins/suggestion/__init__.py
deleted file mode 100644
index b63b5b13ef..0000000000
--- a/couchpotato/core/plugins/suggestion/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from .main import Suggestion
-
-def start():
- return Suggestion()
-
-config = []
diff --git a/couchpotato/core/plugins/suggestion/main.py b/couchpotato/core/plugins/suggestion/main.py
deleted file mode 100644
index 2c31ca32ad..0000000000
--- a/couchpotato/core/plugins/suggestion/main.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from couchpotato.api import addApiView
-from couchpotato.core.event import fireEvent
-from couchpotato.core.helpers.request import jsonified, getParam
-from couchpotato.core.plugins.base import Plugin
-
-class Suggestion(Plugin):
-
- def __init__(self):
-
- addApiView('suggestion.view', self.getView)
-
- def getView(self):
-
- limit_offset = getParam('limit_offset', None)
- total_movies, movies = fireEvent('movie.list', status = 'suggest', limit_offset = limit_offset, single = True)
-
- return jsonified({
- 'success': True,
- 'empty': len(movies) == 0,
- 'total': total_movies,
- 'movies': movies,
- })
diff --git a/couchpotato/core/plugins/trailer.py b/couchpotato/core/plugins/trailer.py
new file mode 100644
index 0000000000..82216b8ed0
--- /dev/null
+++ b/couchpotato/core/plugins/trailer.py
@@ -0,0 +1,80 @@
+import os
+
+from couchpotato.core.event import addEvent, fireEvent
+from couchpotato.core.helpers.variable import getExt, getTitle
+from couchpotato.core.logger import CPLog
+from couchpotato.core.plugins.base import Plugin
+
+
+log = CPLog(__name__)
+
+autoload = 'Trailer'
+
+
+class Trailer(Plugin):
+
+ def __init__(self):
+ addEvent('renamer.after', self.searchSingle)
+
+ def searchSingle(self, message = None, group = None):
+ if not group: group = {}
+ if self.isDisabled() or len(group['files']['trailer']) > 0: return
+
+ trailers = fireEvent('trailer.search', group = group, merge = True)
+ if not trailers or trailers == []:
+ log.info('No trailers found for: %s', getTitle(group))
+ return False
+
+ for trailer in trailers.get(self.conf('quality'), []):
+
+ ext = getExt(trailer)
+ filename = self.conf('name').replace('', group['filename']) + ('.%s' % ('mp4' if len(ext) > 5 else ext))
+ destination = os.path.join(group['destination_dir'], filename)
+ if not os.path.isfile(destination):
+ trailer_file = fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True)
+ if trailer_file and os.path.getsize(trailer_file) < (1024 * 1024): # Don't trust small trailers (1MB), try next one
+ os.unlink(trailer_file)
+ continue
+ else:
+ log.debug('Trailer already exists: %s', destination)
+
+ group['renamed_files'].append(destination)
+
+ # Download first and break
+ break
+
+ return True
+
+
+config = [{
+ 'name': 'trailer',
+ 'groups': [
+ {
+ 'tab': 'renamer',
+ 'name': 'trailer',
+ 'label': 'Download trailer',
+ 'description': 'after rename',
+ 'options': [
+ {
+ 'name': 'enabled',
+ 'label': 'Search and download trailers',
+ 'default': False,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'quality',
+ 'default': '720p',
+ 'type': 'dropdown',
+ 'values': [('1080p', '1080p'), ('720p', '720p'), ('480P', '480p')],
+ },
+ {
+ 'name': 'name',
+ 'label': 'Naming',
+ 'default': '-trailer',
+ 'advanced': True,
+ 'description': 'Use <filename> to use above settings.'
+ },
+ ],
+ },
+ ],
+}]
diff --git a/couchpotato/core/plugins/trailer/__init__.py b/couchpotato/core/plugins/trailer/__init__.py
deleted file mode 100644
index 282b3482a9..0000000000
--- a/couchpotato/core/plugins/trailer/__init__.py
+++ /dev/null
@@ -1,37 +0,0 @@
-from .main import Trailer
-
-def start():
- return Trailer()
-
-config = [{
- 'name': 'trailer',
- 'groups': [
- {
- 'tab': 'renamer',
- 'name': 'trailer',
- 'label': 'Download trailer',
- 'description': 'after rename',
- 'options': [
- {
- 'name': 'enabled',
- 'label': 'Search and download trailers',
- 'default': False,
- 'type': 'enabler',
- },
- {
- 'name': 'quality',
- 'default': '720p',
- 'type': 'dropdown',
- 'values': [('1080P', '1080p'), ('720P', '720p'), ('480P', '480p')],
- },
- {
- 'name': 'name',
- 'label': 'Naming',
- 'default': '-trailer',
- 'advanced': True,
- 'description': 'Use to use above settings.'
- },
- ],
- },
- ],
-}]
diff --git a/couchpotato/core/plugins/trailer/main.py b/couchpotato/core/plugins/trailer/main.py
deleted file mode 100644
index 4ab51e788d..0000000000
--- a/couchpotato/core/plugins/trailer/main.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from couchpotato.core.event import addEvent, fireEvent
-from couchpotato.core.helpers.variable import getExt, getTitle
-from couchpotato.core.logger import CPLog
-from couchpotato.core.plugins.base import Plugin
-import os
-
-log = CPLog(__name__)
-
-
-class Trailer(Plugin):
-
- def __init__(self):
- addEvent('renamer.after', self.searchSingle)
-
- def searchSingle(self, message = None, group = {}):
-
- if self.isDisabled() or len(group['files']['trailer']) > 0: return
-
- trailers = fireEvent('trailer.search', group = group, merge = True)
- if not trailers or trailers == []:
- log.info('No trailers found for: %s', getTitle(group['library']))
- return False
-
- for trailer in trailers.get(self.conf('quality'), []):
- filename = self.conf('name').replace('', group['filename']) + ('.%s' % getExt(trailer))
- destination = os.path.join(group['destination_dir'], filename)
- if not os.path.isfile(destination):
- fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True)
- else:
- log.debug('Trailer already exists: %s', destination)
-
- group['renamed_files'].append(destination)
-
- # Download first and break
- break
-
- return True
-
diff --git a/couchpotato/core/plugins/userscript/__init__.py b/couchpotato/core/plugins/userscript/__init__.py
index 5df5a801f1..9d708593ba 100644
--- a/couchpotato/core/plugins/userscript/__init__.py
+++ b/couchpotato/core/plugins/userscript/__init__.py
@@ -1,6 +1,5 @@
from .main import Userscript
-def start():
- return Userscript()
-config = []
+def autoload():
+ return Userscript()
diff --git a/couchpotato/core/plugins/userscript/bookmark.js b/couchpotato/core/plugins/userscript/bookmark.js
deleted file mode 100644
index 5ee8c376cc..0000000000
--- a/couchpotato/core/plugins/userscript/bookmark.js
+++ /dev/null
@@ -1,43 +0,0 @@
-var includes = {{includes|tojson}};
-var excludes = {{excludes|tojson}};
-
-var specialChars = '\\{}+.():-|^$';
-var makeRegex = function(pattern) {
- pattern = pattern.split('');
- var i, len = pattern.length;
- for( i = 0; i < len; i++) {
- var character = pattern[i];
- if(specialChars.indexOf(character) > -1) {
- pattern[i] = '\\' + character;
- } else if(character === '?') {
- pattern[i] = '.';
- } else if(character === '*') {
- pattern[i] = '.*';
- }
- }
- return new RegExp('^' + pattern.join('') + '$');
-};
-
-var isCorrectUrl = function() {
- for(i in includes) {
- var reg = includes[i]
- if (makeRegex(reg).test(document.location.href))
- return true;
- }
- return false;
-}
-var addUserscript = function() {
- // Add window param
- document.body.setAttribute('cp_auto_open', true)
-
- // Load userscript
- var e = document.createElement('script');
- e.setAttribute('type', 'text/javascript');
- e.setAttribute('charset', 'UTF-8');
- e.setAttribute('src', '{{host}}couchpotato.js?r=' + Math.random() * 99999999);
- document.body.appendChild(e)
-}
-if(isCorrectUrl())
- addUserscript()
-else
- alert('Can\'t find a proper movie on this page..')
diff --git a/couchpotato/core/plugins/userscript/bookmark.js_tmpl b/couchpotato/core/plugins/userscript/bookmark.js_tmpl
new file mode 100644
index 0000000000..cc04baf7f1
--- /dev/null
+++ b/couchpotato/core/plugins/userscript/bookmark.js_tmpl
@@ -0,0 +1,47 @@
+{% autoescape None %}
+
+var includes = {{ json_encode(includes) }};
+var excludes = {{ json_encode(excludes) }};
+
+var specialChars = '\\{}+.():-|^$';
+var makeRegex = function(pattern) {
+ pattern = pattern.split('');
+ var i, len = pattern.length;
+ for( i = 0; i < len; i++) {
+ var character = pattern[i];
+ if(specialChars.indexOf(character) > -1) {
+ pattern[i] = '\\' + character;
+ } else if(character === '?') {
+ pattern[i] = '.';
+ } else if(character === '*') {
+ pattern[i] = '.*';
+ }
+ }
+ return new RegExp('^' + pattern.join('') + '$');
+};
+
+var isCorrectUrl = function() {
+ for(i in includes) {
+ if(!includes.hasOwnProperty(i)) continue;
+
+ var reg = includes[i]
+ if (makeRegex(reg).test(document.location.href))
+ return true;
+ }
+ return false;
+}
+var addUserscript = function() {
+ // Add window param
+ document.body.setAttribute('cp_auto_open', 'true')
+
+ // Load userscript
+ var e = document.createElement('script');
+ e.setAttribute('type', 'text/javascript');
+ e.setAttribute('charset', 'UTF-8');
+ e.setAttribute('src', '{{host}}couchpotato.js?r=' + Math.random() * 99999999);
+ document.body.appendChild(e)
+}
+if(isCorrectUrl())
+ addUserscript()
+else
+ alert('Can\'t find a proper movie on this page..')
diff --git a/couchpotato/core/plugins/userscript/main.py b/couchpotato/core/plugins/userscript/main.py
index 3ff7fb072b..04995260e6 100644
--- a/couchpotato/core/plugins/userscript/main.py
+++ b/couchpotato/core/plugins/userscript/main.py
@@ -1,63 +1,75 @@
+import os
+import traceback
+import time
+from base64 import b64encode, b64decode
+
+from couchpotato import index
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent
-from couchpotato.core.helpers.request import getParam, jsonified
from couchpotato.core.helpers.variable import isDict
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
-from flask.globals import request
-from flask.helpers import url_for
-from flask.templating import render_template
-import os
+from tornado.web import RequestHandler
+
log = CPLog(__name__)
class Userscript(Plugin):
- version = 3
+ version = 8
def __init__(self):
- addApiView('userscript.get//', self.getUserScript, static = True)
+ addApiView('userscript.get/(.*)/(.*)', self.getUserScript, static = True)
+
addApiView('userscript', self.iFrame)
addApiView('userscript.add_via_url', self.getViaUrl)
addApiView('userscript.includes', self.getIncludes)
addApiView('userscript.bookmark', self.bookmark)
addEvent('userscript.get_version', self.getVersion)
+ addEvent('app.test', self.doTest)
- def bookmark(self):
+ def bookmark(self, host = None, **kwargs):
params = {
'includes': fireEvent('userscript.get_includes', merge = True),
'excludes': fireEvent('userscript.get_excludes', merge = True),
- 'host': getParam('host', None),
+ 'host': host,
}
- return self.renderTemplate(__file__, 'bookmark.js', **params)
+ return self.renderTemplate(__file__, 'bookmark.js_tmpl', **params)
- def getIncludes(self):
+ def getIncludes(self, **kwargs):
- return jsonified({
+ return {
'includes': fireEvent('userscript.get_includes', merge = True),
'excludes': fireEvent('userscript.get_excludes', merge = True),
- })
+ }
- def getUserScript(self, random = '', filename = ''):
+ def getUserScript(self, script_route, **kwargs):
- params = {
- 'includes': fireEvent('userscript.get_includes', merge = True),
- 'excludes': fireEvent('userscript.get_excludes', merge = True),
- 'version': self.getVersion(),
- 'api': '%suserscript/' % url_for('api.index').lstrip('/'),
- 'host': request.host_url,
- }
+ klass = self
+
+ class UserscriptHandler(RequestHandler):
+
+ def get(self, random, route):
- script = self.renderTemplate(__file__, 'template.js', **params)
- self.createFile(os.path.join(Env.get('cache_dir'), 'couchpotato.user.js'), script)
+ params = {
+ 'includes': fireEvent('userscript.get_includes', merge = True),
+ 'excludes': fireEvent('userscript.get_excludes', merge = True),
+ 'version': klass.getVersion(),
+ 'api': '%suserscript/' % Env.get('api_base'),
+ 'host': '%s://%s' % (self.request.protocol, self.request.headers.get('X-Forwarded-Host') or self.request.headers.get('host')),
+ }
- from flask.helpers import send_from_directory
- return send_from_directory(Env.get('cache_dir'), 'couchpotato.user.js')
+ script = klass.renderTemplate(__file__, 'template.js_tmpl', **params)
+ klass.createFile(os.path.join(Env.get('cache_dir'), 'couchpotato.user.js'), script)
+
+ self.redirect(Env.get('api_base') + 'file.cache/couchpotato.user.js')
+
+ Env.get('app').add_handlers(".*$", [('%s%s' % (Env.get('api_base'), script_route), UserscriptHandler)])
def getVersion(self):
@@ -69,12 +81,10 @@ def getVersion(self):
return version
- def iFrame(self):
- return render_template('index.html', sep = os.sep, fireEvent = fireEvent, env = Env)
-
- def getViaUrl(self):
+ def iFrame(self, **kwargs):
+ return index()
- url = getParam('url')
+ def getViaUrl(self, url = None, **kwargs):
params = {
'url': url,
@@ -84,4 +94,47 @@ def getViaUrl(self):
log.error('Failed adding movie via url: %s', url)
params['error'] = params['movie'] if params['movie'] else 'Failed getting movie info'
- return jsonified(params)
+ return params
+
+ def doTest(self):
+ time.sleep(1)
+
+ tests = [
+ 'aHR0cDovL3d3dy5hbGxvY2luZS5mci9maWxtL2ZpY2hlZmlsbV9nZW5fY2ZpbG09MjAxMTA1Lmh0bWw=',
+ 'aHR0cDovL3RyYWlsZXJzLmFwcGxlLmNvbS90cmFpbGVycy9wYXJhbW91bnQvbWlzc2lvbmltcG9zc2libGVyb2d1ZW5hdGlvbi8=',
+ 'aHR0cDovL3d3dy55b3V0aGVhdGVyLmNvbS92aWV3LnBocD9pZD0xMTI2Mjk5',
+ 'aHR0cDovL3RyYWt0LnR2L21vdmllcy9taXNzaW9uLWltcG9zc2libGUtcm9ndWUtbmF0aW9uLTIwMTU=',
+ 'aHR0cHM6Ly93d3cucmVkZGl0LmNvbS9yL0lqdXN0d2F0Y2hlZC9jb21tZW50cy8zZjk3bzYvaWp3X21pc3Npb25faW1wb3NzaWJsZV9yb2d1ZV9uYXRpb25fMjAxNS8=',
+ 'aHR0cDovL3d3dy5yb3R0ZW50b21hdG9lcy5jb20vbS9taXNzaW9uX2ltcG9zc2libGVfcm9ndWVfbmF0aW9uLw==',
+ 'aHR0cHM6Ly93d3cudGhlbW92aWVkYi5vcmcvbW92aWUvMTc3Njc3LW1pc3Npb24taW1wb3NzaWJsZS01',
+ 'aHR0cDovL3d3dy5jcml0aWNrZXIuY29tL2ZpbG0vTWlzc2lvbl9JbXBvc3NpYmxlX1JvZ3VlLw==',
+ 'aHR0cDovL2ZpbG1jZW50cnVtLm5sL2ZpbG1zLzE4MzIzL21pc3Npb24taW1wb3NzaWJsZS1yb2d1ZS1uYXRpb24v',
+ 'aHR0cDovL3d3dy5maWxtc3RhcnRzLmRlL2tyaXRpa2VuLzIwMTEwNS5odG1s',
+ 'aHR0cDovL3d3dy5maWxtd2ViLnBsL2ZpbG0vTWlzc2lvbiUzQStJbXBvc3NpYmxlKy0rUm9ndWUrTmF0aW9uLTIwMTUtNjU1MDQ4',
+ 'aHR0cDovL3d3dy5mbGlja2NoYXJ0LmNvbS9tb3ZpZS8zM0NFMzEyNUJB',
+ 'aHR0cDovL3d3dy5pbWRiLmNvbS90aXRsZS90dDIzODEyNDkv',
+ 'aHR0cDovL2xldHRlcmJveGQuY29tL2ZpbG0vbWlzc2lvbi1pbXBvc3NpYmxlLXJvZ3VlLW5hdGlvbi8=',
+ 'aHR0cDovL3d3dy5tb3ZpZW1ldGVyLm5sL2ZpbG0vMTA0MTcw',
+ 'aHR0cDovL21vdmllcy5pby9tLzMxL2Vu',
+ ]
+
+ success = 0
+ for x in tests:
+ x = b64decode(x)
+ try:
+ movie = self.getViaUrl(x)
+ movie = movie.get('movie', {}) or {}
+ imdb = movie.get('imdb')
+
+ if imdb and b64encode(imdb) in ['dHQxMjI5MjM4', 'dHQyMzgxMjQ5']:
+ success += 1
+ continue
+ except:
+ log.error('Failed userscript test "%s": %s', (x, traceback.format_exc()))
+
+ log.error('Failed userscript test "%s"', x)
+
+ if success == len(tests):
+ log.debug('All userscript tests successful')
+ else:
+ log.error('Failed userscript tests, %s out of %s', (success, len(tests)))
diff --git a/couchpotato/core/plugins/userscript/static/userscript.css b/couchpotato/core/plugins/userscript/static/userscript.css
deleted file mode 100644
index 304dfa74f0..0000000000
--- a/couchpotato/core/plugins/userscript/static/userscript.css
+++ /dev/null
@@ -1,14 +0,0 @@
-.page.userscript {
- position: absolute;
- width: 100%;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
-}
-
- .page.userscript .frame.loading {
- text-align: center;
- font-size: 20px;
- padding: 20px;
- }
diff --git a/couchpotato/core/plugins/userscript/static/userscript.js b/couchpotato/core/plugins/userscript/static/userscript.js
index d6d5983c24..15e018dc1a 100644
--- a/couchpotato/core/plugins/userscript/static/userscript.js
+++ b/couchpotato/core/plugins/userscript/static/userscript.js
@@ -2,6 +2,7 @@ Page.Userscript = new Class({
Extends: PageBase,
+ order: 80,
name: 'userscript',
has_tab: false,
@@ -12,10 +13,10 @@ Page.Userscript = new Class({
}
},
- indexAction: function(param){
+ indexAction: function(){
var self = this;
- self.el.adopt(
+ self.content.grab(
self.frame = new Element('div.frame.loading', {
'text': 'Loading...'
})
@@ -34,7 +35,7 @@ Page.Userscript = new Class({
if(json.error)
self.frame.set('html', json.error);
else {
- var item = new Block.Search.Item(json.movie);
+ var item = new BlockSearchMovieItem(json.movie);
self.frame.adopt(item);
item.showOptions();
}
@@ -53,60 +54,56 @@ var UserscriptSettingTab = new Class({
initialize: function(){
var self = this;
- App.addEvent('load', self.addSettings.bind(self))
+ App.addEvent('loadSettings', self.addSettings.bind(self));
},
addSettings: function(){
var self = this;
- self.settings = App.getPage('Settings')
+ self.settings = App.getPage('Settings');
self.settings.addEvent('create', function(){
- // See if userscript can be installed
- var userscript = false;
- try {
- if(Components.interfaces.gmIGreasemonkeyService)
- userscript = true
- }
- catch(e){
- userscript = Browser.chrome === true;
- }
-
var host_url = window.location.protocol + '//' + window.location.host;
self.settings.createGroup({
'name': 'userscript',
- 'label': 'Install the bookmarklet' + (userscript ? ' or userscript' : ''),
+ 'label': 'Install the browser extension or bookmarklet',
'description': 'Easily add movies via imdb.com, appletrailers and more'
}).inject(self.settings.tabs.automation.content, 'top').adopt(
- (userscript ? [new Element('a.userscript.button', {
- 'text': 'Install userscript',
- 'href': Api.createUrl('userscript.get')+randomString()+'/couchpotato.user.js',
- 'target': '_self'
- }), new Element('span.or[text=or]')] : null),
- new Element('span.bookmarklet').adopt(
- new Element('a.button.green', {
- 'text': '+CouchPotato',
- 'href': "javascript:void((function(){var e=document.createElement('script');e.setAttribute('type','text/javascript');e.setAttribute('charset','UTF-8');e.setAttribute('src','" +
- host_url + Api.createUrl('userscript.bookmark') +
- "?host="+ encodeURI(host_url + Api.createUrl('userscript.get')+randomString()+'/') +
- "&r='+Math.random()*99999999);document.body.appendChild(e)})());",
- 'target': '',
- 'events': {
- 'click': function(e){
- (e).stop()
- alert('Drag it to your bookmark ;)')
- }
- }
+ new Element('div').adopt(
+ new Element('a.userscript.button', {
+ 'text': 'Install extension',
+ 'href': 'https://couchpota.to/extension/',
+ 'target': '_blank'
}),
- new Element('span', {
- 'text': 'Б┤╫ Drag this to your bookmarks'
- })
- )
- ).setStyles({
- 'background-image': "url('"+Api.createUrl('static/userscript/userscript.png')+"')"
- });
+ new Element('span.or[text=or]'),
+ new Element('span.bookmarklet').adopt(
+ new Element('a.button.green', {
+ 'text': '+CouchPotato',
+ /* jshint ignore:start */
+ 'href': "javascript:void((function(){var e=document.createElement('script');e.setAttribute('type','text/javascript');e.setAttribute('charset','UTF-8');e.setAttribute('src','" +
+ host_url + Api.createUrl('userscript.bookmark') +
+ "?host="+ encodeURI(host_url + Api.createUrl('userscript.get')+randomString()+'/') +
+ "&r='+Math.random()*99999999);document.body.appendChild(e)})());",
+ /* jshint ignore:end */
+ 'target': '',
+ 'events': {
+ 'click': function(e){
+ (e).stop();
+ alert('Drag it to your bookmark ;)');
+ }
+ }
+ }),
+ new Element('span', {
+ 'text': 'Б┤╫ Drag this to your bookmarks'
+ })
+ )
+ ),
+ new Element('img', {
+ 'src': 'https://couchpota.to/media/images/userscript.gif'
+ })
+ );
});
diff --git a/couchpotato/core/plugins/userscript/static/userscript.png b/couchpotato/core/plugins/userscript/static/userscript.png
deleted file mode 100644
index c8e7657783..0000000000
Binary files a/couchpotato/core/plugins/userscript/static/userscript.png and /dev/null differ
diff --git a/couchpotato/core/plugins/userscript/static/userscript.scss b/couchpotato/core/plugins/userscript/static/userscript.scss
new file mode 100644
index 0000000000..12f4fd2c66
--- /dev/null
+++ b/couchpotato/core/plugins/userscript/static/userscript.scss
@@ -0,0 +1,125 @@
+@import "_mixins";
+
+.page.userscript {
+ position: absolute;
+ width: 100%;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ padding: 0;
+
+ .frame.loading {
+ text-align: center;
+ font-size: 20px;
+ padding: 20px;
+ }
+
+ .media_result {
+ height: 140px;
+ display: flex;
+ }
+
+ .thumbnail {
+ width: 90px;
+ }
+
+ .options {
+ left: 90px;
+ display: flex;
+ align-items: center;
+ padding: $padding/2;
+
+ > div {
+ display: flex;
+ flex-wrap: wrap;
+
+ div {
+ flex: 1 auto;
+ margin: 0;
+ padding: 0 $padding/4;
+ }
+
+ .title {
+ min-width: 100%;
+ margin-bottom: $padding;
+ }
+
+ .add {
+ text-align: right;
+
+ a {
+ display: block;
+ text-align: center;
+ }
+ }
+
+ select {
+ width: 100%;
+ }
+ }
+ }
+
+ .message {
+ font-size: 1.5em;
+ }
+
+ .year,
+ .data {
+ display: none;
+ }
+
+}
+
+
+.group_userscript.group_userscript {
+ display: block;
+
+ .empty_wanted & {
+ padding: $padding 0;
+ }
+
+ .wgroup_automation & {
+ padding: $padding/2 0;
+ margin-left: 0;
+ }
+
+ h2 {
+ margin: 0 0 $padding/2;
+ }
+
+ .userscript {
+ margin-left: $padding;
+
+ @include media-tablet {
+ margin-left: $padding/2;
+ }
+
+ .wgroup_automation & {
+ margin-left: 0;
+ }
+ }
+
+ .bookmarklet {
+
+ span {
+ margin-left: 10px;
+ display: inline-block;
+ }
+ }
+
+ img {
+ clear: both;
+ margin: $padding;
+ width: 100%;
+ max-width: 600px;
+
+ @include media-tablet {
+ margin: $padding/2;
+ }
+
+ .wgroup_automation & {
+ margin-left: 0;
+ }
+ }
+}
diff --git a/couchpotato/core/plugins/userscript/template.js b/couchpotato/core/plugins/userscript/template.js
deleted file mode 100644
index c30fad5bab..0000000000
--- a/couchpotato/core/plugins/userscript/template.js
+++ /dev/null
@@ -1,138 +0,0 @@
-// ==UserScript==
-// @name CouchPotato UserScript
-// @description Add movies like a real CouchPotato
-// @grant none
-// @version {{version}}
-
-// @match {{host}}*
-{% for include in includes %}
-// @match {{include}}{% endfor %}
-{% for exclude in excludes %}
-// @exclude {{exclude}}{% endfor %}
-// @exclude {{host}}{{api.rstrip('/')}}*
-
-// ==/UserScript==
-
-if (window.top == window.self){ // Only run on top window
-
-var version = {{version}},
- host = '{{host}}',
- api = '{{api}}';
-
-function create() {
- switch (arguments.length) {
- case 1:
- var A = document.createTextNode(arguments[0]);
- break;
- default:
- var A = document.createElement(arguments[0]), B = arguments[1];
- for ( var b in B) {
- if (b.indexOf("on") == 0){
- A.addEventListener(b.substring(2), B[b], false);
- }
- else if (",style,accesskey,id,name,src,href,which".indexOf(","
- + b.toLowerCase()) != -1){
- A.setAttribute(b, B[b]);
- }
- else{
- A[b] = B[b];
- }
- }
- for ( var i = 2, len = arguments.length; i < len; ++i){
- A.appendChild(arguments[i]);
- }
- }
- return A;
-}
-
-var addStyle = function(css) {
- var head = document.getElementsByTagName('head')[0],
- style = document.createElement('style');
- if (!head)
- return;
-
- style.type = 'text/css';
- style.textContent = css;
- head.appendChild(style);
-}
-
-// Styles
-addStyle('\
- #cp_popup { font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; -moz-border-radius: 6px 0px 0px 6px; -webkit-border-radius: 6px 0px 0px 6px; border-radius: 6px 0px 0px 6px; -moz-box-shadow: 0 0 20px rgba(0,0,0,0.5); -webkit-box-shadow: 0 0 20px rgba(0,0,0,0.5); box-shadow: 0 0 20px rgba(0,0,0,0.5); position:fixed; z-index:9999; bottom:0; right:0; font-size:15px; margin: 20px 0; display: block; background:#4E5969; } \
- #cp_popup.opened { width: 492px; } \
- #cp_popup a#add_to { cursor:pointer; text-align:center; text-decoration:none; color: #000; display:block; padding:5px 0 5px 5px; } \
- #cp_popup a#close_button { cursor:pointer; float: right; padding:120px 10px 10px; } \
- #cp_popup a img { vertical-align: middle; } \
- #cp_popup a:hover { color:#000; } \
- #cp_popup iframe{ background:#4E5969; margin:6px 0 2px 6px; height:140px; width:450px; overflow:hidden; border:none; } \
-');
-
-var cp_icon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAADHmlDQ1BJQ0MgUHJvZmlsZQAAeAGFVN9r01AU/tplnbDhizpnEQk+aJFuZFN0Q5y2a1e6zVrqNrchSJumbVyaxiTtfrAH2YtvOsV38Qc++QcM2YNve5INxhRh+KyIIkz2IrOemzRNJ1MDufe73/nuOSfn5F6g+XFa0xQvDxRVU0/FwvzE5BTf8gFeHEMr/GhNi4YWSiZHQA/Tsnnvs/MOHsZsdO5v36v+Y9WalQwR8BwgvpQ1xCLhWaBpXNR0E+DWie+dMTXCzUxzWKcECR9nOG9jgeGMjSOWZjQ1QJoJwgfFQjpLuEA4mGng8w3YzoEU5CcmqZIuizyrRVIv5WRFsgz28B9zg/JfsKiU6Zut5xCNbZoZTtF8it4fOX1wjOYA1cE/Xxi9QbidcFg246M1fkLNJK4RJr3n7nRpmO1lmpdZKRIlHCS8YlSuM2xp5gsDiZrm0+30UJKwnzS/NDNZ8+PtUJUE6zHF9fZLRvS6vdfbkZMH4zU+pynWf0D+vff1corleZLw67QejdX0W5I6Vtvb5M2mI8PEd1E/A0hCgo4cZCjgkUIMYZpjxKr4TBYZIkqk0ml0VHmyONY7KJOW7RxHeMlfDrheFvVbsrj24Pue3SXXjrwVhcW3o9hR7bWB6bqyE5obf3VhpaNu4Te55ZsbbasLCFH+iuWxSF5lyk+CUdd1NuaQU5f8dQvPMpTuJXYSWAy6rPBe+CpsCk+FF8KXv9TIzt6tEcuAcSw+q55TzcbsJdJM0utkuL+K9ULGGPmQMUNanb4kTZyKOfLaUAsnBneC6+biXC/XB567zF3h+rkIrS5yI47CF/VFfCHwvjO+Pl+3b4hhp9u+02TrozFa67vTkbqisXqUj9sn9j2OqhMZsrG+sX5WCCu0omNqSrN0TwADJW1Ol/MFk+8RhAt8iK4tiY+rYleQTysKb5kMXpcMSa9I2S6wO4/tA7ZT1l3maV9zOfMqcOkb/cPrLjdVBl4ZwNFzLhegM3XkCbB8XizrFdsfPJ63gJE722OtPW1huos+VqvbdC5bHgG7D6vVn8+q1d3n5H8LeKP8BqkjCtbCoV8yAAAACXBIWXMAAAsTAAALEwEAmpwYAAABZGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNC40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iPgogICAgICAgICA8eG1wOkNyZWF0b3JUb29sPkFkb2JlIEltYWdlUmVhZHk8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Chvleg4AAAdrSURBVFgJzZfPb1VVEMfn3PfaUgotVIpi+a3BWETFRIkYEzTRtRv8BxSiCRp1JStkBXGhC42J7Ay4wGrcqHHhAjfEaNSoESOoVQIWBIGW/qB9797r93Pem8d9zwe4KJFJpnPOnDkz35k559zXkOe5XS8KFsKW3VbC/8CQ5cNbjWAxYG7/LXC4XgABdzUQIYREsbNrFee6AAy7FXxXnt25N6wWgBdL8+1oeaH9Uh6wP5Zutj8/6c/HrwXM18s+mEu5xSyRv8yCbe5cbM9nM2bphHjcqidO2OmN88KZ8iI79NUT9pI6nquaFKrtWbsuAC8uEzRRKNtawGWzNqPwnVKVbdYGBX0wndHfOjhsr0RkOue0cLR2EZKSrUu6ItBS0mFBnCdAFGn8ZQy81TiLbasX7aLRHP859IqluAwlu41aClTQmDkyqYP+DZst62vVZtyO5rzF8faqIptfD92h21YE9SgHINFrj0yIJzSxY+0AtermvsW7axWZWGTLdAaXxgrSXpUiqMXSlfKqxmYjgDmk97EVVHE+ZxX0R3n9UKzPrACs1vPSJTB6DmugGQlkyCp2LpmykxHIkasDnJN3sN2jvHF/2FZeYPuyaUtVolIdRqrLUapO2A/fPWX3cDmu9sSQQNsK7tYrPxS7UCy22XDzNM4Wb9ctfDuvbHotrKv22gMdK+2nLx63b8rdNiQwRgX9/Omu5sk8OZ6wkQjua9XzPcuGn5RFO+eK8K8KDuuebc3zeAvb4LmiSlWcv+Gg/T6vywYU9nRWtR69dAsaG2oXpFLqsY6p47b3++35zsbaVQZNAPeF0LE9zysHensfzi5VdiRZVglpzkUMmaXp16aGiXhxVZzYNWWSp7dYuetU3/ljz409evQx21jWJYg/EYJMhNxPmb68aqmFypiNJKkdXpLa1Noxs01vylxXhhz844ykSo0Wq63lXQJ3sK/v/uzixUNKPWGDX/M+jZeLz4oXidkIT4ovncL5mA2+EezMUJ5dWKUE9KHLefe0DvEUK8uQCWT3YltT7bA1I9I9/LNZ/0gNmKaRkDAgI0DOnMBVPxwYWJePj3+mxz/R13xG4EoEoFoYax6ffOY6SlFfkZRtmO3TcRsrl279qJKM75BSnhOyqyPUTxsTOOusWpjKLUunLXvhfcvXv6sEZeaAiAP7PALUHFfZ1NkLr/aY9SrgrBa6+CGHgQDHDZSc9mKsb79N1Zlv16xaNdNfsdLH3bbokWkb3yQ7FjAWkVmnspmQs65pS545YMkdH5hNL5T+4mVADo5T0mixbiyAlUleriddAgjJs6DvfQRKtYiJExwwJ3v5j1I/AOR01rrekf1dUirbmmfNFW18vtlNSuTpt8xWfqoEexVD1QAIcZCtXM9PKyIFIzbnO6eNDhJQgKy3M4JhbYl4pXiVuF+c6kBeWJra5A89VvpcxeNJkbMORZkU2JUXzLbtMVsmcJM6yPwqdED4bmWK4C3WMILQOY5d0UtR606rgzPS03KYzdgxBuiAePQvvmGTdnJP2Xoe1Ftzq0AL5OBxsyd2KukjZqcXa8/52n5AeYyiBAfzJoAoYq/rkhbDEFVknWrJf9zIGXUbWqGbb7eIN8hg9HzJDg9XbfRls/sE6qFndSz0BIxqLRE4AKiAjTPNfvflMZFNACkpzAISqlfURjWmpSpITKLojDEBnACwSodizX6zX5eb3SvZIXBV3iqtQfjniULFXpJFtnJbgBhTLYwBSPXk3+4We4UdYNFhK9BB2a/YUwOT6Rx0jl1ODv+6wNYtbufL/TYBrMiUM8EFABhZAohMaR+bWEeixzHSq4yesVPsgm5q7KVumSeHBCC+sGFfO1/omwBizCY2eyAkTBV5TnBOhZ08e5foGTu1+/+NdXySOL4AARjI/bhsAZhHgA4KCQEYwlk7gKwVQV1r7MEBhU+X6PHv6xrWgA8zEqVa8rJj6EAByFeDjJH8YqCiVBx2O/ZASOeoqM/xgz17YXzRFferd7jh07vYUsHL54KgBMEQ/lZ8Wsy7R9beGrKlAkXWNO5FOviixC+gRsRLxbyj7s/f32IMLdfIq+cSLZky56vlPxAIALHG2IOjc8DFgFw6QBQllXlQTPL4xxdfq6Jk3FRBAhVboGncQOlvFpMtALyKbPZMXaIrMnqvrp8tl1qK/ogLIYsJA74JYEU7q7IgI7KBPCNA8gsG5w7Aq+RzpOuKgAHooJBXIsA5+9FqAujlBhztgLz8rJEhARgXyZ2yjkOkA6Qj6LyKDlaqJsIH+2AHh2wCKGcBhw5Kw8YYPQxAB1R06qBw6uAAwxh/SAfn1ZQqUtEf+4tAmwCmiVUrshCQKq2FHBgSHc69Su6oVXrmgGINYOiKADVtIk8WWQQZAa6vFcWm0mo/H29l3IURYAjAmPcKY4IgCd4q0Tm7LXP8sK8IEJ1TsYKtyTb+q9M/0B2368euAK7Qc1LRGYw+2HBO/LeYn1lOHtAleh+7dF1xj4+vJInnxLgB0JU3mqQTDeK/ux/rlWso/+fBDV9BjssNTf8AmPnhttjsZCIAAAAASUVORK5CYII=';
-var close_img = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAABGdBTUEAALGOfPtRkwAAACBjSFJNAAB6JQAAgIMAAPn/AACA6QAAdTAAAOpgAAA6mAAAF2+SX8VGAAAA5ElEQVR42tRTQYoEIQwsl/2Bl3gQoY9eBKEf5kvyG8G7h4Z+S38gIu5lp5lZ2R7YPm1BDhZJSFWiGmPgDj5wE7cbfD4/mBkAHprUj9yTTyn9OsGIMSLG+Fxwxc8SiAi9d4QQHskjhIDeO4jorQcq5wwiQmsN3nt479FaAxEh5zxJmyZIKalSClprL1FKQUpJXZr4DBH52xqZeRhjICKw1sJaCxGBMQbMPN41GFpriAicc6i1otYK5xxEBFrraQuThGVZAADbtp2amXms6woAOI7j0gO17/t5MN+HNfEvBf//M30NAKe7aRqUOIlfAAAAAElFTkSuQmCC';
-
-var osd = function(){
- var navbar, newElement;
-
- var createApiUrl = function(url){
- return host + api + "?url=" + escape(url)
- };
-
- var iframe = create('iframe', {
- 'src': createApiUrl(document.location.href),
- 'frameborder': 0,
- 'scrolling': 'no'
- });
-
- var popup = create('div', {
- 'id': 'cp_popup'
- });
-
- var onclick = function(){
-
- // Try and get imdb url
- try {
- var regex = new RegExp(/tt(\d{7})/);
- var imdb_id = document.body.innerHTML.match(regex)[0];
- if (imdb_id)
- iframe.setAttribute('src', createApiUrl('http://imdb.com/title/'+imdb_id+'/'))
- }
- catch(e){}
-
- popup.innerHTML = '';
- popup.setAttribute('class', 'opened');
- popup.appendChild(create('a', {
- 'innerHTML': '',
- 'id': 'close_button',
- 'onclick': function(){
- popup.innerHTML = '';
- popup.appendChild(add_button);
- popup.setAttribute('class', '');
- }
- }));
- popup.appendChild(iframe)
- }
-
- var add_button = create('a', {
- 'innerHTML': '',
- 'id': 'add_to',
- 'onclick': onclick
- });
- popup.appendChild(add_button);
-
- document.body.parentNode.insertBefore(popup, document.body);
-
- // Auto fold open
- if(document.body.getAttribute('cp_auto_open'))
- onclick()
-};
-
-var setVersion = function(){
- document.body.setAttribute('data-userscript_version', version)
-};
-
-if(document.location.href.indexOf(host) == -1)
- osd();
-else
- setVersion();
-
-}
\ No newline at end of file
diff --git a/couchpotato/core/plugins/userscript/template.js_tmpl b/couchpotato/core/plugins/userscript/template.js_tmpl
new file mode 100644
index 0000000000..25e184202d
--- /dev/null
+++ b/couchpotato/core/plugins/userscript/template.js_tmpl
@@ -0,0 +1,147 @@
+// ==UserScript==
+//
+// If you can read this, you need to enable or install the Greasemonkey add-on for firefox
+// If you are using Chrome, download this file and drag it to the extensions tab
+// Other browsers, use the bookmarklet
+//
+// @name CouchPotato UserScript
+// @description Add movies like a real CouchPotato
+// @grant none
+// @version {{version}}
+
+// @match {{host}}/*
+{% for include in includes %}
+// @match {{include}}{% end %}
+{% for exclude in excludes %}
+// @exclude {{exclude}}{% end %}
+// @exclude {{host}}{{api.rstrip('/')}}*
+
+// ==/UserScript==
+
+{% autoescape None %}
+if (window.top == window.self){ // Only run on top window
+
+var version = {{version}},
+ host = '{{host}}',
+ api = '{{api}}';
+
+var create = function() {
+ var A, B;
+ switch (arguments.length) {
+ case 1:
+ A = document.createTextNode(arguments[0]);
+ break;
+ default:
+ A = document.createElement(arguments[0]);
+ B = arguments[1];
+
+ for ( var b in B) {
+ if (b.indexOf("on") == 0){
+ A.addEventListener(b.substring(2), B[b], false);
+ }
+ else if (",style,accesskey,id,name,src,href,which".indexOf(","
+ + b.toLowerCase()) != -1){
+ A.setAttribute(b, B[b]);
+ }
+ else{
+ A[b] = B[b];
+ }
+ }
+ for ( var i = 2, len = arguments.length; i < len; ++i){
+ A.appendChild(arguments[i]);
+ }
+ }
+ return A;
+}
+
+var addStyle = function(css) {
+ var head = document.getElementsByTagName('head')[0],
+ style = document.createElement('style');
+ if (!head)
+ return;
+
+ style.type = 'text/css';
+ style.textContent = css;
+ head.appendChild(style);
+}
+
+// Styles
+addStyle('\
+ #cp_popup { font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; -moz-border-radius: 6px 0px 0px 6px; -webkit-border-radius: 6px 0px 0px 6px; border-radius: 6px 0px 0px 6px; -moz-box-shadow: 0 0 20px rgba(0,0,0,0.5); -webkit-box-shadow: 0 0 20px rgba(0,0,0,0.5); box-shadow: 0 0 20px rgba(0,0,0,0.5); position:fixed; z-index:20000; bottom:0; right:0; font-size:15px; margin: 20px 0; display: block; background:#FFF; } \
+ #cp_popup.opened { width: 492px; } \
+ #cp_popup a#add_to { cursor:pointer; text-align:center; text-decoration:none; color: #000; display:block; padding:5px 0 5px 5px; } \
+ #cp_popup a#close_button { cursor:pointer; float: right; padding:120px 10px 10px; } \
+ #cp_popup a img { vertical-align: middle; } \
+ #cp_popup a:hover { color:#000; } \
+ #cp_popup iframe{ background:#FFF; margin:6px 0 2px 6px; height:140px; width:450px; overflow:hidden; border:none; } \
+');
+
+var cp_icon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAADHmlDQ1BJQ0MgUHJvZmlsZQAAeAGFVN9r01AU/tplnbDhizpnEQk+aJFuZFN0Q5y2a1e6zVrqNrchSJumbVyaxiTtfrAH2YtvOsV38Qc++QcM2YNve5INxhRh+KyIIkz2IrOemzRNJ1MDufe73/nuOSfn5F6g+XFa0xQvDxRVU0/FwvzE5BTf8gFeHEMr/GhNi4YWSiZHQA/Tsnnvs/MOHsZsdO5v36v+Y9WalQwR8BwgvpQ1xCLhWaBpXNR0E+DWie+dMTXCzUxzWKcECR9nOG9jgeGMjSOWZjQ1QJoJwgfFQjpLuEA4mGng8w3YzoEU5CcmqZIuizyrRVIv5WRFsgz28B9zg/JfsKiU6Zut5xCNbZoZTtF8it4fOX1wjOYA1cE/Xxi9QbidcFg246M1fkLNJK4RJr3n7nRpmO1lmpdZKRIlHCS8YlSuM2xp5gsDiZrm0+30UJKwnzS/NDNZ8+PtUJUE6zHF9fZLRvS6vdfbkZMH4zU+pynWf0D+vff1corleZLw67QejdX0W5I6Vtvb5M2mI8PEd1E/A0hCgo4cZCjgkUIMYZpjxKr4TBYZIkqk0ml0VHmyONY7KJOW7RxHeMlfDrheFvVbsrj24Pue3SXXjrwVhcW3o9hR7bWB6bqyE5obf3VhpaNu4Te55ZsbbasLCFH+iuWxSF5lyk+CUdd1NuaQU5f8dQvPMpTuJXYSWAy6rPBe+CpsCk+FF8KXv9TIzt6tEcuAcSw+q55TzcbsJdJM0utkuL+K9ULGGPmQMUNanb4kTZyKOfLaUAsnBneC6+biXC/XB567zF3h+rkIrS5yI47CF/VFfCHwvjO+Pl+3b4hhp9u+02TrozFa67vTkbqisXqUj9sn9j2OqhMZsrG+sX5WCCu0omNqSrN0TwADJW1Ol/MFk+8RhAt8iK4tiY+rYleQTysKb5kMXpcMSa9I2S6wO4/tA7ZT1l3maV9zOfMqcOkb/cPrLjdVBl4ZwNFzLhegM3XkCbB8XizrFdsfPJ63gJE722OtPW1huos+VqvbdC5bHgG7D6vVn8+q1d3n5H8LeKP8BqkjCtbCoV8yAAAACXBIWXMAAAsTAAALEwEAmpwYAAABZGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNC40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iPgogICAgICAgICA8eG1wOkNyZWF0b3JUb29sPkFkb2JlIEltYWdlUmVhZHk8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Chvleg4AAAdrSURBVFgJzZfPb1VVEMfn3PfaUgotVIpi+a3BWETFRIkYEzTRtRv8BxSiCRp1JStkBXGhC42J7Ay4wGrcqHHhAjfEaNSoESOoVQIWBIGW/qB9797r93Pem8d9zwe4KJFJpnPOnDkz35k559zXkOe5XS8KFsKW3VbC/8CQ5cNbjWAxYG7/LXC4XgABdzUQIYREsbNrFee6AAy7FXxXnt25N6wWgBdL8+1oeaH9Uh6wP5Zutj8/6c/HrwXM18s+mEu5xSyRv8yCbe5cbM9nM2bphHjcqidO2OmN88KZ8iI79NUT9pI6nquaFKrtWbsuAC8uEzRRKNtawGWzNqPwnVKVbdYGBX0wndHfOjhsr0RkOue0cLR2EZKSrUu6ItBS0mFBnCdAFGn8ZQy81TiLbasX7aLRHP859IqluAwlu41aClTQmDkyqYP+DZst62vVZtyO5rzF8faqIptfD92h21YE9SgHINFrj0yIJzSxY+0AtermvsW7axWZWGTLdAaXxgrSXpUiqMXSlfKqxmYjgDmk97EVVHE+ZxX0R3n9UKzPrACs1vPSJTB6DmugGQlkyCp2LpmykxHIkasDnJN3sN2jvHF/2FZeYPuyaUtVolIdRqrLUapO2A/fPWX3cDmu9sSQQNsK7tYrPxS7UCy22XDzNM4Wb9ctfDuvbHotrKv22gMdK+2nLx63b8rdNiQwRgX9/Omu5sk8OZ6wkQjua9XzPcuGn5RFO+eK8K8KDuuebc3zeAvb4LmiSlWcv+Gg/T6vywYU9nRWtR69dAsaG2oXpFLqsY6p47b3++35zsbaVQZNAPeF0LE9zysHensfzi5VdiRZVglpzkUMmaXp16aGiXhxVZzYNWWSp7dYuetU3/ljz409evQx21jWJYg/EYJMhNxPmb68aqmFypiNJKkdXpLa1Noxs01vylxXhhz844ykSo0Wq63lXQJ3sK/v/uzixUNKPWGDX/M+jZeLz4oXidkIT4ovncL5mA2+EezMUJ5dWKUE9KHLefe0DvEUK8uQCWT3YltT7bA1I9I9/LNZ/0gNmKaRkDAgI0DOnMBVPxwYWJePj3+mxz/R13xG4EoEoFoYax6ffOY6SlFfkZRtmO3TcRsrl279qJKM75BSnhOyqyPUTxsTOOusWpjKLUunLXvhfcvXv6sEZeaAiAP7PALUHFfZ1NkLr/aY9SrgrBa6+CGHgQDHDZSc9mKsb79N1Zlv16xaNdNfsdLH3bbokWkb3yQ7FjAWkVmnspmQs65pS545YMkdH5hNL5T+4mVADo5T0mixbiyAlUleriddAgjJs6DvfQRKtYiJExwwJ3v5j1I/AOR01rrekf1dUirbmmfNFW18vtlNSuTpt8xWfqoEexVD1QAIcZCtXM9PKyIFIzbnO6eNDhJQgKy3M4JhbYl4pXiVuF+c6kBeWJra5A89VvpcxeNJkbMORZkU2JUXzLbtMVsmcJM6yPwqdED4bmWK4C3WMILQOY5d0UtR606rgzPS03KYzdgxBuiAePQvvmGTdnJP2Xoe1Ftzq0AL5OBxsyd2KukjZqcXa8/52n5AeYyiBAfzJoAoYq/rkhbDEFVknWrJf9zIGXUbWqGbb7eIN8hg9HzJDg9XbfRls/sE6qFndSz0BIxqLRE4AKiAjTPNfvflMZFNACkpzAISqlfURjWmpSpITKLojDEBnACwSodizX6zX5eb3SvZIXBV3iqtQfjniULFXpJFtnJbgBhTLYwBSPXk3+4We4UdYNFhK9BB2a/YUwOT6Rx0jl1ODv+6wNYtbufL/TYBrMiUM8EFABhZAohMaR+bWEeixzHSq4yesVPsgm5q7KVumSeHBCC+sGFfO1/omwBizCY2eyAkTBV5TnBOhZ08e5foGTu1+/+NdXySOL4AARjI/bhsAZhHgA4KCQEYwlk7gKwVQV1r7MEBhU+X6PHv6xrWgA8zEqVa8rJj6EAByFeDjJH8YqCiVBx2O/ZASOeoqM/xgz17YXzRFferd7jh07vYUsHL54KgBMEQ/lZ8Wsy7R9beGrKlAkXWNO5FOviixC+gRsRLxbyj7s/f32IMLdfIq+cSLZky56vlPxAIALHG2IOjc8DFgFw6QBQllXlQTPL4xxdfq6Jk3FRBAhVboGncQOlvFpMtALyKbPZMXaIrMnqvrp8tl1qK/ogLIYsJA74JYEU7q7IgI7KBPCNA8gsG5w7Aq+RzpOuKgAHooJBXIsA5+9FqAujlBhztgLz8rJEhARgXyZ2yjkOkA6Qj6LyKDlaqJsIH+2AHh2wCKGcBhw5Kw8YYPQxAB1R06qBw6uAAwxh/SAfn1ZQqUtEf+4tAmwCmiVUrshCQKq2FHBgSHc69Su6oVXrmgGINYOiKADVtIk8WWQQZAa6vFcWm0mo/H29l3IURYAjAmPcKY4IgCd4q0Tm7LXP8sK8IEJ1TsYKtyTb+q9M/0B2368euAK7Qc1LRGYw+2HBO/LeYn1lOHtAleh+7dF1xj4+vJInnxLgB0JU3mqQTDeK/ux/rlWso/+fBDV9BjssNTf8AmPnhttjsZCIAAAAASUVORK5CYII=';
+var close_img = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAABGdBTUEAALGOfPtRkwAAACBjSFJNAAB6JQAAgIMAAPn/AACA6QAAdTAAAOpgAAA6mAAAF2+SX8VGAAAA5ElEQVR42tRTQYoEIQwsl/2Bl3gQoY9eBKEf5kvyG8G7h4Z+S38gIu5lp5lZ2R7YPm1BDhZJSFWiGmPgDj5wE7cbfD4/mBkAHprUj9yTTyn9OsGIMSLG+Fxwxc8SiAi9d4QQHskjhIDeO4jorQcq5wwiQmsN3nt479FaAxEh5zxJmyZIKalSClprL1FKQUpJXZr4DBH52xqZeRhjICKw1sJaCxGBMQbMPN41GFpriAicc6i1otYK5xxEBFrraQuThGVZAADbtp2amXms6woAOI7j0gO17/t5MN+HNfEvBf//M30NAKe7aRqUOIlfAAAAAElFTkSuQmCC';
+
+var osd = function(){
+ var navbar, newElement;
+
+ var createApiUrl = function(url){
+ return host + api + "?url=" + escape(url)
+ };
+
+ var iframe = create('iframe', {
+ 'src': createApiUrl(document.location.href),
+ 'frameborder': 0,
+ 'scrolling': 'no'
+ });
+
+ var popup = create('div', {
+ 'id': 'cp_popup'
+ });
+
+ var onclick = function(){
+
+ // Try and get imdb url
+ try {
+ var regex = new RegExp(/tt(\d{7})/);
+ var imdb_id = document.body.innerHTML.match(regex)[0];
+ if (imdb_id)
+ iframe.setAttribute('src', createApiUrl('http://imdb.com/title/'+imdb_id+'/'))
+ }
+ catch(e){}
+
+ popup.innerHTML = '';
+ popup.setAttribute('class', 'opened');
+ popup.appendChild(create('a', {
+ 'innerHTML': '',
+ 'id': 'close_button',
+ 'onclick': function(){
+ popup.innerHTML = '';
+ popup.appendChild(add_button);
+ popup.setAttribute('class', '');
+ }
+ }));
+ popup.appendChild(iframe)
+ }
+
+ var add_button = create('a', {
+ 'innerHTML': '',
+ 'id': 'add_to',
+ 'onclick': onclick
+ });
+ popup.appendChild(add_button);
+
+ document.body.parentNode.insertBefore(popup, document.body);
+
+ // Auto fold open
+ if(document.body.getAttribute('cp_auto_open'))
+ onclick()
+};
+
+var setVersion = function(){
+ document.body.setAttribute('data-userscript_version', version)
+};
+
+if(document.location.href.indexOf(host) == -1)
+ osd();
+else
+ setVersion();
+
+}
diff --git a/couchpotato/core/plugins/v1importer/__init__.py b/couchpotato/core/plugins/v1importer/__init__.py
deleted file mode 100644
index 40c1434b4c..0000000000
--- a/couchpotato/core/plugins/v1importer/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from .main import V1Importer
-
-def start():
- return V1Importer()
-
-config = []
diff --git a/couchpotato/core/plugins/v1importer/form.html b/couchpotato/core/plugins/v1importer/form.html
deleted file mode 100644
index e27d1c7228..0000000000
--- a/couchpotato/core/plugins/v1importer/form.html
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
- {% if message: %}
- {{ message }}
- {% else: %}
-
- {% endif %}
-
-
\ No newline at end of file
diff --git a/couchpotato/core/plugins/v1importer/main.py b/couchpotato/core/plugins/v1importer/main.py
deleted file mode 100644
index 08f8ba986f..0000000000
--- a/couchpotato/core/plugins/v1importer/main.py
+++ /dev/null
@@ -1,56 +0,0 @@
-from couchpotato.api import addApiView
-from couchpotato.core.event import fireEventAsync
-from couchpotato.core.helpers.variable import getImdb
-from couchpotato.core.logger import CPLog
-from couchpotato.core.plugins.base import Plugin
-from couchpotato.environment import Env
-from flask.globals import request
-from flask.helpers import url_for
-import os
-
-log = CPLog(__name__)
-
-
-class V1Importer(Plugin):
-
- def __init__(self):
- addApiView('v1.import', self.fromOld, methods = ['GET', 'POST'])
-
- def fromOld(self):
-
- if request.method != 'POST':
- return self.renderTemplate(__file__, 'form.html', url_for = url_for)
-
- file = request.files['old_db']
-
- uploaded_file = os.path.join(Env.get('cache_dir'), 'v1_database.db')
-
- if os.path.isfile(uploaded_file):
- os.remove(uploaded_file)
-
- file.save(uploaded_file)
-
- try:
- import sqlite3
- conn = sqlite3.connect(uploaded_file)
-
- wanted = []
-
- t = ('want',)
- cur = conn.execute('SELECT status, imdb FROM Movie WHERE status=?', t)
- for row in cur:
- status, imdb = row
- if getImdb(imdb):
- wanted.append(imdb)
- conn.close()
-
- wanted = set(wanted)
- for imdb in wanted:
- fireEventAsync('movie.add', {'identifier': imdb}, search_after = False)
-
- message = 'Successfully imported %s movie(s)' % len(wanted)
- except Exception, e:
- message = 'Failed: %s' % e
-
- return self.renderTemplate(__file__, 'form.html', url_for = url_for, message = message)
-
diff --git a/couchpotato/core/plugins/wizard/__init__.py b/couchpotato/core/plugins/wizard/__init__.py
index 78876470ee..7a272b44d0 100644
--- a/couchpotato/core/plugins/wizard/__init__.py
+++ b/couchpotato/core/plugins/wizard/__init__.py
@@ -1,6 +1,7 @@
from .main import Wizard
-def start():
+
+def autoload():
return Wizard()
config = [{
diff --git a/couchpotato/core/plugins/wizard/static/wizard.css b/couchpotato/core/plugins/wizard/static/wizard.css
deleted file mode 100644
index 8d50d9de08..0000000000
--- a/couchpotato/core/plugins/wizard/static/wizard.css
+++ /dev/null
@@ -1,88 +0,0 @@
-.page.wizard .uniForm {
- width: 80%;
- margin: 0 auto 30px;
-}
-
-.page.wizard h1 {
- padding: 10px 30px;
- margin: 0;
- display: block;
- font-size: 30px;
- margin-top: 80px;
-}
-
-.page.wizard .description {
- padding: 10px 30px;
- font-size: 18px;
- display: block;
-}
-
-.page.wizard .tab_wrapper {
- background: #5c697b;
- padding: 10px 0;
- font-size: 18px;
- position: fixed;
- top: 0;
- margin: 0;
- width: 100%;
- min-width: 960px;
- left: 0;
- z-index: 2;
- box-shadow: 0 0 50px rgba(0,0,0,0.55);
-}
-
- .page.wizard .tab_wrapper .tabs {
- text-align: center;
- padding: 0;
- margin: 0;
- display: block;
- }
-
- .page.wizard .tabs li {
- display: inline-block;
- }
- .page.wizard .tabs li a {
- padding: 20px 10px;
- }
-
- .page.wizard .tab_wrapper .pointer {
- border-right: 10px solid transparent;
- border-left: 10px solid transparent;
- border-top: 10px solid #5c697b;
- display: block;
- position: absolute;
- top: 44px;
- }
-
-.page.wizard .tab_content {
- margin: 20px 0 160px;
-}
-
-.page.wizard form > div {
- min-height: 300px;
-}
-.page.wizard .wgroup_finish {
- height: 300px;
-}
- .page.wizard .wgroup_finish h1 {
- text-align: center;
- }
- .page.wizard .wgroup_finish .wizard_support,
- .page.wizard .wgroup_finish .description {
- font-size: 25px;
- line-height: 120%;
- margin: 20px 0;
- text-align: center;
- }
-
- .page.wizard .button.green {
- padding: 20px;
- font-size: 25px;
- margin: 10px 30px 80px;
- display: block;
- text-align: center;
- }
-
-.page.wizard .tab_nzb_providers {
- margin: 20px 0 0 0;
-}
diff --git a/couchpotato/core/plugins/wizard/static/wizard.js b/couchpotato/core/plugins/wizard/static/wizard.js
index eb41cb591c..49080c741d 100644
--- a/couchpotato/core/plugins/wizard/static/wizard.js
+++ b/couchpotato/core/plugins/wizard/static/wizard.js
@@ -1,263 +1,200 @@
-Page.Wizard = new Class({
-
- Extends: Page.Settings,
-
- name: 'wizard',
- has_tab: false,
- wizard_only: true,
-
- headers: {
- 'welcome': {
- 'title': 'Welcome to the new CouchPotato',
- 'description': 'To get started, fill in each of the following settings as much as you can. Maybe first start with importing your movies from the previous CouchPotato',
- 'content': new Element('div', {
- 'styles': {
- 'margin': '0 0 0 30px'
- }
- }).adopt(
- new Element('div', {
- 'html': 'Select the data.db. It should be in your CouchPotato root directory.'
- }),
- self.import_iframe = new Element('iframe', {
- 'styles': {
- 'height': 40,
- 'width': 300,
- 'border': 0,
- 'overflow': 'hidden'
- }
- })
- ),
- 'event': function(){
- self.import_iframe.set('src', Api.createUrl('v1.import'))
- }
- },
- 'general': {
- 'title': 'General',
- 'description': 'If you want to access CP from outside your local network, you better secure it a bit with a username & password.'
- },
- 'downloaders': {
- 'title': 'What download apps are you using?',
- 'description': 'CP needs an external download app to work with. Choose one below. For more downloaders check settings after you have filled in the wizard. If your download app isn\'t in the list, use the default Blackhole.'
- },
- 'providers': {
- 'title': 'Are you registered at any of these sites?',
- 'description': 'CP uses these sites to search for movies. A few free are enabled by default, but it\'s always better to have a few more. Check settings for the full list of available providers.'
- },
- 'renamer': {
- 'title': 'Move & rename the movies after downloading?',
- 'description': 'The coolest part of CP is that it can move and organize your downloaded movies automagically. Check settings and you can even download trailers, subtitles and other data when it has finished downloading. It\'s awesome!'
- },
- 'automation': {
- 'title': 'Easily add movies to your wanted list!',
- 'description': 'You can easily add movies from your favorite movie site, like IMDB, Rotten Tomatoes, Apple Trailers and more. Just install the userscript or drag the bookmarklet to your browsers bookmarks.' +
- ' Once installed, just click the bookmarklet on a movie page and watch the magic happen ;)',
- 'content': function(){
- return App.createUserscriptButtons().setStyles({
- 'background-image': "url('"+Api.createUrl('static/userscript/userscript.png')+"')"
- })
- }
- },
- 'finish': {
- 'title': 'Finishing Up',
- 'description': 'Are you done? Did you fill in everything as much as possible?' +
- ' Be sure to check the settings to see what more CP can do!
' +
- '
After you\'ve used CP for a while, and you like it (which of course you will), consider supporting CP. Maybe even by writing some code. Or by getting a subscription at Usenet Server or Newshosting.
',
- 'content': new Element('div').adopt(
- new Element('a.button.green', {
- 'styles': {
- 'margin-top': 20
- },
- 'text': 'I\'m ready to start the awesomeness, wow this button is big and green!',
- 'events': {
- 'click': function(e){
- (e).preventDefault();
- Api.request('settings.save', {
- 'data': {
- 'section': 'core',
- 'name': 'show_wizard',
- 'value': 0
- },
- 'useSpinner': true,
- 'spinnerOptions': {
- 'target': self.el
- },
- 'onComplete': function(){
- window.location = App.createUrl();
- }
- });
- }
- }
- })
- )
- }
- },
- groups: ['welcome', 'general', 'downloaders', 'searcher', 'providers', 'renamer', 'automation', 'finish'],
-
- open: function(action, params){
- var self = this;
-
- if(!self.initialized){
- App.fireEvent('unload');
- App.getBlock('header').hide();
-
- self.parent(action, params);
-
- self.addEvent('create', function(){
- self.order();
- });
-
- self.initialized = true;
-
- self.scroll = new Fx.Scroll(document.body, {
- 'transition': 'quint:in:out'
- });
- }
- else
- (function(){
- var sc = self.el.getElement('.wgroup_'+action);
- self.scroll.start(0, sc.getCoordinates().top-80);
- }).delay(1)
- },
-
- order: function(){
- var self = this;
-
- var form = self.el.getElement('.uniForm');
- var tabs = self.el.getElement('.tabs');
-
- self.groups.each(function(group, nr){
-
- if(self.headers[group]){
- group_container = new Element('.wgroup_'+group, {
- 'styles': {
- 'opacity': 0.2
- },
- 'tween': {
- 'duration': 350
- }
- });
-
- if(self.headers[group].include){
- self.headers[group].include.each(function(inc){
- group_container.addClass('wgroup_'+inc);
- })
- }
-
- var content = self.headers[group].content
- group_container.adopt(
- new Element('h1', {
- 'text': self.headers[group].title
- }),
- self.headers[group].description ? new Element('span.description', {
- 'html': self.headers[group].description
- }) : null,
- content ? (typeOf(content) == 'function' ? content() : content) : null
- ).inject(form);
- }
-
- var tab_navigation = tabs.getElement('.t_'+group);
-
- if(!tab_navigation && self.headers[group] && self.headers[group].include){
- tab_navigation = []
- self.headers[group].include.each(function(inc){
- tab_navigation.include(tabs.getElement('.t_'+inc));
- })
- }
-
- if(tab_navigation && group_container){
- tabs.adopt(tab_navigation); // Tab navigation
-
- if(self.headers[group] && self.headers[group].include){
-
- self.headers[group].include.each(function(inc){
- self.el.getElement('.tab_'+inc).inject(group_container);
- })
-
- new Element('li.t_'+group).adopt(
- new Element('a', {
- 'href': App.createUrl('wizard/'+group),
- 'text': (self.headers[group].label || group).capitalize()
- })
- ).inject(tabs);
-
- }
- else
- self.el.getElement('.tab_'+group).inject(group_container); // Tab content
-
- if(tab_navigation.getElement && self.headers[group]){
- var a = tab_navigation.getElement('a');
- a.set('text', (self.headers[group].label || group).capitalize());
- var url_split = a.get('href').split('wizard')[1].split('/');
- if(url_split.length > 3)
- a.set('href', a.get('href').replace(url_split[url_split.length-3]+'/', ''));
-
- }
- }
- else {
- new Element('li.t_'+group).adopt(
- new Element('a', {
- 'href': App.createUrl('wizard/'+group),
- 'text': (self.headers[group].label || group).capitalize()
- })
- ).inject(tabs);
- }
-
- if(self.headers[group] && self.headers[group].event)
- self.headers[group].event.call()
- });
-
- // Remove toggle
- self.el.getElement('.advanced_toggle').destroy();
-
- // Hide retention
- self.el.getElement('.tab_searcher').hide();
- self.el.getElement('.t_searcher').hide();
-
- // Add pointer
- new Element('.tab_wrapper').wraps(tabs).adopt(
- self.pointer = new Element('.pointer', {
- 'tween': {
- 'transition': 'quint:in:out'
- }
- })
- );
-
- // Add nav
- var minimum = self.el.getSize().y-window.getSize().y;
- self.groups.each(function(group, nr){
-
- var g = self.el.getElement('.wgroup_'+group);
- if(!g || !g.isVisible()) return;
- var t = self.el.getElement('.t_'+group);
- if(!t) return;
-
- var func = function(){
- var ct = t.getCoordinates();
- self.pointer.tween('left', ct.left+(ct.width/2)-(self.pointer.getWidth()/2));
- g.tween('opacity', 1);
- }
-
- if(nr == 0)
- func();
-
-
- var ss = new ScrollSpy( {
- min: function(){
- var c = g.getCoordinates();
- var top = c.top-(window.getSize().y/2);
- return top > minimum ? minimum : top
- },
- max: function(){
- var c = g.getCoordinates();
- return c.top+(c.height/2)
- },
- onEnter: func,
- onLeave: function(){
- g.tween('opacity', 0.2)
- }
- });
- });
-
- }
-
-});
\ No newline at end of file
+Page.Wizard = new Class({
+
+ Extends: Page.Settings,
+
+ order: 70,
+ name: 'wizard',
+ current: 'welcome',
+ has_tab: false,
+ wizard_only: true,
+
+ headers: {
+ 'welcome': {
+ 'title': 'Welcome to the new CouchPotato',
+ 'description': 'To get started, fill in each of the following settings as much as you can.',
+ 'content': new Element('div', {
+ 'styles': {
+ 'margin': '0 0 0 30px'
+ }
+ })
+ },
+ 'general': {
+ 'title': 'General',
+ 'description': 'If you want to access CP from outside your local network, you better secure it a bit with a username & password.'
+ },
+ 'downloaders': {
+ 'title': 'What download apps are you using?',
+ 'description': 'CP needs an external download app to work with. Choose one below. For more downloaders check settings after you have filled in the wizard. If your download app isn\'t in the list, use the default Blackhole.'
+ },
+ 'searcher': {
+ 'label': 'Providers',
+ 'title': 'Are you registered at any of these sites?',
+ 'description': 'CP uses these sites to search for movies. A few free are enabled by default, but it\'s always better to have more.'
+ },
+ 'renamer': {
+ 'title': 'Move & rename the movies after downloading?',
+ 'description': 'The coolest part of CP is that it can move and organize your downloaded movies automagically. Check settings and you can even download trailers, subtitles and other data when it has finished downloading. It\'s awesome!'
+ },
+ 'automation': {
+ 'title': 'Easily add movies to your wanted list!',
+ 'description': 'You can easily add movies from your favorite movie site, like IMDB, Rotten Tomatoes, Apple Trailers and more. Just install the extension or drag the bookmarklet to your bookmarks.' +
+ ' Once installed, just click the bookmarklet on a movie page and watch the magic happen ;)',
+ 'content': function(){
+ return App.createUserscriptButtons();
+ }
+ },
+ 'finish': {
+ 'title': 'Finishing Up',
+ 'description': 'Are you done? Did you fill in everything as much as possible?' +
+ ' Be sure to check the settings to see what more CP can do!
' +
+ '
After you\'ve used CP for a while, and you like it (which of course you will), consider supporting CP. Maybe even by writing some code. Or by getting a subscription at Usenet Server or Newshosting.
"
+ }).inject(self.message_container, "top");
+ requestTimeout(function() {
+ new_message.addClass("show");
+ }, 10);
+ var hide_message = function() {
+ new_message.addClass("hide");
+ requestTimeout(function() {
+ new_message.destroy();
+ }, 1e3);
+ };
+ if (sticky) new_message.grab(new Element("a.close.icon2", {
+ events: {
+ click: function() {
+ self.markAsRead([ data._id ]);
+ hide_message();
+ }
+ }
+ })); else requestTimeout(hide_message, 4e3);
+ },
+ addTestButtons: function() {
+ var self = this;
+ var setting_page = App.getPage("Settings");
+ setting_page.addEvent("create", function() {
+ Object.each(setting_page.tabs.notifications.groups, self.addTestButton.bind(self));
+ });
+ },
+ addTestButton: function(fieldset, plugin_name) {
+ var self = this, button_name = self.testButtonName(fieldset);
+ if (button_name.contains("Notifications")) return;
+ new Element(".ctrlHolder.test_button").grab(new Element("a.button", {
+ text: button_name,
+ events: {
+ click: function() {
+ var button = fieldset.getElement(".test_button .button");
+ button.set("text", "Sending notification");
+ Api.request("notify." + plugin_name + ".test", {
+ onComplete: function(json) {
+ button.set("text", button_name);
+ var message;
+ if (json.success) {
+ message = new Element("span.success", {
+ text: "Notification successful"
+ }).inject(button, "after");
+ } else {
+ message = new Element("span.failed", {
+ text: "Notification failed. Check logs for details."
+ }).inject(button, "after");
+ }
+ requestTimeout(function() {
+ message.destroy();
+ }, 3e3);
+ }
+ });
+ }
+ }
+ })).inject(fieldset);
+ },
+ testButtonName: function(fieldset) {
+ var name = fieldset.getElement("h2 .group_label").get("text");
+ return "Test " + name;
+ }
+});
+
+window.Notification = new NotificationBase();
+
+var TwitterNotification = new Class({
+ initialize: function() {
+ var self = this;
+ App.addEvent("loadSettings", self.addRegisterButton.bind(self));
+ },
+ addRegisterButton: function() {
+ var self = this;
+ var setting_page = App.getPage("Settings");
+ setting_page.addEvent("create", function() {
+ var fieldset = setting_page.tabs.notifications.groups.twitter, l = window.location;
+ var twitter_set = 0;
+ fieldset.getElements("input[type=text]").each(function(el) {
+ twitter_set += +(el.get("value") !== "");
+ });
+ new Element(".ctrlHolder").adopt(twitter_set > 0 ? [ self.unregister = new Element("a.button.red", {
+ text: 'Unregister "' + fieldset.getElement("input[name*=screen_name]").get("value") + '"',
+ events: {
+ click: function() {
+ fieldset.getElements("input[type=text]").set("value", "").fireEvent("change");
+ self.unregister.destroy();
+ self.unregister_or.destroy();
+ }
+ }
+ }), self.unregister_or = new Element("span[text=or]") ] : null, new Element("a.button", {
+ text: twitter_set > 0 ? "Register a different account" : "Register your Twitter account",
+ events: {
+ click: function() {
+ Api.request("notify.twitter.auth_url", {
+ data: {
+ host: l.protocol + "//" + l.hostname + (l.port ? ":" + l.port : "")
+ },
+ onComplete: function(json) {
+ window.location = json.url;
+ }
+ });
+ }
+ }
+ })).inject(fieldset.getElement(".test_button"), "before");
+ });
+ }
+});
+
+window.addEvent("domready", function() {
+ new TwitterNotification();
+});
+
+var CategoryListBase = new Class({
+ initialize: function() {
+ var self = this;
+ App.addEvent("loadSettings", self.addSettings.bind(self));
+ },
+ setup: function(categories) {
+ var self = this;
+ self.categories = [];
+ Array.each(categories, self.createCategory.bind(self));
+ },
+ addSettings: function() {
+ var self = this;
+ self.settings = App.getPage("Settings");
+ self.settings.addEvent("create", function() {
+ var tab = self.settings.createSubTab("category", {
+ label: "Categories",
+ name: "category",
+ subtab_label: "Category & filtering"
+ }, self.settings.tabs.searcher, "searcher");
+ self.tab = tab.tab;
+ self.content = tab.content;
+ self.createList();
+ self.createOrdering();
+ });
+ self.settings.addEvent("create", function() {
+ var renamer_group = self.settings.tabs.renamer.groups.renamer;
+ self.categories.each(function(category) {
+ var input = new Option.Directory("section_name", "option.name", category.get("destination"), {
+ name: category.get("label")
+ });
+ input.inject(renamer_group.getElement(".renamer_to"));
+ input.fireEvent("injected");
+ input.save = function() {
+ category.data.destination = input.getValue();
+ category.save();
+ };
+ });
+ });
+ },
+ createList: function() {
+ var self = this;
+ var count = self.categories.length;
+ self.settings.createGroup({
+ label: "Categories",
+ description: "Create categories, each one extending global filters. (Needs refresh '" + (App.isMac() ? "CMD+R" : "F5") + "' after editing)"
+ }).inject(self.content).adopt(self.category_container = new Element("div.container"), new Element("a.add_new_category", {
+ text: count > 0 ? "Create another category" : "Click here to create a category.",
+ events: {
+ click: function() {
+ var category = self.createCategory();
+ $(category).inject(self.category_container);
+ }
+ }
+ }));
+ Array.each(self.categories, function(category) {
+ $(category).inject(self.category_container);
+ });
+ },
+ getCategory: function(id) {
+ return this.categories.filter(function(category) {
+ return category.data._id == id;
+ }).pick();
+ },
+ getAll: function() {
+ return this.categories;
+ },
+ createCategory: function(data) {
+ var self = this;
+ data = data || {
+ id: randomString()
+ };
+ var category = new Category(data);
+ self.categories.include(category);
+ return category;
+ },
+ createOrdering: function() {
+ var self = this;
+ var category_list;
+ self.settings.createGroup({
+ label: "Category ordering"
+ }).adopt(new Element(".ctrlHolder#category_ordering").adopt(new Element("label[text=Order]"), category_list = new Element("ul"), new Element("p.formHint", {
+ html: "Change the order the categories are in the dropdown list."
+ }))).inject(self.content);
+ Array.each(self.categories, function(category) {
+ new Element("li", {
+ "data-id": category.data._id
+ }).adopt(new Element("span.category_label", {
+ text: category.data.label
+ }), new Element("span.handle.icon-handle")).inject(category_list);
+ });
+ self.category_sortable = new Sortables(category_list, {
+ revert: true,
+ handle: "",
+ opacity: .5,
+ onComplete: self.saveOrdering.bind(self)
+ });
+ },
+ saveOrdering: function() {
+ var self = this;
+ var ids = [];
+ self.category_sortable.list.getElements("li").each(function(el) {
+ ids.include(el.get("data-id"));
+ });
+ Api.request("category.save_order", {
+ data: {
+ ids: ids
+ }
+ });
+ }
+});
+
+window.CategoryList = new CategoryListBase();
+
+var Category = new Class({
+ data: {},
+ initialize: function(data) {
+ var self = this;
+ self.data = data;
+ self.create();
+ self.el.addEvents({
+ "change:relay(select)": self.save.bind(self, 0),
+ "keyup:relay(input[type=text])": self.save.bind(self, [ 300 ])
+ });
+ },
+ create: function() {
+ var self = this;
+ var data = self.data;
+ self.el = new Element("div.category").adopt(self.delete_button = new Element("span.delete.icon-delete", {
+ events: {
+ click: self.del.bind(self)
+ }
+ }), new Element(".category_label.ctrlHolder").adopt(new Element("label", {
+ text: "Name"
+ }), new Element("input", {
+ type: "text",
+ value: data.label,
+ placeholder: "Example: Kids, Horror or His"
+ }), new Element("p.formHint", {
+ text: "See global filters for explanation."
+ })), new Element(".category_preferred.ctrlHolder").adopt(new Element("label", {
+ text: "Preferred"
+ }), new Element("input", {
+ type: "text",
+ value: data.preferred,
+ placeholder: "Blu-ray, DTS"
+ })), new Element(".category_required.ctrlHolder").adopt(new Element("label", {
+ text: "Required"
+ }), new Element("input", {
+ type: "text",
+ value: data.required,
+ placeholder: "Example: DTS, AC3 & English"
+ })), new Element(".category_ignored.ctrlHolder").adopt(new Element("label", {
+ text: "Ignored"
+ }), new Element("input", {
+ type: "text",
+ value: data.ignored,
+ placeholder: "Example: dubbed, swesub, french"
+ })));
+ self.makeSortable();
+ },
+ save: function(delay) {
+ var self = this;
+ if (self.save_timer) clearRequestTimeout(self.save_timer);
+ self.save_timer = requestTimeout(function() {
+ Api.request("category.save", {
+ data: self.getData(),
+ useSpinner: true,
+ spinnerOptions: {
+ target: self.el
+ },
+ onComplete: function(json) {
+ if (json.success) {
+ self.data = json.category;
+ }
+ }
+ });
+ }, delay || 0);
+ },
+ getData: function() {
+ var self = this;
+ return {
+ id: self.data._id,
+ label: self.el.getElement(".category_label input").get("value"),
+ required: self.el.getElement(".category_required input").get("value"),
+ preferred: self.el.getElement(".category_preferred input").get("value"),
+ ignored: self.el.getElement(".category_ignored input").get("value"),
+ destination: self.data.destination
+ };
+ },
+ del: function() {
+ var self = this;
+ if (self.data.label === undefined) {
+ self.el.destroy();
+ return;
+ }
+ var label = self.el.getElement(".category_label input").get("value");
+ var qObj = new Question('Are you sure you want to delete "' + label + '"?', "", [ {
+ text: 'Delete "' + label + '"',
+ class: "delete",
+ events: {
+ click: function(e) {
+ e.preventDefault();
+ Api.request("category.delete", {
+ data: {
+ id: self.data._id
+ },
+ useSpinner: true,
+ spinnerOptions: {
+ target: self.el
+ },
+ onComplete: function(json) {
+ if (json.success) {
+ qObj.close();
+ self.el.destroy();
+ } else {
+ alert(json.message);
+ }
+ }
+ });
+ }
+ }
+ }, {
+ text: "Cancel",
+ cancel: true
+ } ]);
+ },
+ makeSortable: function() {
+ var self = this;
+ self.sortable = new Sortables(self.category_container, {
+ revert: true,
+ handle: ".handle",
+ opacity: .5,
+ onComplete: self.save.bind(self, 300)
+ });
+ },
+ get: function(attr) {
+ return this.data[attr];
+ },
+ toElement: function() {
+ return this.el;
+ }
+});
+
+Page.Log = new Class({
+ Extends: PageBase,
+ order: 60,
+ name: "log",
+ title: "Show recent logs.",
+ has_tab: false,
+ navigation: null,
+ log_items: [],
+ report_text: "### Steps to reproduce:\n" + "1. ..\n" + "2. ..\n" + "\n" + "### Information:\n" + "Movie(s) I have this with: ...\n" + "Quality of the movie being searched: ...\n" + "Providers I use: ...\n" + "Version of CouchPotato: {version}\n" + "Running on: ...\n" + "\n" + "### Logs:\n" + "```\n{issue}```",
+ indexAction: function() {
+ var self = this;
+ self.getLogs(0);
+ },
+ getLogs: function(nr) {
+ var self = this;
+ if (self.log) self.log.destroy();
+ self.log = new Element("div.container.loading", {
+ text: "loading...",
+ events: {
+ "mouseup:relay(.time)": function(e) {
+ requestTimeout(function() {
+ self.showSelectionButton(e);
+ }, 100);
+ }
+ }
+ }).inject(self.content);
+ if (self.navigation) {
+ var nav = self.navigation.getElement(".nav");
+ nav.getElements(".active").removeClass("active");
+ self.navigation.getElements("li")[nr + 1].addClass("active");
+ }
+ if (self.request && self.request.running) self.request.cancel();
+ self.request = Api.request("logging.get", {
+ data: {
+ nr: nr
+ },
+ onComplete: function(json) {
+ self.log.set("text", "");
+ self.log_items = self.createLogElements(json.log);
+ self.log.adopt(self.log_items);
+ self.log.removeClass("loading");
+ self.scrollToBottom();
+ if (!self.navigation) {
+ self.navigation = new Element("div.navigation").adopt(new Element("h2[text=Logs]"), new Element("div.hint", {
+ text: "Select multiple lines & report an issue"
+ }));
+ var nav = new Element("ul.nav", {
+ events: {
+ "click:relay(li.select)": function(e, el) {
+ self.getLogs(parseInt(el.get("text")) - 1);
+ }
+ }
+ }).inject(self.navigation);
+ new Element("li.filter").grab(new Element("select", {
+ events: {
+ change: function() {
+ var type_filter = this.getSelected()[0].get("value");
+ self.content.set("data-filter", type_filter);
+ self.scrollToBottom();
+ }
+ }
+ }).adopt(new Element("option", {
+ value: "ALL",
+ text: "Show all logs"
+ }), new Element("option", {
+ value: "INFO",
+ text: "Show only INFO"
+ }), new Element("option", {
+ value: "DEBUG",
+ text: "Show only DEBUG"
+ }), new Element("option", {
+ value: "ERROR",
+ text: "Show only ERROR"
+ }))).inject(nav);
+ for (var i = 0; i <= json.total; i++) {
+ new Element("li", {
+ text: i + 1,
+ class: "select " + (nr == i ? "active" : "")
+ }).inject(nav);
+ }
+ new Element("li.clear", {
+ text: "clear",
+ events: {
+ click: function() {
+ Api.request("logging.clear", {
+ onComplete: function() {
+ self.getLogs(0);
+ }
+ });
+ }
+ }
+ }).inject(nav);
+ self.navigation.inject(self.content, "top");
+ }
+ }
+ });
+ },
+ createLogElements: function(logs) {
+ var elements = [];
+ logs.each(function(log) {
+ elements.include(new Element("div", {
+ class: "time " + log.type.toLowerCase()
+ }).adopt(new Element("span", {
+ text: log.time
+ }), new Element("span.type", {
+ text: log.type
+ }), new Element("span.message", {
+ text: log.message
+ })));
+ });
+ return elements;
+ },
+ scrollToBottom: function() {
+ new Fx.Scroll(this.content, {
+ duration: 0
+ }).toBottom();
+ },
+ showSelectionButton: function(e) {
+ var self = this, selection = self.getSelected(), start_node = selection.anchorNode, parent_start = start_node.parentNode.getParent(".time"), end_node = selection.focusNode.parentNode.getParent(".time"), text = "";
+ var remove_button = function() {
+ self.log.getElements(".highlight").removeClass("highlight");
+ if (self.do_report) self.do_report.destroy();
+ document.body.removeEvent("click", remove_button);
+ };
+ remove_button();
+ if (parent_start) start_node = parent_start;
+ var index = {
+ start: self.log_items.indexOf(start_node),
+ end: self.log_items.indexOf(end_node)
+ };
+ if (index.start > index.end) {
+ index = {
+ start: index.end,
+ end: index.start
+ };
+ }
+ var nodes = self.log_items.slice(index.start, index.end + 1);
+ nodes.each(function(node, nr) {
+ node.addClass("highlight");
+ node.getElements("span").each(function(span) {
+ text += self.spaceFill(span.get("text") + " ", 6);
+ });
+ text += "\n";
+ });
+ self.do_report = new Element("a.do_report.button", {
+ text: "Report issue",
+ styles: {
+ top: e.page.y,
+ left: e.page.x
+ },
+ events: {
+ click: function(e) {
+ e.stop();
+ self.showReport(text);
+ }
+ }
+ }).inject(document.body);
+ requestTimeout(function() {
+ document.body.addEvent("click", remove_button);
+ }, 0);
+ },
+ showReport: function(text) {
+ var self = this, version = Updater.getInfo(), body = self.report_text.replace("{issue}", text).replace("{version}", version ? version.version.repr : "..."), textarea;
+ var overlay = new Element("div.mask.report_popup", {
+ method: "post",
+ events: {
+ click: function(e) {
+ overlay.destroy();
+ }
+ }
+ }).grab(new Element("div.bug", {
+ events: {
+ click: function(e) {
+ e.stopPropagation();
+ }
+ }
+ }).adopt(new Element("h1", {
+ text: "Report a bug"
+ }), new Element("span").adopt(new Element("span", {
+ text: "Read "
+ }), new Element("a.button", {
+ target: "_blank",
+ text: "the contributing guide",
+ href: "https://github.com/RuudBurger/CouchPotatoServer/blob/develop/contributing.md"
+ }), new Element("span", {
+ html: " before posting, then copy the text below and FILL IN the dots."
+ })), textarea = new Element("textarea", {
+ text: body
+ }), new Element("a.button", {
+ target: "_blank",
+ text: "Create a new issue on GitHub with the text above",
+ href: "https://github.com/RuudBurger/CouchPotatoServer/issues/new",
+ events: {
+ click: function(e) {
+ e.stop();
+ var body = textarea.get("value"), bdy = "?body=" + (body.length < 2e3 ? encodeURIComponent(body) : "Paste the text here"), win = window.open(e.target.get("href") + bdy, "_blank");
+ win.focus();
+ }
+ }
+ })));
+ overlay.inject(document.body);
+ },
+ getSelected: function() {
+ if (window.getSelection) return window.getSelection(); else if (document.getSelection) return document.getSelection(); else {
+ var selection = document.selection && document.selection.createRange();
+ if (selection.text) return selection.text;
+ }
+ return false;
+ },
+ spaceFill: function(number, width) {
+ if (number.toString().length >= width) return number;
+ return (new Array(width).join(" ") + number.toString()).substr(-width);
+ }
+});
+
+var Profile = new Class({
+ data: {},
+ types: [],
+ initialize: function(data) {
+ var self = this;
+ self.data = data;
+ self.types = [];
+ self.create();
+ self.el.addEvents({
+ "change:relay(select, input[type=checkbox])": self.save.bind(self, 0),
+ "keyup:relay(input[type=text])": self.save.bind(self, [ 300 ])
+ });
+ },
+ create: function() {
+ var self = this;
+ var data = self.data;
+ self.el = new Element("div.profile").adopt(self.delete_button = new Element("span.delete.icon-delete", {
+ events: {
+ click: self.del.bind(self)
+ }
+ }), new Element(".quality_label.ctrlHolder").adopt(new Element("label", {
+ text: "Name"
+ }), new Element("input", {
+ type: "text",
+ value: data.label,
+ placeholder: "Profile name"
+ })), new Element("div.qualities.ctrlHolder").adopt(new Element("label", {
+ text: "Search for"
+ }), self.type_container = new Element("ol.types"), new Element("div.formHint", {
+ html: "Search these qualities (2 minimum), from top to bottom. Use the checkbox, to stop searching after it found this quality."
+ })), new Element("div.wait_for.ctrlHolder").adopt(new Element("span", {
+ text: "Wait"
+ }), new Element("input.wait_for_input.xsmall", {
+ type: "text",
+ value: data.wait_for && data.wait_for.length > 0 ? data.wait_for[0] : 0
+ }), new Element("span", {
+ text: "day(s) for a better quality "
+ }), new Element("span.advanced", {
+ text: "and keep searching"
+ }), new Element("input.xsmall.stop_after_input.advanced", {
+ type: "text",
+ value: data.stop_after && data.stop_after.length > 0 ? data.stop_after[0] : 0
+ }), new Element("span.advanced", {
+ text: "day(s) for a better (checked) quality."
+ }), new Element("span.advanced", {
+ html: " Releases need a minimum score of"
+ }), new Element("input.advanced.xsmall.minimum_score_input", {
+ size: 4,
+ type: "text",
+ value: data.minimum_score || 1
+ })));
+ self.makeSortable();
+ if (data.qualities) {
+ data.types = [];
+ data.qualities.each(function(quality, nr) {
+ data.types.include({
+ quality: quality,
+ finish: data.finish[nr] || false,
+ "3d": data["3d"] ? data["3d"][nr] || false : false
+ });
+ });
+ }
+ if (data.types) data.types.each(self.addType.bind(self)); else self.delete_button.hide();
+ self.addType();
+ },
+ save: function(delay) {
+ var self = this;
+ if (self.save_timer) clearRequestTimeout(self.save_timer);
+ self.save_timer = requestTimeout(function() {
+ self.addType();
+ var data = self.getData();
+ if (data.types.length < 2) return; else self.delete_button.show();
+ Api.request("profile.save", {
+ data: self.getData(),
+ useSpinner: true,
+ spinnerOptions: {
+ target: self.el
+ },
+ onComplete: function(json) {
+ if (json.success) {
+ self.data = json.profile;
+ self.type_container.getElement("li:first-child input.finish[type=checkbox]").set("checked", true).getParent().addClass("checked");
+ }
+ }
+ });
+ }, delay);
+ },
+ getData: function() {
+ var self = this;
+ var data = {
+ id: self.data._id,
+ label: self.el.getElement(".quality_label input").get("value"),
+ wait_for: self.el.getElement(".wait_for_input").get("value"),
+ stop_after: self.el.getElement(".stop_after_input").get("value"),
+ minimum_score: self.el.getElement(".minimum_score_input").get("value"),
+ types: []
+ };
+ Array.each(self.type_container.getElements(".type"), function(type) {
+ if (!type.hasClass("deleted") && type.getElement("select").get("value") != -1) data.types.include({
+ quality: type.getElement("select").get("value"),
+ finish: +type.getElement("input.finish[type=checkbox]").checked,
+ "3d": +type.getElement("input.3d[type=checkbox]").checked
+ });
+ });
+ return data;
+ },
+ addType: function(data) {
+ var self = this;
+ var has_empty = false;
+ self.types.each(function(type) {
+ if ($(type).hasClass("is_empty")) has_empty = true;
+ });
+ if (has_empty) return;
+ var t = new Profile.Type(data, {
+ onChange: self.save.bind(self, 0)
+ });
+ $(t).inject(self.type_container);
+ self.sortable.addItems($(t));
+ self.types.include(t);
+ },
+ getTypes: function() {
+ var self = this;
+ return self.types.filter(function(type) {
+ return type.get("quality");
+ });
+ },
+ del: function() {
+ var self = this;
+ var label = self.el.getElement(".quality_label input").get("value");
+ var qObj = new Question('Are you sure you want to delete "' + label + '"?', "Items using this profile, will be set to the default quality.", [ {
+ text: 'Delete "' + label + '"',
+ class: "delete",
+ events: {
+ click: function(e) {
+ e.preventDefault();
+ Api.request("profile.delete", {
+ data: {
+ id: self.data._id
+ },
+ useSpinner: true,
+ spinnerOptions: {
+ target: self.el
+ },
+ onComplete: function(json) {
+ if (json.success) {
+ qObj.close();
+ self.el.destroy();
+ } else {
+ alert(json.message);
+ }
+ }
+ });
+ }
+ }
+ }, {
+ text: "Cancel",
+ cancel: true
+ } ]);
+ },
+ makeSortable: function() {
+ var self = this;
+ self.sortable = new Sortables(self.type_container, {
+ revert: true,
+ handle: ".handle",
+ opacity: .5,
+ onComplete: self.save.bind(self, 300)
+ });
+ },
+ get: function(attr) {
+ return this.data[attr];
+ },
+ isCore: function() {
+ return this.data.core;
+ },
+ toElement: function() {
+ return this.el;
+ }
+});
+
+Profile.Type = new Class({
+ Implements: [ Events, Options ],
+ deleted: false,
+ initialize: function(data, options) {
+ var self = this;
+ self.setOptions(options);
+ self.data = data || {};
+ self.create();
+ self.addEvent("change", function() {
+ self.el[self.qualities.get("value") == "-1" ? "addClass" : "removeClass"]("is_empty");
+ self.el[Quality.getQuality(self.qualities.get("value")).allow_3d ? "addClass" : "removeClass"]("allow_3d");
+ self.deleted = self.qualities.get("value") == "-1";
+ });
+ },
+ create: function() {
+ var self = this;
+ var data = self.data;
+ self.el = new Element("li.type").adopt(new Element("span.quality_type.select_wrapper.icon-dropdown").grab(self.fillQualities()), self.finish_container = new Element("label.finish").adopt(self.finish = new Element("input.finish[type=checkbox]", {
+ checked: data.finish !== undefined ? data.finish : 1,
+ events: {
+ change: function() {
+ if (self.el == self.el.getParent().getElement(":first-child")) {
+ alert("Top quality always finishes the search");
+ return;
+ }
+ self.fireEvent("change");
+ }
+ }
+ }), new Element("span.check_label[text=finish]")), self["3d_container"] = new Element("label.threed").adopt(self["3d"] = new Element("input.3d[type=checkbox]", {
+ checked: data["3d"] !== undefined ? data["3d"] : 0,
+ events: {
+ change: function() {
+ self.fireEvent("change");
+ }
+ }
+ }), new Element("span.check_label[text=3D]")), new Element("span.delete.icon-cancel", {
+ events: {
+ click: self.del.bind(self)
+ }
+ }), new Element("span.handle.icon-handle"));
+ self.el[self.data.quality ? "removeClass" : "addClass"]("is_empty");
+ if (self.data.quality && Quality.getQuality(self.data.quality).allow_3d) self.el.addClass("allow_3d");
+ },
+ fillQualities: function() {
+ var self = this;
+ self.qualities = new Element("select", {
+ events: {
+ change: self.fireEvent.bind(self, "change")
+ }
+ }).grab(new Element("option", {
+ text: "+ Add another quality",
+ value: -1
+ }));
+ Object.each(Quality.qualities, function(q) {
+ new Element("option", {
+ text: q.label,
+ value: q.identifier,
+ "data-allow_3d": q.allow_3d
+ }).inject(self.qualities);
+ });
+ self.qualities.set("value", self.data.quality);
+ return self.qualities;
+ },
+ getData: function() {
+ var self = this;
+ return {
+ quality: self.qualities.get("value"),
+ finish: +self.finish.checked,
+ "3d": +self["3d"].checked
+ };
+ },
+ get: function(key) {
+ return this.data[key];
+ },
+ del: function() {
+ var self = this;
+ self.el.addClass("deleted");
+ self.el.hide();
+ self.deleted = true;
+ self.fireEvent("change");
+ },
+ toElement: function() {
+ return this.el;
+ }
+});
+
+var QualityBase = new Class({
+ tab: "",
+ content: "",
+ setup: function(data) {
+ var self = this;
+ self.qualities = data.qualities;
+ self.profiles_list = null;
+ self.profiles = [];
+ Array.each(data.profiles, self.createProfilesClass.bind(self));
+ App.addEvent("loadSettings", self.addSettings.bind(self));
+ },
+ getProfile: function(id) {
+ return this.profiles.filter(function(profile) {
+ return profile.data._id == id;
+ }).pick();
+ },
+ getActiveProfiles: function() {
+ return Array.filter(this.profiles, function(profile) {
+ return !profile.data.hide;
+ });
+ },
+ getQuality: function(identifier) {
+ try {
+ return this.qualities.filter(function(q) {
+ return q.identifier == identifier;
+ }).pick();
+ } catch (e) {}
+ return {};
+ },
+ addSettings: function() {
+ var self = this;
+ self.settings = App.getPage("Settings");
+ self.settings.addEvent("create", function() {
+ var tab = self.settings.createSubTab("profile", {
+ label: "Quality",
+ name: "profile",
+ subtab_label: "Qualities"
+ }, self.settings.tabs.searcher, "searcher");
+ self.tab = tab.tab;
+ self.content = tab.content;
+ self.createProfiles();
+ self.createProfileOrdering();
+ self.createSizes();
+ });
+ },
+ createProfiles: function() {
+ var self = this;
+ var non_core_profiles = Array.filter(self.profiles, function(profile) {
+ return !profile.isCore();
+ });
+ var count = non_core_profiles.length;
+ self.settings.createGroup({
+ label: "Quality Profiles",
+ description: "Create your own profiles with multiple qualities."
+ }).inject(self.content).adopt(self.profile_container = new Element("div.container"), new Element("a.add_new_profile", {
+ text: count > 0 ? "Create another quality profile" : "Click here to create a quality profile.",
+ events: {
+ click: function() {
+ var profile = self.createProfilesClass();
+ $(profile).inject(self.profile_container);
+ }
+ }
+ }));
+ Array.each(non_core_profiles, function(profile) {
+ $(profile).inject(self.profile_container);
+ });
+ },
+ createProfilesClass: function(data) {
+ var self = this;
+ data = data || {
+ id: randomString()
+ };
+ var profile = new Profile(data);
+ self.profiles.include(profile);
+ return profile;
+ },
+ createProfileOrdering: function() {
+ var self = this;
+ self.settings.createGroup({
+ label: "Profile Defaults",
+ description: "(Needs refresh '" + (App.isMac() ? "CMD+R" : "F5") + "' after editing)"
+ }).grab(new Element(".ctrlHolder#profile_ordering").adopt(new Element("label[text=Order]"), self.profiles_list = new Element("ul"), new Element("p.formHint", {
+ html: "Change the order the profiles are in the dropdown list. Uncheck to hide it completely. First one will be default."
+ }))).inject(self.content);
+ Array.each(self.profiles, function(profile) {
+ var check;
+ new Element("li", {
+ "data-id": profile.data._id
+ }).adopt(check = new Element("input[type=checkbox]", {
+ checked: !profile.data.hide,
+ events: {
+ change: self.saveProfileOrdering.bind(self)
+ }
+ }), new Element("span.profile_label", {
+ text: profile.data.label
+ }), new Element("span.handle.icon-handle")).inject(self.profiles_list);
+ });
+ var sorted_changed = false;
+ self.profile_sortable = new Sortables(self.profiles_list, {
+ revert: true,
+ handle: ".handle",
+ opacity: .5,
+ onSort: function() {
+ sorted_changed = true;
+ },
+ onComplete: function() {
+ if (sorted_changed) {
+ self.saveProfileOrdering();
+ sorted_changed = false;
+ }
+ }
+ });
+ },
+ saveProfileOrdering: function() {
+ var self = this, ids = [], hidden = [];
+ self.profiles_list.getElements("li").each(function(el, nr) {
+ ids.include(el.get("data-id"));
+ hidden[nr] = +!el.getElement("input[type=checkbox]").get("checked");
+ });
+ Api.request("profile.save_order", {
+ data: {
+ ids: ids,
+ hidden: hidden
+ }
+ });
+ },
+ createSizes: function() {
+ var self = this;
+ var group = self.settings.createGroup({
+ label: "Sizes",
+ description: "Edit the minimal and maximum sizes (in MB) for each quality.",
+ advanced: true,
+ name: "sizes"
+ }).inject(self.content);
+ new Element("div.item.head.ctrlHolder").adopt(new Element("span.label", {
+ text: "Quality"
+ }), new Element("span.min", {
+ text: "Min"
+ }), new Element("span.max", {
+ text: "Max"
+ })).inject(group);
+ Array.each(self.qualities, function(quality) {
+ new Element("div.ctrlHolder.item").adopt(new Element("span.label", {
+ text: quality.label
+ }), new Element("input.min[type=text]", {
+ value: quality.size_min,
+ events: {
+ keyup: function(e) {
+ self.changeSize(quality.identifier, "size_min", e.target.get("value"));
+ }
+ }
+ }), new Element("input.max[type=text]", {
+ value: quality.size_max,
+ events: {
+ keyup: function(e) {
+ self.changeSize(quality.identifier, "size_max", e.target.get("value"));
+ }
+ }
+ })).inject(group);
+ });
+ },
+ size_timer: {},
+ changeSize: function(identifier, type, value) {
+ var self = this;
+ if (self.size_timer[identifier + type]) clearRequestTimeout(self.size_timer[identifier + type]);
+ self.size_timer[identifier + type] = requestTimeout(function() {
+ Api.request("quality.size.save", {
+ data: {
+ identifier: identifier,
+ value_type: type,
+ value: value
+ }
+ });
+ }, 300);
+ }
+});
+
+window.Quality = new QualityBase();
+
+Page.Userscript = new Class({
+ Extends: PageBase,
+ order: 80,
+ name: "userscript",
+ has_tab: false,
+ options: {
+ onOpened: function() {
+ App.fireEvent("unload");
+ App.getBlock("header").hide();
+ }
+ },
+ indexAction: function() {
+ var self = this;
+ self.content.grab(self.frame = new Element("div.frame.loading", {
+ text: "Loading..."
+ }));
+ var url = window.location.href.split("url=")[1];
+ Api.request("userscript.add_via_url", {
+ data: {
+ url: url
+ },
+ onComplete: function(json) {
+ self.frame.empty();
+ self.frame.removeClass("loading");
+ if (json.error) self.frame.set("html", json.error); else {
+ var item = new BlockSearchMovieItem(json.movie);
+ self.frame.adopt(item);
+ item.showOptions();
+ }
+ }
+ });
+ }
+});
+
+var UserscriptSettingTab = new Class({
+ tab: "",
+ content: "",
+ initialize: function() {
+ var self = this;
+ App.addEvent("loadSettings", self.addSettings.bind(self));
+ },
+ addSettings: function() {
+ var self = this;
+ self.settings = App.getPage("Settings");
+ self.settings.addEvent("create", function() {
+ var host_url = window.location.protocol + "//" + window.location.host;
+ self.settings.createGroup({
+ name: "userscript",
+ label: "Install the browser extension or bookmarklet",
+ description: "Easily add movies via imdb.com, appletrailers and more"
+ }).inject(self.settings.tabs.automation.content, "top").adopt(new Element("div").adopt(new Element("a.userscript.button", {
+ text: "Install extension",
+ href: "https://couchpota.to/extension/",
+ target: "_blank"
+ }), new Element("span.or[text=or]"), new Element("span.bookmarklet").adopt(new Element("a.button.green", {
+ text: "+CouchPotato",
+ href: "javascript:void((function(){var e=document.createElement('script');e.setAttribute('type','text/javascript');e.setAttribute('charset','UTF-8');e.setAttribute('src','" + host_url + Api.createUrl("userscript.bookmark") + "?host=" + encodeURI(host_url + Api.createUrl("userscript.get") + randomString() + "/") + "&r='+Math.random()*99999999);document.body.appendChild(e)})());",
+ target: "",
+ events: {
+ click: function(e) {
+ e.stop();
+ alert("Drag it to your bookmark ;)");
+ }
+ }
+ }), new Element("span", {
+ text: "Б┤╫ Drag this to your bookmarks"
+ }))), new Element("img", {
+ src: "https://couchpota.to/media/images/userscript.gif"
+ }));
+ });
+ }
+});
+
+window.addEvent("domready", function() {
+ new UserscriptSettingTab();
+});
+
+window.addEvent("load", function() {
+ var your_version = $(document.body).get("data-userscript_version"), latest_version = App.getOption("userscript_version") || "", key = "cp_version_check", checked_already = Cookie.read(key);
+ if (your_version && your_version < latest_version && checked_already < latest_version) {
+ if (confirm("Update to the latest Userscript?\nYour version: " + your_version + ", new version: " + latest_version)) {
+ document.location = Api.createUrl("userscript.get") + randomString() + "/couchpotato.user.js";
+ }
+ Cookie.write(key, latest_version, {
+ duration: 100
+ });
+ }
+});
+
+Page.Wizard = new Class({
+ Extends: Page.Settings,
+ order: 70,
+ name: "wizard",
+ current: "welcome",
+ has_tab: false,
+ wizard_only: true,
+ headers: {
+ welcome: {
+ title: "Welcome to the new CouchPotato",
+ description: "To get started, fill in each of the following settings as much as you can.",
+ content: new Element("div", {
+ styles: {
+ margin: "0 0 0 30px"
+ }
+ })
+ },
+ general: {
+ title: "General",
+ description: "If you want to access CP from outside your local network, you better secure it a bit with a username & password."
+ },
+ downloaders: {
+ title: "What download apps are you using?",
+ description: "CP needs an external download app to work with. Choose one below. For more downloaders check settings after you have filled in the wizard. If your download app isn't in the list, use the default Blackhole."
+ },
+ searcher: {
+ label: "Providers",
+ title: "Are you registered at any of these sites?",
+ description: "CP uses these sites to search for movies. A few free are enabled by default, but it's always better to have more."
+ },
+ renamer: {
+ title: "Move & rename the movies after downloading?",
+ description: "The coolest part of CP is that it can move and organize your downloaded movies automagically. Check settings and you can even download trailers, subtitles and other data when it has finished downloading. It's awesome!"
+ },
+ automation: {
+ title: "Easily add movies to your wanted list!",
+ description: "You can easily add movies from your favorite movie site, like IMDB, Rotten Tomatoes, Apple Trailers and more. Just install the extension or drag the bookmarklet to your bookmarks." + " Once installed, just click the bookmarklet on a movie page and watch the magic happen ;)",
+ content: function() {
+ return App.createUserscriptButtons();
+ }
+ },
+ finish: {
+ title: "Finishing Up",
+ description: "Are you done? Did you fill in everything as much as possible?" + " Be sure to check the settings to see what more CP can do!
" + '
After you\'ve used CP for a while, and you like it (which of course you will), consider supporting CP. Maybe even by writing some code. Or by getting a subscription at Usenet Server or Newshosting.
',
+ content: new Element("div").grab(new Element("a.button.green", {
+ styles: {
+ "margin-top": 20
+ },
+ text: "I'm ready to start the awesomeness!",
+ events: {
+ click: function(e) {
+ e.preventDefault();
+ Api.request("settings.save", {
+ data: {
+ section: "core",
+ name: "show_wizard",
+ value: 0
+ },
+ useSpinner: true,
+ spinnerOptions: {
+ target: self.el
+ },
+ onComplete: function() {
+ window.location = App.createUrl("wanted");
+ }
+ });
+ }
+ }
+ }))
+ }
+ },
+ groups: [ "welcome", "general", "downloaders", "searcher", "renamer", "automation", "finish" ],
+ open: function(action, params) {
+ var self = this;
+ if (!self.initialized) {
+ App.fireEvent("unload");
+ App.getBlock("header").hide();
+ self.parent(action, params);
+ self.el.addClass("settings");
+ self.addEvent("create", function() {
+ self.orderGroups();
+ });
+ self.initialized = true;
+ self.scroll = new Fx.Scroll(document.body, {
+ transition: "quint:in:out"
+ });
+ } else requestTimeout(function() {
+ var sc = self.el.getElement(".wgroup_" + action);
+ self.scroll.start(0, sc.getCoordinates().top - 80);
+ }, 1);
+ },
+ orderGroups: function() {
+ var self = this;
+ var form = self.el.getElement(".uniForm");
+ var tabs = self.el.getElement(".tabs").hide();
+ self.groups.each(function(group) {
+ var group_container;
+ if (self.headers[group]) {
+ group_container = new Element(".wgroup_" + group);
+ if (self.headers[group].include) {
+ self.headers[group].include.each(function(inc) {
+ group_container.addClass("wgroup_" + inc);
+ });
+ }
+ var content = self.headers[group].content;
+ group_container.adopt(new Element("h1", {
+ text: self.headers[group].title
+ }), self.headers[group].description ? new Element("span.description", {
+ html: self.headers[group].description
+ }) : null, content ? typeOf(content) == "function" ? content() : content : null).inject(form);
+ }
+ var tab_navigation = tabs.getElement(".t_" + group);
+ if (!tab_navigation && self.headers[group] && self.headers[group].include) {
+ tab_navigation = [];
+ self.headers[group].include.each(function(inc) {
+ tab_navigation.include(tabs.getElement(".t_" + inc));
+ });
+ }
+ if (tab_navigation && group_container) {
+ tabs.adopt(tab_navigation);
+ if (self.headers[group] && self.headers[group].include) {
+ self.headers[group].include.each(function(inc) {
+ self.el.getElement(".tab_" + inc).inject(group_container);
+ });
+ new Element("li.t_" + group).grab(new Element("a", {
+ href: App.createUrl("wizard/" + group),
+ text: (self.headers[group].label || group).capitalize()
+ })).inject(tabs);
+ } else self.el.getElement(".tab_" + group).inject(group_container);
+ if (tab_navigation.getElement && self.headers[group]) {
+ var a = tab_navigation.getElement("a");
+ a.set("text", (self.headers[group].label || group).capitalize());
+ var url_split = a.get("href").split("wizard")[1].split("/");
+ if (url_split.length > 3) a.set("href", a.get("href").replace(url_split[url_split.length - 3] + "/", ""));
+ }
+ } else {
+ new Element("li.t_" + group).grab(new Element("a", {
+ href: App.createUrl("wizard/" + group),
+ text: (self.headers[group].label || group).capitalize()
+ })).inject(tabs);
+ }
+ if (self.headers[group] && self.headers[group].event) self.headers[group].event.call();
+ });
+ self.el.getElement(".advanced_toggle").destroy();
+ self.el.getElement(".section_nzb").hide();
+ }
+});
\ No newline at end of file
diff --git a/couchpotato/static/scripts/combined.vendor.min.js b/couchpotato/static/scripts/combined.vendor.min.js
new file mode 100644
index 0000000000..cbc329cda0
--- /dev/null
+++ b/couchpotato/static/scripts/combined.vendor.min.js
@@ -0,0 +1,9070 @@
+(function() {
+ this.MooTools = {
+ version: "1.5.1",
+ build: "0542c135fdeb7feed7d9917e01447a408f22c876"
+ };
+ var typeOf = this.typeOf = function(item) {
+ if (item == null) return "null";
+ if (item.$family != null) return item.$family();
+ if (item.nodeName) {
+ if (item.nodeType == 1) return "element";
+ if (item.nodeType == 3) return /\S/.test(item.nodeValue) ? "textnode" : "whitespace";
+ } else if (typeof item.length == "number") {
+ if ("callee" in item) return "arguments";
+ if ("item" in item) return "collection";
+ }
+ return typeof item;
+ };
+ var instanceOf = this.instanceOf = function(item, object) {
+ if (item == null) return false;
+ var constructor = item.$constructor || item.constructor;
+ while (constructor) {
+ if (constructor === object) return true;
+ constructor = constructor.parent;
+ }
+ if (!item.hasOwnProperty) return false;
+ return item instanceof object;
+ };
+ var Function = this.Function;
+ var enumerables = true;
+ for (var i in {
+ toString: 1
+ }) enumerables = null;
+ if (enumerables) enumerables = [ "hasOwnProperty", "valueOf", "isPrototypeOf", "propertyIsEnumerable", "toLocaleString", "toString", "constructor" ];
+ Function.prototype.overloadSetter = function(usePlural) {
+ var self = this;
+ return function(a, b) {
+ if (a == null) return this;
+ if (usePlural || typeof a != "string") {
+ for (var k in a) self.call(this, k, a[k]);
+ if (enumerables) for (var i = enumerables.length; i--; ) {
+ k = enumerables[i];
+ if (a.hasOwnProperty(k)) self.call(this, k, a[k]);
+ }
+ } else {
+ self.call(this, a, b);
+ }
+ return this;
+ };
+ };
+ Function.prototype.overloadGetter = function(usePlural) {
+ var self = this;
+ return function(a) {
+ var args, result;
+ if (typeof a != "string") args = a; else if (arguments.length > 1) args = arguments; else if (usePlural) args = [ a ];
+ if (args) {
+ result = {};
+ for (var i = 0; i < args.length; i++) result[args[i]] = self.call(this, args[i]);
+ } else {
+ result = self.call(this, a);
+ }
+ return result;
+ };
+ };
+ Function.prototype.extend = function(key, value) {
+ this[key] = value;
+ }.overloadSetter();
+ Function.prototype.implement = function(key, value) {
+ this.prototype[key] = value;
+ }.overloadSetter();
+ var slice = Array.prototype.slice;
+ Function.from = function(item) {
+ return typeOf(item) == "function" ? item : function() {
+ return item;
+ };
+ };
+ Array.from = function(item) {
+ if (item == null) return [];
+ return Type.isEnumerable(item) && typeof item != "string" ? typeOf(item) == "array" ? item : slice.call(item) : [ item ];
+ };
+ Number.from = function(item) {
+ var number = parseFloat(item);
+ return isFinite(number) ? number : null;
+ };
+ String.from = function(item) {
+ return item + "";
+ };
+ Function.implement({
+ hide: function() {
+ this.$hidden = true;
+ return this;
+ },
+ protect: function() {
+ this.$protected = true;
+ return this;
+ }
+ });
+ var Type = this.Type = function(name, object) {
+ if (name) {
+ var lower = name.toLowerCase();
+ var typeCheck = function(item) {
+ return typeOf(item) == lower;
+ };
+ Type["is" + name] = typeCheck;
+ if (object != null) {
+ object.prototype.$family = function() {
+ return lower;
+ }.hide();
+ }
+ }
+ if (object == null) return null;
+ object.extend(this);
+ object.$constructor = Type;
+ object.prototype.$constructor = object;
+ return object;
+ };
+ var toString = Object.prototype.toString;
+ Type.isEnumerable = function(item) {
+ return item != null && typeof item.length == "number" && toString.call(item) != "[object Function]";
+ };
+ var hooks = {};
+ var hooksOf = function(object) {
+ var type = typeOf(object.prototype);
+ return hooks[type] || (hooks[type] = []);
+ };
+ var implement = function(name, method) {
+ if (method && method.$hidden) return;
+ var hooks = hooksOf(this);
+ for (var i = 0; i < hooks.length; i++) {
+ var hook = hooks[i];
+ if (typeOf(hook) == "type") implement.call(hook, name, method); else hook.call(this, name, method);
+ }
+ var previous = this.prototype[name];
+ if (previous == null || !previous.$protected) this.prototype[name] = method;
+ if (this[name] == null && typeOf(method) == "function") extend.call(this, name, function(item) {
+ return method.apply(item, slice.call(arguments, 1));
+ });
+ };
+ var extend = function(name, method) {
+ if (method && method.$hidden) return;
+ var previous = this[name];
+ if (previous == null || !previous.$protected) this[name] = method;
+ };
+ Type.implement({
+ implement: implement.overloadSetter(),
+ extend: extend.overloadSetter(),
+ alias: function(name, existing) {
+ implement.call(this, name, this.prototype[existing]);
+ }.overloadSetter(),
+ mirror: function(hook) {
+ hooksOf(this).push(hook);
+ return this;
+ }
+ });
+ new Type("Type", Type);
+ var force = function(name, object, methods) {
+ var isType = object != Object, prototype = object.prototype;
+ if (isType) object = new Type(name, object);
+ for (var i = 0, l = methods.length; i < l; i++) {
+ var key = methods[i], generic = object[key], proto = prototype[key];
+ if (generic) generic.protect();
+ if (isType && proto) object.implement(key, proto.protect());
+ }
+ if (isType) {
+ var methodsEnumerable = prototype.propertyIsEnumerable(methods[0]);
+ object.forEachMethod = function(fn) {
+ if (!methodsEnumerable) for (var i = 0, l = methods.length; i < l; i++) {
+ fn.call(prototype, prototype[methods[i]], methods[i]);
+ }
+ for (var key in prototype) fn.call(prototype, prototype[key], key);
+ };
+ }
+ return force;
+ };
+ force("String", String, [ "charAt", "charCodeAt", "concat", "contains", "indexOf", "lastIndexOf", "match", "quote", "replace", "search", "slice", "split", "substr", "substring", "trim", "toLowerCase", "toUpperCase" ])("Array", Array, [ "pop", "push", "reverse", "shift", "sort", "splice", "unshift", "concat", "join", "slice", "indexOf", "lastIndexOf", "filter", "forEach", "every", "map", "some", "reduce", "reduceRight" ])("Number", Number, [ "toExponential", "toFixed", "toLocaleString", "toPrecision" ])("Function", Function, [ "apply", "call", "bind" ])("RegExp", RegExp, [ "exec", "test" ])("Object", Object, [ "create", "defineProperty", "defineProperties", "keys", "getPrototypeOf", "getOwnPropertyDescriptor", "getOwnPropertyNames", "preventExtensions", "isExtensible", "seal", "isSealed", "freeze", "isFrozen" ])("Date", Date, [ "now" ]);
+ Object.extend = extend.overloadSetter();
+ Date.extend("now", function() {
+ return +new Date();
+ });
+ new Type("Boolean", Boolean);
+ Number.prototype.$family = function() {
+ return isFinite(this) ? "number" : "null";
+ }.hide();
+ Number.extend("random", function(min, max) {
+ return Math.floor(Math.random() * (max - min + 1) + min);
+ });
+ var hasOwnProperty = Object.prototype.hasOwnProperty;
+ Object.extend("forEach", function(object, fn, bind) {
+ for (var key in object) {
+ if (hasOwnProperty.call(object, key)) fn.call(bind, object[key], key, object);
+ }
+ });
+ Object.each = Object.forEach;
+ Array.implement({
+ forEach: function(fn, bind) {
+ for (var i = 0, l = this.length; i < l; i++) {
+ if (i in this) fn.call(bind, this[i], i, this);
+ }
+ },
+ each: function(fn, bind) {
+ Array.forEach(this, fn, bind);
+ return this;
+ }
+ });
+ var cloneOf = function(item) {
+ switch (typeOf(item)) {
+ case "array":
+ return item.clone();
+
+ case "object":
+ return Object.clone(item);
+
+ default:
+ return item;
+ }
+ };
+ Array.implement("clone", function() {
+ var i = this.length, clone = new Array(i);
+ while (i--) clone[i] = cloneOf(this[i]);
+ return clone;
+ });
+ var mergeOne = function(source, key, current) {
+ switch (typeOf(current)) {
+ case "object":
+ if (typeOf(source[key]) == "object") Object.merge(source[key], current); else source[key] = Object.clone(current);
+ break;
+
+ case "array":
+ source[key] = current.clone();
+ break;
+
+ default:
+ source[key] = current;
+ }
+ return source;
+ };
+ Object.extend({
+ merge: function(source, k, v) {
+ if (typeOf(k) == "string") return mergeOne(source, k, v);
+ for (var i = 1, l = arguments.length; i < l; i++) {
+ var object = arguments[i];
+ for (var key in object) mergeOne(source, key, object[key]);
+ }
+ return source;
+ },
+ clone: function(object) {
+ var clone = {};
+ for (var key in object) clone[key] = cloneOf(object[key]);
+ return clone;
+ },
+ append: function(original) {
+ for (var i = 1, l = arguments.length; i < l; i++) {
+ var extended = arguments[i] || {};
+ for (var key in extended) original[key] = extended[key];
+ }
+ return original;
+ }
+ });
+ [ "Object", "WhiteSpace", "TextNode", "Collection", "Arguments" ].each(function(name) {
+ new Type(name);
+ });
+ var UID = Date.now();
+ String.extend("uniqueID", function() {
+ return (UID++).toString(36);
+ });
+})();
+
+Array.implement({
+ every: function(fn, bind) {
+ for (var i = 0, l = this.length >>> 0; i < l; i++) {
+ if (i in this && !fn.call(bind, this[i], i, this)) return false;
+ }
+ return true;
+ },
+ filter: function(fn, bind) {
+ var results = [];
+ for (var value, i = 0, l = this.length >>> 0; i < l; i++) if (i in this) {
+ value = this[i];
+ if (fn.call(bind, value, i, this)) results.push(value);
+ }
+ return results;
+ },
+ indexOf: function(item, from) {
+ var length = this.length >>> 0;
+ for (var i = from < 0 ? Math.max(0, length + from) : from || 0; i < length; i++) {
+ if (this[i] === item) return i;
+ }
+ return -1;
+ },
+ map: function(fn, bind) {
+ var length = this.length >>> 0, results = Array(length);
+ for (var i = 0; i < length; i++) {
+ if (i in this) results[i] = fn.call(bind, this[i], i, this);
+ }
+ return results;
+ },
+ some: function(fn, bind) {
+ for (var i = 0, l = this.length >>> 0; i < l; i++) {
+ if (i in this && fn.call(bind, this[i], i, this)) return true;
+ }
+ return false;
+ },
+ clean: function() {
+ return this.filter(function(item) {
+ return item != null;
+ });
+ },
+ invoke: function(methodName) {
+ var args = Array.slice(arguments, 1);
+ return this.map(function(item) {
+ return item[methodName].apply(item, args);
+ });
+ },
+ associate: function(keys) {
+ var obj = {}, length = Math.min(this.length, keys.length);
+ for (var i = 0; i < length; i++) obj[keys[i]] = this[i];
+ return obj;
+ },
+ link: function(object) {
+ var result = {};
+ for (var i = 0, l = this.length; i < l; i++) {
+ for (var key in object) {
+ if (object[key](this[i])) {
+ result[key] = this[i];
+ delete object[key];
+ break;
+ }
+ }
+ }
+ return result;
+ },
+ contains: function(item, from) {
+ return this.indexOf(item, from) != -1;
+ },
+ append: function(array) {
+ this.push.apply(this, array);
+ return this;
+ },
+ getLast: function() {
+ return this.length ? this[this.length - 1] : null;
+ },
+ getRandom: function() {
+ return this.length ? this[Number.random(0, this.length - 1)] : null;
+ },
+ include: function(item) {
+ if (!this.contains(item)) this.push(item);
+ return this;
+ },
+ combine: function(array) {
+ for (var i = 0, l = array.length; i < l; i++) this.include(array[i]);
+ return this;
+ },
+ erase: function(item) {
+ for (var i = this.length; i--; ) {
+ if (this[i] === item) this.splice(i, 1);
+ }
+ return this;
+ },
+ empty: function() {
+ this.length = 0;
+ return this;
+ },
+ flatten: function() {
+ var array = [];
+ for (var i = 0, l = this.length; i < l; i++) {
+ var type = typeOf(this[i]);
+ if (type == "null") continue;
+ array = array.concat(type == "array" || type == "collection" || type == "arguments" || instanceOf(this[i], Array) ? Array.flatten(this[i]) : this[i]);
+ }
+ return array;
+ },
+ pick: function() {
+ for (var i = 0, l = this.length; i < l; i++) {
+ if (this[i] != null) return this[i];
+ }
+ return null;
+ },
+ hexToRgb: function(array) {
+ if (this.length != 3) return null;
+ var rgb = this.map(function(value) {
+ if (value.length == 1) value += value;
+ return parseInt(value, 16);
+ });
+ return array ? rgb : "rgb(" + rgb + ")";
+ },
+ rgbToHex: function(array) {
+ if (this.length < 3) return null;
+ if (this.length == 4 && this[3] == 0 && !array) return "transparent";
+ var hex = [];
+ for (var i = 0; i < 3; i++) {
+ var bit = (this[i] - 0).toString(16);
+ hex.push(bit.length == 1 ? "0" + bit : bit);
+ }
+ return array ? hex : "#" + hex.join("");
+ }
+});
+
+String.implement({
+ contains: function(string, index) {
+ return (index ? String(this).slice(index) : String(this)).indexOf(string) > -1;
+ },
+ test: function(regex, params) {
+ return (typeOf(regex) == "regexp" ? regex : new RegExp("" + regex, params)).test(this);
+ },
+ trim: function() {
+ return String(this).replace(/^\s+|\s+$/g, "");
+ },
+ clean: function() {
+ return String(this).replace(/\s+/g, " ").trim();
+ },
+ camelCase: function() {
+ return String(this).replace(/-\D/g, function(match) {
+ return match.charAt(1).toUpperCase();
+ });
+ },
+ hyphenate: function() {
+ return String(this).replace(/[A-Z]/g, function(match) {
+ return "-" + match.charAt(0).toLowerCase();
+ });
+ },
+ capitalize: function() {
+ return String(this).replace(/\b[a-z]/g, function(match) {
+ return match.toUpperCase();
+ });
+ },
+ escapeRegExp: function() {
+ return String(this).replace(/([-.*+?^${}()|[\]\/\\])/g, "\\$1");
+ },
+ toInt: function(base) {
+ return parseInt(this, base || 10);
+ },
+ toFloat: function() {
+ return parseFloat(this);
+ },
+ hexToRgb: function(array) {
+ var hex = String(this).match(/^#?(\w{1,2})(\w{1,2})(\w{1,2})$/);
+ return hex ? hex.slice(1).hexToRgb(array) : null;
+ },
+ rgbToHex: function(array) {
+ var rgb = String(this).match(/\d{1,3}/g);
+ return rgb ? rgb.rgbToHex(array) : null;
+ },
+ substitute: function(object, regexp) {
+ return String(this).replace(regexp || /\\?\{([^{}]+)\}/g, function(match, name) {
+ if (match.charAt(0) == "\\") return match.slice(1);
+ return object[name] != null ? object[name] : "";
+ });
+ }
+});
+
+Function.extend({
+ attempt: function() {
+ for (var i = 0, l = arguments.length; i < l; i++) {
+ try {
+ return arguments[i]();
+ } catch (e) {}
+ }
+ return null;
+ }
+});
+
+Function.implement({
+ attempt: function(args, bind) {
+ try {
+ return this.apply(bind, Array.from(args));
+ } catch (e) {}
+ return null;
+ },
+ bind: function(that) {
+ var self = this, args = arguments.length > 1 ? Array.slice(arguments, 1) : null, F = function() {};
+ var bound = function() {
+ var context = that, length = arguments.length;
+ if (this instanceof bound) {
+ F.prototype = self.prototype;
+ context = new F();
+ }
+ var result = !args && !length ? self.call(context) : self.apply(context, args && length ? args.concat(Array.slice(arguments)) : args || arguments);
+ return context == that ? result : context;
+ };
+ return bound;
+ },
+ pass: function(args, bind) {
+ var self = this;
+ if (args != null) args = Array.from(args);
+ return function() {
+ return self.apply(bind, args || arguments);
+ };
+ },
+ delay: function(delay, bind, args) {
+ return setTimeout(this.pass(args == null ? [] : args, bind), delay);
+ },
+ periodical: function(periodical, bind, args) {
+ return setInterval(this.pass(args == null ? [] : args, bind), periodical);
+ }
+});
+
+Number.implement({
+ limit: function(min, max) {
+ return Math.min(max, Math.max(min, this));
+ },
+ round: function(precision) {
+ precision = Math.pow(10, precision || 0).toFixed(precision < 0 ? -precision : 0);
+ return Math.round(this * precision) / precision;
+ },
+ times: function(fn, bind) {
+ for (var i = 0; i < this; i++) fn.call(bind, i, this);
+ },
+ toFloat: function() {
+ return parseFloat(this);
+ },
+ toInt: function(base) {
+ return parseInt(this, base || 10);
+ }
+});
+
+Number.alias("each", "times");
+
+(function(math) {
+ var methods = {};
+ math.each(function(name) {
+ if (!Number[name]) methods[name] = function() {
+ return Math[name].apply(null, [ this ].concat(Array.from(arguments)));
+ };
+ });
+ Number.implement(methods);
+})([ "abs", "acos", "asin", "atan", "atan2", "ceil", "cos", "exp", "floor", "log", "max", "min", "pow", "sin", "sqrt", "tan" ]);
+
+(function() {
+ var Class = this.Class = new Type("Class", function(params) {
+ if (instanceOf(params, Function)) params = {
+ initialize: params
+ };
+ var newClass = function() {
+ reset(this);
+ if (newClass.$prototyping) return this;
+ this.$caller = null;
+ var value = this.initialize ? this.initialize.apply(this, arguments) : this;
+ this.$caller = this.caller = null;
+ return value;
+ }.extend(this).implement(params);
+ newClass.$constructor = Class;
+ newClass.prototype.$constructor = newClass;
+ newClass.prototype.parent = parent;
+ return newClass;
+ });
+ var parent = function() {
+ if (!this.$caller) throw new Error('The method "parent" cannot be called.');
+ var name = this.$caller.$name, parent = this.$caller.$owner.parent, previous = parent ? parent.prototype[name] : null;
+ if (!previous) throw new Error('The method "' + name + '" has no parent.');
+ return previous.apply(this, arguments);
+ };
+ var reset = function(object) {
+ for (var key in object) {
+ var value = object[key];
+ switch (typeOf(value)) {
+ case "object":
+ var F = function() {};
+ F.prototype = value;
+ object[key] = reset(new F());
+ break;
+
+ case "array":
+ object[key] = value.clone();
+ break;
+ }
+ }
+ return object;
+ };
+ var wrap = function(self, key, method) {
+ if (method.$origin) method = method.$origin;
+ var wrapper = function() {
+ if (method.$protected && this.$caller == null) throw new Error('The method "' + key + '" cannot be called.');
+ var caller = this.caller, current = this.$caller;
+ this.caller = current;
+ this.$caller = wrapper;
+ var result = method.apply(this, arguments);
+ this.$caller = current;
+ this.caller = caller;
+ return result;
+ }.extend({
+ $owner: self,
+ $origin: method,
+ $name: key
+ });
+ return wrapper;
+ };
+ var implement = function(key, value, retain) {
+ if (Class.Mutators.hasOwnProperty(key)) {
+ value = Class.Mutators[key].call(this, value);
+ if (value == null) return this;
+ }
+ if (typeOf(value) == "function") {
+ if (value.$hidden) return this;
+ this.prototype[key] = retain ? value : wrap(this, key, value);
+ } else {
+ Object.merge(this.prototype, key, value);
+ }
+ return this;
+ };
+ var getInstance = function(klass) {
+ klass.$prototyping = true;
+ var proto = new klass();
+ delete klass.$prototyping;
+ return proto;
+ };
+ Class.implement("implement", implement.overloadSetter());
+ Class.Mutators = {
+ Extends: function(parent) {
+ this.parent = parent;
+ this.prototype = getInstance(parent);
+ },
+ Implements: function(items) {
+ Array.from(items).each(function(item) {
+ var instance = new item();
+ for (var key in instance) implement.call(this, key, instance[key], true);
+ }, this);
+ }
+ };
+})();
+
+(function() {
+ this.Chain = new Class({
+ $chain: [],
+ chain: function() {
+ this.$chain.append(Array.flatten(arguments));
+ return this;
+ },
+ callChain: function() {
+ return this.$chain.length ? this.$chain.shift().apply(this, arguments) : false;
+ },
+ clearChain: function() {
+ this.$chain.empty();
+ return this;
+ }
+ });
+ var removeOn = function(string) {
+ return string.replace(/^on([A-Z])/, function(full, first) {
+ return first.toLowerCase();
+ });
+ };
+ this.Events = new Class({
+ $events: {},
+ addEvent: function(type, fn, internal) {
+ type = removeOn(type);
+ this.$events[type] = (this.$events[type] || []).include(fn);
+ if (internal) fn.internal = true;
+ return this;
+ },
+ addEvents: function(events) {
+ for (var type in events) this.addEvent(type, events[type]);
+ return this;
+ },
+ fireEvent: function(type, args, delay) {
+ type = removeOn(type);
+ var events = this.$events[type];
+ if (!events) return this;
+ args = Array.from(args);
+ events.each(function(fn) {
+ if (delay) fn.delay(delay, this, args); else fn.apply(this, args);
+ }, this);
+ return this;
+ },
+ removeEvent: function(type, fn) {
+ type = removeOn(type);
+ var events = this.$events[type];
+ if (events && !fn.internal) {
+ var index = events.indexOf(fn);
+ if (index != -1) delete events[index];
+ }
+ return this;
+ },
+ removeEvents: function(events) {
+ var type;
+ if (typeOf(events) == "object") {
+ for (type in events) this.removeEvent(type, events[type]);
+ return this;
+ }
+ if (events) events = removeOn(events);
+ for (type in this.$events) {
+ if (events && events != type) continue;
+ var fns = this.$events[type];
+ for (var i = fns.length; i--; ) if (i in fns) {
+ this.removeEvent(type, fns[i]);
+ }
+ }
+ return this;
+ }
+ });
+ this.Options = new Class({
+ setOptions: function() {
+ var options = this.options = Object.merge.apply(null, [ {}, this.options ].append(arguments));
+ if (this.addEvent) for (var option in options) {
+ if (typeOf(options[option]) != "function" || !/^on[A-Z]/.test(option)) continue;
+ this.addEvent(option, options[option]);
+ delete options[option];
+ }
+ return this;
+ }
+ });
+})();
+
+(function() {
+ var document = this.document;
+ var window = document.window = this;
+ var parse = function(ua, platform) {
+ ua = ua.toLowerCase();
+ platform = platform ? platform.toLowerCase() : "";
+ var UA = ua.match(/(opera|ie|firefox|chrome|trident|crios|version)[\s\/:]([\w\d\.]+)?.*?(safari|(?:rv[\s\/:]|version[\s\/:])([\w\d\.]+)|$)/) || [ null, "unknown", 0 ];
+ if (UA[1] == "trident") {
+ UA[1] = "ie";
+ if (UA[4]) UA[2] = UA[4];
+ } else if (UA[1] == "crios") {
+ UA[1] = "chrome";
+ }
+ platform = ua.match(/ip(?:ad|od|hone)/) ? "ios" : (ua.match(/(?:webos|android)/) || platform.match(/mac|win|linux/) || [ "other" ])[0];
+ if (platform == "win") platform = "windows";
+ return {
+ extend: Function.prototype.extend,
+ name: UA[1] == "version" ? UA[3] : UA[1],
+ version: parseFloat(UA[1] == "opera" && UA[4] ? UA[4] : UA[2]),
+ platform: platform
+ };
+ };
+ var Browser = this.Browser = parse(navigator.userAgent, navigator.platform);
+ if (Browser.name == "ie") {
+ Browser.version = document.documentMode;
+ }
+ Browser.extend({
+ Features: {
+ xpath: !!document.evaluate,
+ air: !!window.runtime,
+ query: !!document.querySelector,
+ json: !!window.JSON
+ },
+ parseUA: parse
+ });
+ Browser.Request = function() {
+ var XMLHTTP = function() {
+ return new XMLHttpRequest();
+ };
+ var MSXML2 = function() {
+ return new ActiveXObject("MSXML2.XMLHTTP");
+ };
+ var MSXML = function() {
+ return new ActiveXObject("Microsoft.XMLHTTP");
+ };
+ return Function.attempt(function() {
+ XMLHTTP();
+ return XMLHTTP;
+ }, function() {
+ MSXML2();
+ return MSXML2;
+ }, function() {
+ MSXML();
+ return MSXML;
+ });
+ }();
+ Browser.Features.xhr = !!Browser.Request;
+ Browser.exec = function(text) {
+ if (!text) return text;
+ if (window.execScript) {
+ window.execScript(text);
+ } else {
+ var script = document.createElement("script");
+ script.setAttribute("type", "text/javascript");
+ script.text = text;
+ document.head.appendChild(script);
+ document.head.removeChild(script);
+ }
+ return text;
+ };
+ String.implement("stripScripts", function(exec) {
+ var scripts = "";
+ var text = this.replace(/{% endfor %}
-
- {% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'head', single = True) %}
- {% endfor %}
- {% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'head', single = True) %}
- {% endfor %}
-
-
-
-
-
-
-
- CouchPotato
-
-
-