Merge branch 'refs/heads/develop' into desktop

This commit is contained in:
Ruud
2012-04-28 23:14:59 +02:00
184 changed files with 5634 additions and 4017 deletions
+6 -2
View File
@@ -114,6 +114,7 @@ class Loader(object):
if __name__ == '__main__':
l = None
try:
l = Loader()
l.daemonize()
@@ -135,7 +136,10 @@ if __name__ == '__main__':
try:
# if this fails we will have two tracebacks
# one for failing to log, and one for the exception that got us here.
l.log.critical(traceback.format_exc())
if l:
l.log.critical(traceback.format_exc())
else:
print traceback.format_exc()
except:
print traceback.format_exc()
raise
raise
+34 -1
View File
@@ -2,4 +2,37 @@ CouchPotato Server
=====
CouchPotato (CP) is an automatic NZB and torrent downloader. You can keep a "movies I want"-list and it will search for NZBs/torrents of these movies every X hours.
Once a movie is found, it will send it to SABnzbd or download the torrent to a specified directory.
Once a movie is found, it will send it to SABnzbd or download the torrent to a specified directory.
## Running from Source
CouchPotatoServer can be run from source. This will use *git* as updater, so make sure that is installed also.
Windows:
* Install [PyWin32 2.7](http://sourceforge.net/projects/pywin32/files/pywin32/Build%20217/) and [GIT](http://git-scm.com/)
* If you come and ask on the forums 'why directory selection no work?', I will kill a kitten, also this is because you need PyWin32
* Open up `Git Bash` (or CMD) and go to the folder you want to install CP. Something like Program Files.
* Run `git clone https://RuudBurger@github.com/RuudBurger/CouchPotatoServer.git`.
* You can now start CP via `CouchPotatoServer\CouchPotato.py` to start
OSx:
* If you're on Leopard (10.5) install Python 2.6+: [Python 2.6.5](http://www.python.org/download/releases/2.6.5/)
* Install [GIT](http://git-scm.com/)
* Open up `Terminal`
* Go to your App folder `cd /Applications`
* Run `git clone https://RuudBurger@github.com/RuudBurger/CouchPotatoServer.git`
* Then do `python CouchPotatoServer/CouchPotato.py`
Linux (ubuntu / debian):
* Install [GIT](http://git-scm.com/) with `apt-get install git-core`
* 'cd' to the folder of your choosing.
* Run `git clone https://RuudBurger@github.com/RuudBurger/CouchPotatoServer.git`
* Then do `python CouchPotatoServer/CouchPotato.py` to start
* To run on boot copy the init script. `cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato`
* Change the paths inside the init script. `nano /etc/init.d/couchpotato`
* Make it executable. `chmod +x /etc/init.d/couchpotato`
* Add it to defaults. `sudo update-rc.d couchpotato defaults`
+18
View File
@@ -1,6 +1,8 @@
from couchpotato.api import api_docs, api_docs_missing
from couchpotato.core.auth import requires_auth
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.request import getParams, jsonified
from couchpotato.core.helpers.variable import md5
from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
from flask.app import Flask
@@ -51,6 +53,22 @@ def apiDocs():
del api_docs_missing['']
return render_template('api.html', fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing))
@web.route('getkey/')
def getApiKey():
api = None
params = getParams()
username = Env.setting('username')
password = Env.setting('password')
if (params.get('u') == md5(username) or not username) and (params.get('p') == password or not password):
api = Env.setting('api_key')
return jsonified({
'success': api is not None,
'api_key': api
})
@app.errorhandler(404)
def page_not_found(error):
index_url = url_for('web.index')
+1 -1
View File
@@ -55,7 +55,7 @@ config = [{
'name': 'api_key',
'default': uuid4().hex,
'readonly': 1,
'description': 'Let 3rd party app do stuff. <a target="_self" href="/docs/">Docs</a>',
'description': 'Let 3rd party app do stuff. <a target="_self" href="../../docs/">Docs</a>',
},
{
'name': 'debug',
+19
View File
@@ -8,6 +8,7 @@ from couchpotato.environment import Env
from flask import request
from uuid import uuid4
import os
import platform
import time
import traceback
import webbrowser
@@ -32,12 +33,16 @@ class Core(Plugin):
addApiView('app.available', self.available, docs = {
'desc': 'Check if app available.'
})
addApiView('app.version', self.versionView, docs = {
'desc': 'Get version.'
})
addEvent('app.crappy_shutdown', self.crappyShutdown)
addEvent('app.crappy_restart', self.crappyRestart)
addEvent('app.load', self.launchBrowser, priority = 1)
addEvent('app.base_url', self.createBaseUrl)
addEvent('app.api_url', self.createApiUrl)
addEvent('app.version', self.version)
addEvent('setting.save.core.password', self.md5Password)
addEvent('setting.save.core.api_key', self.checkApikey)
@@ -161,3 +166,17 @@ class Core(Plugin):
def createApiUrl(self):
return '%s/%s' % (self.createBaseUrl(), Env.setting('api_key'))
def version(self):
ver = fireEvent('updater.info', single = True)
if os.name == 'nt': platf = 'windows'
elif 'Darwin' in platform.platform(): platf = 'osx'
else: platf = 'linux'
return '%s - %s-%s - v2' % (platf, ver.get('version')['type'], ver.get('version')['hash'])
def versionView(self):
return jsonified({
'version': self.version()
})
+246 -80
View File
@@ -4,9 +4,13 @@ from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
from git.repository import LocalRepository
from datetime import datetime
from dateutil.parser import parse
from git.repository import LocalRepository
import json
import os
import shutil
import tarfile
import time
import traceback
@@ -15,27 +19,22 @@ log = CPLog(__name__)
class Updater(Plugin):
repo_name = 'RuudBurger/CouchPotatoServer'
version = None
update_failed = False
update_version = None
last_check = 0
def __init__(self):
self.repo = LocalRepository(Env.get('app_dir'), command = self.conf('git_command', default = 'git'))
if os.path.isdir(os.path.join(Env.get('app_dir'), '.git')):
self.updater = GitUpdater(self.conf('git_command', default = 'git'))
else:
self.updater = SourceUpdater()
fireEvent('schedule.interval', 'updater.check', self.check, hours = 6)
addEvent('app.load', self.check)
addEvent('updater.info', self.info)
addApiView('updater.info', self.getInfo, docs = {
'desc': 'Get updater information',
'return': {
'type': 'object',
'example': """{
'repo_name': "Name of used repository",
'last_check': "last checked for update",
'update_version': "available update version or empty",
'version': current_cp_version
@@ -47,75 +46,93 @@ class Updater(Plugin):
'return': {'type': 'see updater.info'}
})
def getInfo(self):
def check(self):
if self.isDisabled():
return
if self.updater.check():
if self.conf('automatic') and not self.updater.update_failed:
if self.updater.doUpdate():
fireEventAsync('app.crappy_restart')
else:
if self.conf('notification'):
fireEvent('updater.available', message = 'A new update is available', data = self.updater.getVersion())
def info(self):
return self.updater.info()
def getInfo(self):
return jsonified(self.updater.info())
def checkView(self):
self.check()
return self.updater.getInfo()
def doUpdateView(self):
return jsonified({
'success': self.updater.doUpdate()
})
class BaseUpdater(Plugin):
repo_user = 'RuudBurger'
repo_name = 'CouchPotatoServer'
branch = 'develop'
version = None
update_failed = False
update_version = None
last_check = 0
def doUpdate(self):
pass
def getInfo(self):
return jsonified(self.info())
def info(self):
return {
'repo_name': self.repo_name,
'last_check': self.last_check,
'update_version': self.update_version,
'version': self.getVersion()
'version': self.getVersion(),
'repo_name': '%s/%s' % (self.repo_user, self.repo_name),
'branch': self.branch,
}
def getVersion(self):
if not self.version:
try:
output = self.repo.getHead() # Yes, please
log.debug('Git version output: %s' % output.hash)
self.version = {
'hash': output.hash[:8],
'date': output.getDate(),
}
except Exception, e:
log.error('Failed using GIT updater, running from source, you need to have GIT installed. %s' % e)
return 'No GIT'
return self.version
def check(self):
pass
if self.update_version or self.isDisabled():
return
def deletePyc(self, only_excess = True):
log.info('Checking for new version on github for %s' % self.repo_name)
if not Env.get('dev'):
self.repo.fetch()
for root, dirs, files in os.walk(Env.get('app_dir')):
current_branch = self.repo.getCurrentBranch().name
pyc_files = filter(lambda filename: filename.endswith('.pyc'), files)
py_files = set(filter(lambda filename: filename.endswith('.py'), files))
excess_pyc_files = filter(lambda pyc_filename: pyc_filename[:-1] not in py_files, pyc_files) if only_excess else pyc_files
for branch in self.repo.getRemoteByName('origin').getBranches():
if current_branch == branch.name:
for excess_pyc_file in excess_pyc_files:
full_path = os.path.join(root, excess_pyc_file)
log.debug('Removing old PYC file: %s' % full_path)
try:
os.remove(full_path)
except:
log.error('Couldn\'t remove %s: %s' % (full_path, traceback.format_exc()))
local = self.repo.getHead()
remote = branch.getHead()
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:
log.error('Couldn\'t remove empty directory %s: %s' % (full_path, traceback.format_exc()))
log.info('Versions, local:%s, remote:%s' % (local.hash[:8], remote.hash[:8]))
if local.getDate() < remote.getDate():
self.update_version = {
'hash': remote.hash[:8],
'date': remote.getDate(),
}
if self.conf('automatic') and not self.update_failed:
if self.doUpdate():
fireEventAsync('app.crappy_restart')
else:
if self.conf('notification'):
fireEvent('updater.available', message = 'A new update is available', data = self.getVersion())
self.last_check = time.time()
class GitUpdater(BaseUpdater):
def checkView(self):
self.check()
return self.getInfo()
def doUpdateView(self):
return jsonified({
'success': self.doUpdate()
})
def __init__(self, git_command):
self.repo = LocalRepository(Env.get('app_dir'), command = git_command)
def doUpdate(self):
try:
@@ -141,29 +158,178 @@ class Updater(Plugin):
return False
def deletePyc(self):
def getVersion(self):
for root, dirs, files in os.walk(Env.get('app_dir')):
if not self.version:
try:
output = self.repo.getHead() # Yes, please
log.debug('Git version output: %s' % output.hash)
self.version = {
'hash': output.hash[:8],
'date': output.getDate(),
'type': 'git',
}
except Exception, e:
log.error('Failed using GIT updater, running from source, you need to have GIT installed. %s' % e)
return 'No GIT'
pyc_files = filter(lambda filename: filename.endswith('.pyc'), files)
py_files = set(filter(lambda filename: filename.endswith('.py'), files))
excess_pyc_files = filter(lambda pyc_filename: pyc_filename[:-1] not in py_files, pyc_files)
return self.version
for excess_pyc_file in excess_pyc_files:
full_path = os.path.join(root, excess_pyc_file)
log.debug('Removing old PYC file: %s' % full_path)
try:
os.remove(full_path)
except:
log.error('Couldn\'t remove %s: %s' % (full_path, traceback.format_exc()))
def check(self):
for dir_name in dirs:
full_path = os.path.join(root, dir_name)
if len(os.listdir(full_path)) == 0:
if self.update_version:
return
log.info('Checking for new version on github for %s' % self.repo_name)
if not Env.get('dev'):
self.repo.fetch()
current_branch = self.repo.getCurrentBranch().name
for branch in self.repo.getRemoteByName('origin').getBranches():
if current_branch == branch.name:
local = self.repo.getHead()
remote = branch.getHead()
log.info('Versions, local:%s, remote:%s' % (local.hash[:8], remote.hash[:8]))
if local.getDate() < remote.getDate():
self.update_version = {
'hash': remote.hash[:8],
'date': remote.getDate(),
}
return True
self.last_check = time.time()
return False
class SourceUpdater(BaseUpdater):
def __init__(self):
# Create version file in cache
self.version_file = os.path.join(Env.get('cache_dir'), 'version')
if not os.path.isfile(self.version_file):
self.createFile(self.version_file, json.dumps(self.latestCommit()))
def doUpdate(self):
try:
url = 'https://github.com/%s/%s/tarball/%s' % (self.repo_user, self.repo_name, self.branch)
destination = os.path.join(Env.get('cache_dir'), self.update_version.get('hash') + '.tar.gz')
extracted_path = os.path.join(Env.get('cache_dir'), 'temp_updater')
destination = fireEvent('file.download', url = url, dest = destination, single = True)
# Cleanup leftover from last time
if os.path.isdir(extracted_path):
self.removeDir(extracted_path)
self.makeDir(extracted_path)
# Extract
tar = tarfile.open(destination)
tar.extractall(path = extracted_path)
os.remove(destination)
self.replaceWith(os.path.join(extracted_path, os.listdir(extracted_path)[0]))
self.removeDir(extracted_path)
# Write update version to file
self.createFile(self.version_file, json.dumps(self.update_version))
return True
except:
log.error('Failed updating: %s' % traceback.format_exc())
self.update_failed = True
return False
def replaceWith(self, path):
app_dir = Env.get('app_dir')
# Get list of files we want to overwrite
self.deletePyc(only_excess = False)
existing_files = []
for root, subfiles, filenames in os.walk(app_dir):
for filename in filenames:
existing_files.append(os.path.join(root, filename))
for root, subfiles, filenames in os.walk(path):
for filename in filenames:
fromfile = os.path.join(root, filename)
tofile = os.path.join(app_dir, fromfile.replace(path + os.path.sep, ''))
if not Env.get('dev'):
try:
os.rmdir(full_path)
os.remove(tofile)
except:
log.error('Couldn\'t remove empty directory %s: %s' % (full_path, traceback.format_exc()))
pass
def isEnabled(self):
return super(Updater, self).isEnabled() and Env.get('uses_git')
try:
os.renames(fromfile, tofile)
try:
existing_files.remove(tofile)
except ValueError:
pass
except Exception, e:
log.error('Failed overwriting file: %s' % e)
def removeDir(self, path):
try:
if os.path.isdir(path):
shutil.rmtree(path)
except OSError, inst:
os.chmod(inst.filename, 0777)
self.removeDir(path)
def getVersion(self):
if not self.version:
try:
f = open(self.version_file, 'r')
output = json.loads(f.read())
f.close()
log.debug('Source version output: %s' % output)
self.version = output
self.version['type'] = 'source'
except Exception, e:
log.error('Failed using source updater. %s' % e)
return {}
return self.version
def check(self):
current_version = self.getVersion()
try:
latest = self.latestCommit()
if latest.get('hash') != current_version.get('hash') and latest.get('date') >= current_version.get('date'):
self.update_version = latest
self.last_check = time.time()
except:
log.error('Failed updating via source: %s' % traceback.format_exc())
return self.update_version is not None
def latestCommit(self):
try:
url = 'https://api.github.com/repos/%s/%s/commits?per_page=1&sha=%s' % (self.repo_user, self.repo_name, self.branch)
data = self.getCache('github.commit', url = url)
commit = json.loads(data)[0]
return {
'hash': commit['sha'],
'date': int(time.mktime(parse(commit['commit']['committer']['date']).timetuple())),
}
except:
log.error('Failed getting latest request from github: %s' % traceback.format_exc())
return {}
@@ -79,6 +79,8 @@ var UpdaterBase = new Class({
if(json.success){
App.restart('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
App.checkAvailable.delay(500, App);
if(self.message)
self.message.destroy();
}
}
});
+7
View File
@@ -40,3 +40,10 @@ class Downloader(Plugin):
log.debug("Downloader doesn't support this type")
return is_correct
def isDisabled(self, manual):
return not self.isEnabled(manual)
def isEnabled(self, manual):
d_manual = self.conf('manual', default = False)
return super(Downloader, self).isEnabled() and ((d_manual and manual) or (d_manual is False))
@@ -32,6 +32,13 @@ config = [{
'type': 'dropdown',
'values': [('nzbs & torrents', 'both'), ('nzb', 'nzb'), ('torrent', 'torrent')],
},
{
'name': 'manual',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
},
],
}
],
@@ -10,8 +10,8 @@ class Blackhole(Downloader):
type = ['nzb', 'torrent']
def download(self, data = {}, movie = {}):
if self.isDisabled() or (not self.isCorrectType(data.get('type')) or (not self.conf('use_for') in ['both', data.get('type')])):
def download(self, data = {}, movie = {}, manual = False):
if self.isDisabled(manual) or (not self.isCorrectType(data.get('type')) or (not self.conf('use_for') in ['both', data.get('type')])):
return
directory = self.conf('directory')
@@ -33,6 +33,13 @@ config = [{
'default': 'Movies',
'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
},
{
'name': 'manual',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
},
],
}
],
+2 -2
View File
@@ -14,9 +14,9 @@ class NZBGet(Downloader):
url = 'http://nzbget:%(password)s@%(host)s/xmlrpc'
def download(self, data = {}, movie = {}):
def download(self, data = {}, movie = {}, manual = False):
if self.isDisabled() or not self.isCorrectType(data.get('type')):
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
return
log.info('Sending "%s" to NZBGet.' % data.get('name'))
@@ -39,6 +39,13 @@ config = [{
'type': 'directory',
'description': 'Your Post-Processing Script directory, set in Sabnzbd > Config > Directories.',
},
{
'name': 'manual',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
},
],
}
],
+4 -4
View File
@@ -1,9 +1,9 @@
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog
from inspect import isfunction
from tempfile import mkstemp
from urllib import urlencode
import base64
import os
import re
@@ -15,9 +15,9 @@ class Sabnzbd(Downloader):
type = ['nzb']
def download(self, data = {}, movie = {}):
def download(self, data = {}, movie = {}, manual = False):
if self.isDisabled() or not self.isCorrectType(data.get('type')):
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
return
log.info("Sending '%s' to SABnzbd." % data.get('name'))
@@ -58,7 +58,7 @@ class Sabnzbd(Downloader):
if pp:
params['script'] = pp_script_fn
url = cleanHost(self.conf('host')) + "api?" + urlencode(params)
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
try:
if params.get('mode') is 'addfile':
@@ -48,6 +48,13 @@ config = [{
'advanced': True,
'description': 'Stop transfer when reaching ratio',
},
{
'name': 'manual',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
},
],
}
],
@@ -11,9 +11,9 @@ class Transmission(Downloader):
type = ['torrent']
def download(self, data = {}, movie = {}):
def download(self, data = {}, movie = {}, manual = False):
if self.isDisabled() or not self.isCorrectType(data.get('type')):
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
return
log.info('Sending "%s" to Transmission.' % data.get('name'))
+3 -1
View File
@@ -19,7 +19,7 @@ def addEvent(name, handler, priority = 100):
if events.get(name):
e = events[name]
else:
e = events[name] = Event(threads = 20, exc_info = True, traceback = True, lock = threading.RLock())
e = events[name] = Event(name = name, threads = 20, exc_info = True, traceback = True, lock = threading.RLock())
def createHandle(*args, **kwargs):
@@ -42,6 +42,8 @@ def removeEvent(name, handler):
e -= handler
def fireEvent(name, *args, **kwargs):
if not events.get(name): return
#log.debug('Firing event %s' % name)
try:
# Fire after event
+19 -2
View File
@@ -1,5 +1,6 @@
from couchpotato.core.logger import CPLog
from string import ascii_letters, digits
from urllib import quote_plus
import re
import unicodedata
@@ -19,7 +20,7 @@ def simplifyString(original):
def toUnicode(original, *args):
try:
if type(original) is unicode:
if isinstance(original, unicode):
return original
else:
try:
@@ -35,7 +36,7 @@ def toUnicode(original, *args):
return toUnicode(ascii_text)
def ek(original, *args):
if type(original) in [str, unicode]:
if isinstance(original, (str, unicode)):
try:
from couchpotato.environment import Env
return original.decode(Env.get('encoding'))
@@ -53,3 +54,19 @@ def isInt(value):
def stripAccents(s):
return ''.join((c for c in unicodedata.normalize('NFD', toUnicode(s)) if unicodedata.category(c) != 'Mn'))
def tryUrlencode(s):
new = u''
if isinstance(s, (dict)):
for key, value in s.iteritems():
new += u'&%s=%s' % (key, tryUrlencode(value))
return new[1:]
else:
for letter in toUnicode(s):
try:
new += quote_plus(letter)
except:
new += letter
return new
+4 -3
View File
@@ -1,3 +1,4 @@
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import natcmp
from flask.globals import current_app
from flask.helpers import json
@@ -25,7 +26,7 @@ def getParams():
for item in nested:
if item is nested[-1]:
current[item] = unquote_plus(value)
current[item] = toUnicode(unquote_plus(value)).encode('utf-8')
else:
try:
current[item]
@@ -34,7 +35,7 @@ def getParams():
current = current[item]
else:
temp[param] = unquote_plus(value)
temp[param] = toUnicode(unquote_plus(value)).encode('utf-8')
return dictToList(temp)
@@ -56,7 +57,7 @@ def dictToList(params):
def getParam(attr, default = None):
try:
return unquote_plus(getattr(flask.request, 'args').get(attr, default))
return toUnicode(unquote_plus(getattr(flask.request, 'args').get(attr, default))).encode('utf-8')
except:
return None
+10
View File
@@ -9,6 +9,8 @@ def getDataDir():
if os.name == 'nt':
return os.path.join(os.environ['APPDATA'], 'CouchPotato')
import pwd
os.environ['HOME'] = pwd.getpwuid(os.geteuid()).pw_dir
user_dir = os.path.expanduser('~')
# OSX
@@ -36,10 +38,18 @@ def mergeDicts(a, b):
stack.append((current_dst[key], current_src[key]))
elif isinstance(current_src[key], list) and isinstance(current_dst[key], list):
current_dst[key].extend(current_src[key])
current_dst[key] = removeListDuplicates(current_dst[key])
else:
current_dst[key] = current_src[key]
return dst
def removeListDuplicates(seq):
checked = []
for e in seq:
if e not in checked:
checked.append(e)
return checked
def flattenList(l):
if isinstance(l, list):
return sum(map(flattenList, l))
+2 -2
View File
@@ -56,7 +56,7 @@ class CoreNotifier(Notification):
addEvent('library.update_finish', lambda data: fireEvent('notify.frontend', type = 'library.update', data = data))
def markAsRead(self):
ids = getParam('ids').split(',')
ids = [x.strip() for x in getParam('ids').split(',')]
db = get_session()
@@ -78,7 +78,7 @@ class CoreNotifier(Notification):
q = db.query(Notif)
if limit_offset:
splt = limit_offset.split(',')
splt = [x.strip() for x in limit_offset.split(',')]
limit = splt[0]
offset = 0 if len(splt) is 1 else splt[1]
q = q.limit(limit).offset(offset)
+2 -3
View File
@@ -1,12 +1,11 @@
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.request import getParams, jsonified
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from couchpotato.environment import Env
import re
import telnetlib
import urllib
try:
import xml.etree.cElementTree as etree
@@ -90,7 +89,7 @@ class NMJ(Notification):
'arg2': 'background',
'arg3': '',
}
params = urllib.urlencode(params)
params = tryUrlencode(params)
UPDATE_URL = 'http://%(host)s:8008/metadata_database?%(params)s'
updateUrl = UPDATE_URL % {'host': host, 'params': params}
@@ -11,7 +11,7 @@ class NotifyMyAndroid(Notification):
if self.isDisabled(): return
nma = pynma.PyNMA()
keys = self.conf('api_key').split(',')
keys = [x.strip() for x in self.conf('api_key').split(',')]
nma.addkey(keys)
nma.developerkey(self.conf('dev_key'))
@@ -10,7 +10,7 @@ class NotifyMyWP(Notification):
def notify(self, message = '', data = {}):
if self.isDisabled(): return
keys = self.conf('api_key').split(',')
keys = [x.strip() for x in self.conf('api_key').split(',')]
p = PyNMWP(keys, self.conf('dev_key'))
response = p.push(application = self.default_title, event = message, description = message, priority = self.conf('priority'), batch_mode = len(keys) > 1)
+2 -3
View File
@@ -1,8 +1,7 @@
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from httplib import HTTPSConnection
from urllib import urlencode
log = CPLog(__name__)
@@ -24,7 +23,7 @@ class Prowl(Notification):
http_handler.request('POST',
'/publicapi/add',
headers = {'Content-type': 'application/x-www-form-urlencoded'},
body = urlencode(data)
body = tryUrlencode(data)
)
response = http_handler.getresponse()
request_status = response.status
@@ -0,0 +1,38 @@
from .main import Pushover
def start():
return Pushover()
config = [{
'name': 'pushover',
'groups': [
{
'tab': 'notifications',
'name': 'pushover',
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
},
{
'name': 'user_key',
'description': 'Register on pushover.net to get one.'
},
{
'name': 'priority',
'default': 0,
'type': 'dropdown',
'values': [('Normal', 0), ('High', 1)],
},
{
'name': 'on_snatch',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Also send message when movie is snatched.',
},
],
}
],
}]
@@ -0,0 +1,42 @@
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from httplib import HTTPSConnection
log = CPLog(__name__)
class Pushover(Notification):
app_token = 'YkxHMYDZp285L265L3IwH3LmzkTaCy'
def notify(self, message = '', data = {}):
if self.isDisabled(): return
http_handler = HTTPSConnection("api.pushover.net:443")
data = {
'user': self.conf('user_key'),
'token': self.app_token,
'message': toUnicode(message),
'priority': self.conf('priority')
}
http_handler.request('POST',
"/1/messages.json",
headers = {'Content-type': 'application/x-www-form-urlencoded'},
body = tryUrlencode(data)
)
response = http_handler.getresponse()
request_status = response.status
if request_status == 200:
log.info('Pushover notifications sent.')
return True
elif request_status == 401:
log.error('Pushover auth failed: %s' % response.reason)
return False
else:
log.error('Pushover notification failed.')
return False
@@ -1,4 +1,5 @@
from couchpotato.api import addApiView
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog
@@ -7,7 +8,6 @@ from flask.helpers import url_for
from pytwitter import Api, parse_qsl
from werkzeug.utils import redirect
import oauth2
import urllib
log = CPLog(__name__)
@@ -56,7 +56,7 @@ class Twitter(Notification):
oauth_consumer = oauth2.Consumer(self.consumer_key, self.consumer_secret)
oauth_client = oauth2.Client(oauth_consumer)
resp, content = oauth_client.request(self.urls['request'], 'POST', body = urllib.urlencode({'oauth_callback': callback_url}))
resp, content = oauth_client.request(self.urls['request'], 'POST', body = tryUrlencode({'oauth_callback': callback_url}))
if resp['status'] != '200':
log.error('Invalid response from Twitter requesting temp token: %s' % resp['status'])
+2 -2
View File
@@ -1,7 +1,7 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
import base64
import urllib
log = CPLog(__name__)
@@ -21,7 +21,7 @@ class XBMC(Notification):
def send(self, command, host):
url = 'http://%s/xbmcCmds/xbmcHttp/?%s' % (host, urllib.urlencode(command))
url = 'http://%s/xbmcCmds/xbmcHttp/?%s' % (host, tryUrlencode(command))
headers = {}
if self.conf('password'):
@@ -16,21 +16,27 @@ config = [{
{
'name': 'year',
'default': 2011,
'Label': 'Year',
'type': 'int',
},
{
'name': 'votes',
'default': 1000,
'Label': 'Votes',
'type': 'int',
},
{
'name': 'rating',
'default': 6.0,
'Label': 'Rating',
'type': 'float',
},
{
'name': 'hour',
'advanced': True,
'default': 12,
'label': 'Check every',
'type': 'int',
'unit': 'hours',
'description': 'hours',
},
],
},
],
@@ -10,6 +10,8 @@ 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)
+31 -4
View File
@@ -1,5 +1,6 @@
from couchpotato import addView
from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import getExt
from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
@@ -11,10 +12,8 @@ import glob
import math
import os.path
import re
import socket
import time
import traceback
import urllib
import urllib2
log = CPLog(__name__)
@@ -29,6 +28,8 @@ class Plugin(object):
http_last_use = {}
http_time_between_calls = 0
http_failed_request = {}
http_failed_disabled = {}
def registerPlugin(self):
addEvent('app.shutdown', self.doShutdown)
@@ -101,8 +102,17 @@ class Plugin(object):
headers['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:10.0.2) Gecko/20100101 Firefox/10.0.2'
host = urlparse(url).hostname
self.wait(host)
# 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.info('Disabled calls to %s for 15 minutes because so many failed requests.' % host)
raise Exception
else:
del self.http_failed_request[host]
del self.http_failed_disabled[host]
self.wait(host)
try:
if multipart:
@@ -115,13 +125,30 @@ class Plugin(object):
data = opener.open(request, timeout = timeout).read()
else:
log.info('Opening url: %s, params: %s' % (url, [x for x in params.iterkeys()]))
data = urllib.urlencode(params) if len(params) > 0 else None
data = tryUrlencode(params) if len(params) > 0 else None
request = urllib2.Request(url, data, headers)
data = urllib2.urlopen(request, timeout = timeout).read()
self.http_failed_request[host] = 0
except IOError:
if show_error:
log.error('Failed opening url in %s: %s %s' % (self.getName(), url, traceback.format_exc(1)))
# Save failed requests by hosts
try:
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:
self.http_failed_disabled[host] = time.time()
except:
log.debug('Failed logging failed requests for %s: %s' % (url, traceback.format_exc()))
raise
self.http_last_use[host] = time.time()
+24 -3
View File
@@ -1,10 +1,12 @@
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__)
@@ -16,6 +18,8 @@ class LibraryPlugin(Plugin):
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):
@@ -45,7 +49,7 @@ class LibraryPlugin(Plugin):
# 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 = attrs.get('title', ''))
handle('library.update', identifier = l.identifier, default_title = toUnicode(attrs.get('title', '')))
return l.to_dict(self.default_dict)
@@ -86,10 +90,11 @@ class LibraryPlugin(Plugin):
for title in titles:
if not title:
continue
title = toUnicode(title)
t = LibraryTitle(
title = toUnicode(title),
title = title,
simple_title = self.simplifyTitle(title),
default = title.lower() == default_title.lower() or (default_title is '' and titles[0] == title)
default = title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == u'' and toUnicode(titles[0]) == title)
)
library.titles.append(t)
@@ -117,6 +122,22 @@ class LibraryPlugin(Plugin):
return library_dict
def updateReleaseDate(self, identifier):
db = get_session()
library = db.query(Library).filter_by(identifier = identifier).first()
if library.info.get('release_date', {}).get('expires', 0) < time.time():
dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
library.info = mergeDicts(library.info, {'release_date': dates})
db.commit()
dates = library.info.get('release_date', {})
db.remove()
return dates
def simplifyTitle(self, title):
title = toUnicode(title)
+19 -17
View File
@@ -1,10 +1,8 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
from couchpotato.core.helpers.request import jsonified, getParams
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import File
from couchpotato.environment import Env
import os
import time
@@ -14,8 +12,6 @@ log = CPLog(__name__)
class Manage(Plugin):
last_update = 0
def __init__(self):
fireEvent('scheduler.interval', identifier = 'manage.update_library', handle = self.updateLibrary, hours = 2)
@@ -42,12 +38,14 @@ class Manage(Plugin):
})
def updateLibrary(self, full = False):
def updateLibrary(self, full = True):
last_update = float(Env.prop('manage.last_update', default = 0))
if self.isDisabled() or (self.last_update > time.time() - 20):
if self.isDisabled() or (last_update > time.time() - 20):
return
directories = self.directories()
added_identifiers = []
for directory in directories:
@@ -57,24 +55,28 @@ class Manage(Plugin):
continue
log.info('Updating manage library: %s' % directory)
fireEvent('scanner.folder', folder = directory)
# If cleanup option is enabled, remove offline files from database
if self.conf('cleanup'):
db = get_session()
files_in_path = db.query(File).filter(File.path.like(directory + '%%')).filter_by(available = 0).all()
[db.delete(x) for x in files_in_path]
db.commit()
db.remove()
identifiers = fireEvent('scanner.folder', folder = directory, newer_than = last_update, single = True)
if identifiers:
added_identifiers.extend(identifiers)
# Break if CP wants to shut down
if self.shuttingDown():
break
self.last_update = time.time()
# 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
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'])
Env.prop('manage.last_update', time.time())
def directories(self):
try:
return self.conf('library', default = '').split('::')
return [x.strip() for x in self.conf('library', default = '').split('::')]
except:
return []
+41 -30
View File
@@ -1,8 +1,10 @@
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
from couchpotato.core.helpers.request import getParams, jsonified
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode, \
simplifyString
from couchpotato.core.helpers.request import getParams, jsonified, getParam
from couchpotato.core.helpers.variable import getImdb
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie, Library, LibraryTitle
@@ -10,7 +12,6 @@ from couchpotato.environment import Env
from sqlalchemy.orm import joinedload_all
from sqlalchemy.sql.expression import or_, asc, not_
from string import ascii_lowercase
from urllib import urlencode
log = CPLog(__name__)
@@ -74,7 +75,7 @@ class MoviePlugin(Plugin):
'default_title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'},
}
})
addApiView('movie.delete', self.delete, docs = {
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)'},
@@ -82,6 +83,7 @@ class MoviePlugin(Plugin):
})
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)
@@ -137,7 +139,7 @@ class MoviePlugin(Plugin):
if limit_offset:
splt = limit_offset.split(',')
splt = [x.strip() for x in limit_offset.split(',')]
limit = splt[0]
offset = 0 if len(splt) is 1 else splt[1]
q2 = q2.limit(limit).offset(offset)
@@ -224,7 +226,6 @@ class MoviePlugin(Plugin):
if title.default: default_title = title.title
if movie:
#addEvent('library.update.after', )
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True)
fireEventAsync('searcher.single', movie.to_dict(self.default_dict))
@@ -234,12 +235,16 @@ class MoviePlugin(Plugin):
def search(self):
params = getParams()
cache_key = '%s/%s' % (__name__, urlencode(params))
q = getParam('q')
cache_key = u'%s/%s' % (__name__, simplifyString(q))
movies = Env.get('cache').get(cache_key)
if not movies:
movies = fireEvent('movie.search', q = params.get('q'), merge = True)
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({
@@ -264,7 +269,8 @@ class MoviePlugin(Plugin):
if not m:
m = Movie(
library_id = library.get('id'),
profile_id = params.get('profile_id', default_profile.get('id'))
profile_id = params.get('profile_id', default_profile.get('id')),
status_id = status_active.get('id'),
)
db.add(m)
fireEvent('library.update', params.get('identifier'), default_title = params.get('title', ''))
@@ -318,7 +324,7 @@ class MoviePlugin(Plugin):
available_status = fireEvent('status.get', 'available', single = True)
ids = params.get('id').split(',')
ids = [x.strip() for x in params.get('id').split(',')]
for movie_id in ids:
m = db.query(Movie).filter_by(id = movie_id).first()
@@ -333,7 +339,7 @@ class MoviePlugin(Plugin):
# Default title
if params.get('default_title'):
for title in m.library.titles:
title.default = params.get('default_title').lower() == title.title.lower()
title.default = toUnicode(params.get('default_title', '')).lower() == toUnicode(title.title).lower()
db.commit()
@@ -346,23 +352,29 @@ class MoviePlugin(Plugin):
'success': True,
})
def delete(self):
def deleteView(self):
params = getParams()
db = get_session()
status = fireEvent('status.add', 'deleted', single = True)
ids = params.get('id').split(',')
ids = [x.strip() for x in params.get('id').split(',')]
for movie_id in ids:
movie = db.query(Movie).filter_by(id = movie_id).first()
movie.status_id = status.get('id')
db.commit()
self.delete(movie_id)
return jsonified({
'success': True,
})
def delete(self, movie_id):
db = get_session()
movie = db.query(Movie).filter_by(id = movie_id).first()
if movie:
db.delete(movie)
db.commit()
return True
def restatus(self, movie_id):
active_status = fireEvent('status.get', 'active', single = True)
@@ -372,18 +384,17 @@ class MoviePlugin(Plugin):
m = db.query(Movie).filter_by(id = movie_id).first()
if not m.profile:
return
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
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
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')
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
db.commit()
@@ -279,10 +279,11 @@
border: 0;
}
.movies .options .table .provider {
width: 130px;
width: 120px;
text-overflow: ellipsis;
}
.movies .options .table .name {
width: 370px;
width: 350px;
overflow: hidden;
text-align: left;
padding: 0 10px;
@@ -290,6 +291,8 @@
.movies .options .table.files .name { width: 605px; }
.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;
+15 -4
View File
@@ -79,7 +79,7 @@ var Movie = new Class({
var q = self.quality.getElement('.q_id'+ release.quality_id),
status = Status.get(release.status_id);
if(!q && status.identifier == 'snatched')
if(!q && (status.identifier == 'snatched' || status.identifier == 'done'))
var q = self.addQuality(release.quality_id)
if (q)
q.addClass(status.identifier);
@@ -270,8 +270,9 @@ var ReleaseAction = new Class({
// 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 (MB)'}),
new Element('span.size', {'text': 'Size'}),
new Element('span.age', {'text': 'Age'}),
new Element('span.score', {'text': 'Score'}),
new Element('span.provider', {'text': 'Provider'})
@@ -280,17 +281,27 @@ var ReleaseAction = new Class({
Array.each(self.movie.data.releases, function(release){
var status = Status.get(release.status_id),
quality = Quality.getProfile(release.quality_id)
quality = Quality.getProfile(release.quality_id),
info = release.info;
try {
var details_url = info.filter(function(item){ return item.identifier == 'detail_url' }).pick().value;
} catch(e){}
new Element('div', {
'class': 'item ' + status.identifier
'class': 'item'
}).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')}),
new Element('span.size', {'text': (self.get(release, 'size') || 'unknown')}),
new Element('span.age', {'text': self.get(release, 'age')}),
new Element('span.score', {'text': self.get(release, 'score')}),
new Element('span.provider', {'text': self.get(release, 'provider')}),
details_url ? new Element('a.info.icon', {
'href': details_url,
'target': '_blank'
}) : null,
new Element('a.download.icon', {
'events': {
'click': function(e){
@@ -148,6 +148,9 @@ Block.Search = new Class({
$(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())
@@ -158,7 +161,7 @@ Block.Search = new Class({
rc = self.result_container.getCoordinates();
self.results.setStyle('max-height', (w.y - rc.top - 50) + 'px')
self.mask.hide()
self.mask.fade('out')
},
@@ -367,12 +370,13 @@ Block.Search.Item = new Class({
self.mask = new Element('span.mask', {
'styles': {
'position': 'relative',
'width': s.x,
'height': s.y
'height': s.y,
'top': -s.y,
'display': 'block'
}
}).inject(self.options).fade('hide').position({
'relativeTo': self.options
})
}).inject(self.options).fade('hide')
createSpinner(self.mask)
self.mask.fade('in')
+14
View File
@@ -21,9 +21,23 @@ class ProfilePlugin(Plugin):
addApiView('profile.save', self.save)
addApiView('profile.save_order', self.saveOrder)
addApiView('profile.delete', self.delete)
addApiView('profile.list', self.allView, docs = {
'desc': 'List all available profiles',
'return': {'type': 'object', 'example': """{
'success': True,
'list': array, profiles
}"""}
})
addEvent('app.initialize', self.fill, priority = 90)
def allView(self):
return jsonified({
'success': True,
'list': self.all()
})
def all(self):
db = get_session()
+24 -5
View File
@@ -1,7 +1,7 @@
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.encoding import toUnicode, toSafeString
from couchpotato.core.helpers.request import jsonified, getParams
from couchpotato.core.helpers.variable import mergeDicts, md5, getExt
from couchpotato.core.logger import CPLog
@@ -16,9 +16,9 @@ log = CPLog(__name__)
class QualityPlugin(Plugin):
qualities = [
{'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'width': 1920, 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]},
{'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, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts']},
{'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts']},
{'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts', 'ts']},
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], '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', 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
@@ -34,11 +34,29 @@ class QualityPlugin(Plugin):
addEvent('quality.all', self.all)
addEvent('quality.single', self.single)
addEvent('quality.guess', self.guess)
addEvent('quality.pre_releases', self.preReleases)
addApiView('quality.size.save', self.saveSize)
addApiView('quality.list', self.allView, docs = {
'desc': 'List all available qualities',
'return': {'type': 'object', 'example': """{
'success': True,
'list': array, qualities
}"""}
})
addEvent('app.initialize', self.fill, priority = 10)
def preReleases(self):
return self.pre_releases
def allView(self):
return jsonified({
'success': True,
'list': self.all()
})
def all(self):
db = get_session()
@@ -138,7 +156,7 @@ class QualityPlugin(Plugin):
# Create hash for cache
hash = md5(str([f.replace('.' + getExt(f), '') for f in files]))
cached = self.getCache(hash)
if cached: return cached
if cached and extra is {}: return cached
for cur_file in files:
size = (os.path.getsize(cur_file) / 1024 / 1024) if os.path.isfile(cur_file) else 0
@@ -157,6 +175,7 @@ class QualityPlugin(Plugin):
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)
if list(set(quality.get('tags', [])) & set(words)):
@@ -182,5 +201,5 @@ class QualityPlugin(Plugin):
if quality:
return self.setCache(hash, quality)
log.error('Could not identify quality for: %s' % files)
log.debug('Could not identify quality for: %s' % files)
return self.setCache(hash, self.single('dvdrip'))
+1 -1
View File
@@ -147,7 +147,7 @@ class Release(Plugin):
'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files':{}},
'files': {}
}))
}), manual = True)
return jsonified({
'success': True
+17 -4
View File
@@ -192,11 +192,16 @@ class Renamer(Plugin):
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))
else:
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':
# rename subtitles with or without language
#rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
sub_langs = group['subtitle_language'].get(current_file, [])
rename_extras = self.getRenameExtras(
extra_type = 'subtitle_extra',
replacements = replacements,
@@ -206,6 +211,14 @@ class Renamer(Plugin):
group = group,
current_file = current_file
)
# Don't add language if multiple languages in 1 file
if len(sub_langs) > 1:
rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
elif 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
@@ -315,7 +328,7 @@ class Renamer(Plugin):
try:
os.remove(src)
except:
log.error('Failed removing %s: %s', (src, traceback.format_exc()))
log.error('Failed removing %s: %s' % (src, traceback.format_exc()))
# Remove matching releases
for release in remove_releases:
@@ -323,14 +336,14 @@ class Renamer(Plugin):
try:
db.delete(release)
except:
log.error('Failed removing %s: %s', (release.identifier, traceback.format_exc()))
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()))
log.error('Failed removing %s: %s' % (group['parentdir'], traceback.format_exc()))
# Search for trailers etc
fireEventAsync('renamer.after', group)
+86 -24
View File
@@ -7,6 +7,8 @@ from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import File
from couchpotato.environment import Env
from enzyme.exceptions import NoParserError, ParseError
from guessit import guess_movie_info
from subliminal.videos import scan
import enzyme
import logging
import os
@@ -29,7 +31,7 @@ class Scanner(Plugin):
ignored_in_path = ['_unpack', '_failed_', '_unknown_', '_exists_', '.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'],
'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img', 'mdf', 'ts'],
'movie_extra': ['mds'],
'dvd': ['vts_*', 'vob'],
'nfo': ['nfo', 'txt', 'tag'],
@@ -95,6 +97,10 @@ class Scanner(Plugin):
addEvent('rename.after', after_rename)
# Disable lib logging
logging.getLogger('guessit').setLevel(logging.ERROR)
logging.getLogger('subliminal').setLevel(logging.ERROR)
def scanFilesToLibrary(self, folder = None, files = None):
groups = self.scan(folder = folder, files = files)
@@ -103,21 +109,14 @@ class Scanner(Plugin):
if group['library']:
fireEvent('release.add', group = group)
def scanFolderToLibrary(self, folder = None, newer_as = None):
def scanFolderToLibrary(self, folder = None, newer_than = None):
if not os.path.isdir(folder):
return
groups = self.scan(folder = folder)
# Open up the db
db = get_session()
# Mark all files as "offline" before a adding them to the database (again)
files_in_path = db.query(File).filter(File.path.like(toUnicode(folder) + u'%%'))
files_in_path.update({'available': 0}, synchronize_session = False)
db.commit()
added_identifier = []
while True and not self.shuttingDown():
try:
identifier, group = groups.popitem()
@@ -129,9 +128,11 @@ class Scanner(Plugin):
# Add release
fireEvent('release.add', group = group)
fireEvent('library.update', identifier = group['library'].get('identifier'))
library_item = fireEvent('library.update', identifier = group['library'].get('identifier'), single = True)
if library_item:
added_identifier.append(library_item['identifier'])
db.remove()
return added_identifier
def scan(self, folder = None, files = []):
@@ -164,6 +165,9 @@ class Scanner(Plugin):
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)
@@ -178,7 +182,7 @@ class Scanner(Plugin):
identifier = self.createStringIdentifier(file_path, folder, exclude_filename = is_dvd_file)
# Identifier with quality
quality = fireEvent('quality.guess', [file_path], single = True)
quality = fireEvent('quality.guess', [file_path], single = True) if not is_dvd_file else {'identifier':'dvdr'}
identifier_with_quality = '%s %s' % (identifier, quality.get('identifier', ''))
if not movie_files.get(identifier):
@@ -208,7 +212,7 @@ class Scanner(Plugin):
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 for: %s' % identifier)
log.debug('Grouping files: %s' % identifier)
for file_path in group['unsorted_files']:
wo_ext = file_path[:-(len(getExt(file_path)) + 1)]
@@ -233,7 +237,7 @@ class Scanner(Plugin):
# Group the files based on the identifier
for identifier, group in movie_files.iteritems():
log.debug('Grouping files for: %s' % identifier)
log.debug('Grouping files on identifier: %s' % identifier)
found_files = set(self.path_identifiers.get(identifier, []))
group['unsorted_files'].extend(found_files)
@@ -291,6 +295,9 @@ class Scanner(Plugin):
log.debug('Getting metadata for %s' % identifier)
group['meta_data'] = self.getMetaData(group)
# Subtitle meta
group['subtitle_language'] = self.getSubtitleLanguage(group)
# Get parent dir from movie files
for movie_file in group['files']['movie']:
group['parentdir'] = os.path.dirname(movie_file)
@@ -381,6 +388,42 @@ class Scanner(Plugin):
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']:
scan_result.extend(scan(p))
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
@@ -433,12 +476,17 @@ class Scanner(Plugin):
for identifier in group['identifiers']:
if len(identifier) > 2:
movie = fireEvent('movie.search', q = '%(name)s %(year)s' % self.getReleaseNameYear(identifier), merge = True, limit = 1)
try: filename = list(group['files'].get('movie'))[0]
except: filename = None
if len(movie) > 0:
imdb_id = movie[0]['imdb']
log.debug('Found movie via search: %s' % cur_file)
if imdb_id: break
name_year = self.getReleaseNameYear(identifier, file_name = filename)
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)
@@ -645,13 +693,27 @@ class Scanner(Plugin):
return None
def findYear(self, text):
matches = re.search('(?P<year>[0-9]{4})', text)
matches = re.search('(?P<year>[12]{1}[0-9]{3})', text)
if matches:
return matches.group('year')
return ''
def getReleaseNameYear(self, release_name):
def getReleaseNameYear(self, release_name, file_name = None):
# Use guessit first
if file_name:
try:
guess = guess_movie_info(file_name)
if guess.get('title') and guess.get('year'):
return {
'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)
@@ -661,7 +723,7 @@ class Scanner(Plugin):
movie_name = cleaned.split(year).pop(0).strip()
return {
'name': movie_name,
'year': year,
'year': int(year),
}
except:
pass
@@ -670,7 +732,7 @@ class Scanner(Plugin):
movie_name = cleaned.split(' ').pop(0).strip()
return {
'name': movie_name,
'year': year,
'year': int(year),
}
except:
pass
+1 -1
View File
@@ -40,7 +40,7 @@ def nameScore(name, year):
# Contains preferred word
nzb_words = re.split('\W+', simplifyString(name))
preferred_words = Env.setting('preferred_words', section = 'searcher').split(',')
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
@@ -77,7 +77,7 @@ config = [{
'options': [
{
'name': 'retention',
'default': 350,
'default': 1000,
'type': 'int',
'unit': 'days'
},
+78 -11
View File
@@ -7,7 +7,9 @@ from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
from couchpotato.environment import Env
from sqlalchemy.exc import InterfaceError
import datetime
import re
import time
import traceback
log = CPLog(__name__)
@@ -15,6 +17,8 @@ log = CPLog(__name__)
class Searcher(Plugin):
in_progress = False
def __init__(self):
addEvent('searcher.all', self.all_movies)
addEvent('searcher.single', self.single)
@@ -26,6 +30,12 @@ class Searcher(Plugin):
def all_movies(self):
if self.in_progress:
log.info('Search already in progress')
return
self.in_progress = True
db = get_session()
movies = db.query(Movie).filter(
@@ -51,12 +61,19 @@ class Searcher(Plugin):
if self.shuttingDown():
break
self.in_progress = False
def single(self, movie):
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)
default_title = movie['library']['titles'][0]['title']
for quality_type in movie['profile']['types']:
if not self.couldBeReleased(quality_type['quality']['identifier'], release_dates, pre_releases):
log.info('To early to search for %s, %s' % (quality_type['quality']['identifier'], default_title))
continue
has_better_quality = 0
@@ -72,6 +89,8 @@ class Searcher(Plugin):
quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True)
results = fireEvent('yarr.search', movie, quality, merge = True)
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']))
# Add them to this movie releases list
for nzb in sorted_results:
@@ -104,7 +123,11 @@ class Searcher(Plugin):
for nzb in sorted_results:
return self.download(data = nzb, movie = movie)
downloaded = self.download(data = nzb, movie = movie)
if downloaded:
return True
else:
break
else:
log.info('Better quality (%s) already available or snatched for %s' % (quality_type['quality']['label'], default_title))
fireEvent('movie.restatus', movie['id'])
@@ -116,10 +139,10 @@ class Searcher(Plugin):
return False
def download(self, data, movie):
def download(self, data, movie, manual = False):
snatched_status = fireEvent('status.get', 'snatched', single = True)
successful = fireEvent('download', data = data, movie = movie, single = True)
successful = fireEvent('download', data = data, movie = movie, manual = manual, single = True)
if successful:
@@ -171,24 +194,31 @@ class Searcher(Plugin):
log.info('Wrong: Outside retention, age is %s, needs %s or lower: %s' % (nzb['age'], retention, nzb['name']))
return False
nzb_words = re.split('\W+', simplifyString(nzb['name']))
required_words = self.conf('required_words').split(',')
movie_name = simplifyString(nzb['name'])
nzb_words = re.split('\W+', movie_name)
required_words = [x.strip() for x in self.conf('required_words').split(',')]
if self.conf('required_words') and not list(set(nzb_words) & set(required_words)):
log.info("NZB doesn't contain any of the required words.")
return False
ignored_words = self.conf('ignored_words').split(',')
ignored_words = [x.strip() for x in self.conf('ignored_words').split(',')]
blacklisted = list(set(nzb_words) & set(ignored_words))
if self.conf('ignored_words') and blacklisted:
log.info("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted)))
return False
pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs']
for p_tag in pron_tags:
if p_tag in movie_name:
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['name'], preferred_quality, single_category):
if self.containsOtherQuality(nzb, movie_year = movie['library']['year'], preferred_quality = preferred_quality, single_category = single_category):
log.info('Wrong: %s, looking for %s' % (nzb['name'], quality['label']))
return False
@@ -230,8 +260,10 @@ class Searcher(Plugin):
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie['library']['titles'][0]['title'], movie['library']['year']))
return False
def containsOtherQuality(self, name, preferred_quality = {}, single_category = False):
def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = {}, single_category = False):
name = nzb['name']
size = nzb.get('size', 0)
nzb_words = re.split('\W+', simplifyString(name))
qualities = fireEvent('quality.all', single = True)
@@ -246,6 +278,14 @@ class Searcher(Plugin):
if list(set(nzb_words) & set(quality['alternative'])):
found[quality['identifier']] = True
# Hack for older movies that don't contain quality tag
year_name = fireEvent('scanner.name_year', name, single = True)
if movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None):
if size > 3000: # Assume dvdr
return 'dvdr' == preferred_quality['identifier']
else: # Assume dvdrip
return 'dvdrip' == preferred_quality['identifier']
# Allow other qualities
for allowed in preferred_quality.get('allow'):
if found.get(allowed):
@@ -284,10 +324,10 @@ class Searcher(Plugin):
check_movie = fireEvent('scanner.name_year', check_name, single = True)
try:
check_words = re.split('\W+', check_movie.get('name', ''))
movie_words = re.split('\W+', simplifyString(movie_name))
check_words = filter(None, re.split('\W+', check_movie.get('name', '')))
movie_words = filter(None, re.split('\W+', simplifyString(movie_name)))
if len(list(set(check_words) - set(movie_words))) == 0:
if len(check_words) > 0 and len(movie_words) > 0 and len(list(set(check_words) - set(movie_words))) == 0:
return True
except:
pass
@@ -306,3 +346,30 @@ class Searcher(Plugin):
pass
return nfo and getImdb(nfo) == imdb_id
def couldBeReleased(self, wanted_quality, dates, pre_releases):
now = int(time.time())
if not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0):
return True
else:
if wanted_quality in pre_releases:
# Prerelease 1 week before theaters
if dates.get('theater') >= now - 604800 and wanted_quality in pre_releases:
return True
else:
# 6 weeks after theater release
if dates.get('theater') < now - 3628800:
return True
# 6 weeks before dvd release
if dates.get('dvd') > now - 3628800:
return True
# Dvd should be released
if dates.get('dvd') > 0 and dates.get('dvd') < now:
return True
return False
@@ -20,14 +20,14 @@ config = [{
},
{
'name': 'languages',
'description': 'The languages you want to download the sub',
},
{
'name': 'automatic',
'default': True,
'type': 'bool',
'description': 'Automaticly search & download for movies in library',
'description': 'Comma separated, 2 letter country code. Example: en, nl',
},
# {
# 'name': 'automatic',
# 'default': True,
# 'type': 'bool',
# 'description': 'Automaticly search & download for movies in library',
# },
],
},
],
+16 -11
View File
@@ -3,15 +3,18 @@ from couchpotato.core.event import addEvent, fireEvent
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
log = CPLog(__name__)
class Subtitle(Plugin):
services = ['opensubtitles', 'thesubdb', 'subswiki']
def __init__(self):
pass
#addEvent('renamer.before', self.searchSingle)
addEvent('renamer.before', self.searchSingle)
def searchLibrary(self):
@@ -33,20 +36,22 @@ class Subtitle(Plugin):
files.append(file.path);
# get subtitles for those files
subtitles = fireEvent('subtitle.search', files = files, languages = self.getLanguages(), merge = True)
# do something with the returned subtitles
print subtitles
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
subtitles = fireEvent('subtitle.search', files = group['files']['movie'], languages = self.getLanguages(), merge = True)
available_languages = sum(group['subtitle_language'].itervalues(), [])
downloaded = []
for lang in self.getLanguages():
if lang not in available_languages:
download = subliminal.download_subtitles(group['files']['movie'], multi = True, force = False, languages = [lang], services = self.services, cache_dir = Env.get('cache_dir'))
downloaded.extend(download)
# do something with the returned subtitles
print subtitles
for d_sub in downloaded:
group['files']['subtitle'].add(d_sub.path)
group['subtitle_language'][d_sub.path] = [d_sub.language]
def getLanguages(self):
return self.conf('languages').split(',')
return [x.strip() for x in self.conf('languages').split(',')]
@@ -58,7 +58,7 @@ if (typeof GM_addStyle == 'undefined'){
// Styles
GM_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:hover { } \
#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; } \
@@ -98,12 +98,14 @@ var osd = function(){
catch(e){}
popup.innerHTML = '';
popup.setAttribute('class', 'opened');
popup.appendChild(create('a', {
'innerHTML': '<img src="' + close_img + '" />',
'id': 'close_button',
'onclick': function(){
popup.innerHTML = '';
popup.appendChild(add_button);
popup.setAttribute('class', '');
}
}));
popup.appendChild(iframe)
@@ -10,7 +10,7 @@ config = [{
'tab': 'automation',
'name': 'imdb_automation',
'label': 'IMDB',
'description': 'From any public IMDB watchlists',
'description': 'From any <strong>public</strong> IMDB watchlists',
'options': [
{
'name': 'automation_enabled',
@@ -1,31 +0,0 @@
from .main import Kinepolis
def start():
return Kinepolis()
config = [{
'name': 'kinepolis',
'groups': [
{
'tab': 'automation',
'name': 'kinepolis_automation',
'label': 'Kinepolis',
'description': 'from <a href="http://kinepolis.be">Kinepolis</a>',
'options': [
{
'name': 'automation_enabled',
'default': False,
'type': 'enabler',
},
{
'name': 'automation_use_requirements',
'label': 'Use requirements',
'description': 'Use the minimal requirements set above',
'default': True,
'advanced': True,
'type': 'bool',
},
],
},
],
}]
@@ -1,41 +0,0 @@
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation
import xml.etree.ElementTree as XMLTree
log = CPLog(__name__)
class Kinepolis(Automation, RSS):
urls = {
'top10': 'http://kinepolis.be/nl/top10-box-office/feed',
}
def getIMDBids(self):
if self.isDisabled():
return
movies = []
for key in self.urls:
url = self.urls[key]
cache_key = 'kinepolis.%s' % key
rss_data = self.getCache(cache_key, url)
try:
items = self.getElements(XMLTree.fromstring(rss_data), 'channel/item')
except Exception, e:
log.debug('%s, %s' % (self.getName(), e))
continue
for item in items:
title = self.getTextElement(item, "title").lower()
result = self.search(title)
if result:
if not self.conf('automation_use_requirements') or self.isMinimal(result):
movies.append(result)
return movies
@@ -10,7 +10,7 @@ config = [{
'tab': 'automation',
'name': 'trakt_automation',
'label': 'Trakt',
'description': 'from watchlist',
'description': 'import movies from your own watchlist',
'options': [
{
'name': 'automation_enabled',
+7 -3
View File
@@ -19,7 +19,7 @@ class Provider(Plugin):
def isAvailable(self, test_url):
if Env.get('debug'): return True
if Env.get('dev'): return True
now = time.time()
host = urlparse(test_url).hostname
@@ -65,9 +65,13 @@ class YarrProvider(Provider):
def belongsTo(self, url, host = None):
try:
hostname = urlparse(url).hostname
download_url = host if host else self.urls['download']
if hostname in download_url:
if host and hostname in host:
return self
else:
for url_type in self.urls:
download_url = self.urls[url_type]
if hostname in download_url:
return self
except:
log.debug('Url % s doesn\'t belong to %s' % (url, self.getName()))
@@ -1,39 +0,0 @@
from .main import MediaBrowser
def start():
return MediaBrowser()
config = [{
'name': 'mediabrowser',
'groups': [
{
'tab': 'renamer',
'subtab': 'metadata',
'name': 'mediabrowser_metadata',
'label': 'MediaBrowser',
'description': 'Enable metadata MediaBrowser can understand',
'options': [
{
'name': 'meta_enabled',
'default': False,
'type': 'enabler',
},
{
'name': 'meta_nfo',
'default': True,
'type': 'bool',
},
{
'name': 'meta_fanart',
'default': True,
'type': 'bool',
},
{
'name': 'meta_thumbnail',
'default': True,
'type': 'bool',
},
],
},
],
}]
@@ -1,5 +0,0 @@
from couchpotato.core.providers.metadata.base import MetaDataBase
class MediaBrowser(MetaDataBase):
pass
@@ -1,29 +0,0 @@
from .main import SonyPS3
def start():
return SonyPS3()
config = [{
'name': 'sonyps3',
'groups': [
{
'tab': 'renamer',
'subtab': 'metadata',
'name': 'sonyps3_metadata',
'label': 'Sony PS3',
'description': 'Enable metadata your Playstation 3 can understand',
'options': [
{
'name': 'meta_enabled',
'default': False,
'type': 'enabler',
},
{
'name': 'meta_thumbnail',
'default': True,
'type': 'bool',
},
],
},
],
}]
@@ -1,5 +0,0 @@
from couchpotato.core.providers.metadata.base import MetaDataBase
class SonyPS3(MetaDataBase):
pass
@@ -1,29 +0,0 @@
from .main import WDTV
def start():
return WDTV()
config = [{
'name': 'wdtv',
'groups': [
{
'tab': 'renamer',
'subtab': 'metadata',
'name': 'wdtv_metadata',
'label': 'WDTV',
'description': 'Enable metadata WDTV can understand',
'options': [
{
'name': 'meta_enabled',
'default': False,
'type': 'enabler',
},
{
'name': 'meta_thumbnail',
'default': True,
'type': 'bool',
},
],
},
],
}]
@@ -1,5 +0,0 @@
from couchpotato.core.providers.metadata.base import MetaDataBase
class WDTV(MetaDataBase):
pass
@@ -20,19 +20,41 @@ config = [{
},
{
'name': 'meta_nfo',
'label': 'NFO',
'default': True,
'type': 'bool',
},
{
'name': 'meta_nfo_name',
'label': 'NFO filename',
'default': '%s.nfo',
'advanced': True,
'description': '<strong>%s</strong> is the rootname of the movie. For example "/path/to/movie cd1.mkv" will be "/path/to/movie"'
},
{
'name': 'meta_fanart',
'label': 'Fanart',
'default': True,
'type': 'bool',
},
{
'name': 'meta_fanart_name',
'label': 'Fanart filename',
'default': '%s-fanart.jpg',
'advanced': True,
},
{
'name': 'meta_thumbnail',
'label': 'Thumbnail',
'default': True,
'type': 'bool',
},
{
'name': 'meta_thumbnail_name',
'label': 'Thumbnail filename',
'default': '%s.tbn',
'advanced': True,
},
],
},
],
@@ -15,13 +15,13 @@ class XBMC(MetaDataBase):
return os.path.join(data['destination_dir'], data['filename'])
def getFanartName(self, root):
return '%s-fanart.jpg' % root
return self.conf('meta_fanart_name') % root
def getThumbnailName(self, root):
return '%s.tbn' % root
return self.conf('meta_thumbnail_name') % root
def getNfoName(self, root):
return '%s.nfo' % root
return self.conf('meta_nfo_name') % root
def getNfo(self, movie_info = {}, data = {}):
nfoxml = Element('movie')
@@ -1,6 +1,5 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.request import jsonified, getParams
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.movie.base import MovieProvider
@@ -12,26 +11,37 @@ log = CPLog(__name__)
class CouchPotatoApi(MovieProvider):
apiUrl = 'http://couchpota.to/api/%s/'
api_url = 'http://couchpota.to/api/%s/'
http_time_between_calls = 0
def __init__(self):
addEvent('movie.release_date', self.releaseDate)
addApiView('movie.suggest', self.suggestView)
#addApiView('movie.suggest', self.suggestView)
def releaseDate(self, imdb_id):
addEvent('movie.info', self.getInfo)
addEvent('movie.release_date', self.getReleaseDate)
def getInfo(self, identifier = None):
return {
'release_date': self.getReleaseDate(identifier)
}
def getReleaseDate(self, identifier = None):
if identifier is None: return {}
try:
data = self.urlopen((self.apiUrl % ('eta')) + (id + '/'))
headers = {'X-CP-Version': fireEvent('app.version', single = True)}
data = self.urlopen((self.api_url % ('eta')) + (identifier + '/'), headers = headers)
dates = json.loads(data)
log.info('Found ETA for %s: %s' % (imdb_id, dates))
log.debug('Found ETA for %s: %s' % (identifier, dates))
return dates
except Exception, e:
log.error('Error getting ETA for %s: %s' % (imdb_id, e))
log.error('Error getting ETA for %s: %s' % (identifier, e))
return dates
return {}
def suggest(self, movies = [], ignore = []):
try:
data = self.urlopen((self.apiUrl % ('suggest')) + ','.join(movies) + '/' + ','.join(ignore) + '/')
data = self.urlopen((self.api_url % ('suggest')) + ','.join(movies) + '/' + ','.join(ignore) + '/')
suggestions = json.loads(data)
log.info('Found Suggestions for %s' % (suggestions))
except Exception, e:
@@ -1,8 +1,8 @@
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import tryInt, tryFloat
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.movie.base import MovieProvider
from urllib import urlencode
import json
import re
import traceback
@@ -27,8 +27,11 @@ class IMDBAPI(MovieProvider):
name_year = fireEvent('scanner.name_year', q, single = True)
if not q or not name_year.get('name'):
return []
cache_key = 'imdbapi.cache.%s' % q
cached = self.getCache(cache_key, self.urls['search'] % urlencode({'t': name_year.get('name'), 'y': name_year.get('year')}))
cached = self.getCache(cache_key, self.urls['search'] % tryUrlencode({'t': name_year.get('name'), 'y': name_year.get('year', '')}))
if cached:
result = self.parseMovie(cached)
@@ -42,6 +45,9 @@ class IMDBAPI(MovieProvider):
def getInfo(self, identifier = None):
if not identifier:
return {}
cache_key = 'imdbapi.cache.%s' % identifier
cached = self.getCache(cache_key, self.urls['info'] % identifier)
@@ -69,6 +75,8 @@ class IMDBAPI(MovieProvider):
if tmp_movie.get(key).lower() == 'n/a':
del movie[key]
year = tryInt(movie.get('Year', ''))
movie_data = {
'titles': [movie.get('Title')] if movie.get('Title') else [],
'original_title': movie.get('Title', ''),
@@ -82,7 +90,7 @@ class IMDBAPI(MovieProvider):
'imdb': str(movie.get('ID', '')),
'runtime': self.runtimeToMinutes(movie.get('Runtime', '')),
'released': movie.get('Released', ''),
'year': movie.get('Year', ''),
'year': year if isinstance(year, (int)) else None,
'plot': movie.get('Plot', ''),
'genres': movie.get('Genre', '').split(','),
'directors': movie.get('Director', '').split(','),
@@ -3,6 +3,7 @@ from couchpotato.core.helpers.encoding import simplifyString, toUnicode
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.movie.base import MovieProvider
from libs.themoviedb import tmdb
import re
log = CPLog(__name__)
@@ -67,8 +68,14 @@ class TheMovieDb(MovieProvider):
if raw:
try:
nr = 0
for movie in raw:
# Sort on returned score first when year is in q
if re.search('\s\d{4}', q):
movies = sorted(raw, key = lambda k: k['score'], reverse = True)
else:
movies = raw
for movie in movies:
results.append(self.parseMovie(movie))
nr += 1
@@ -1,8 +1,8 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider
from dateutil.parser import parse
from urllib import quote_plus
import re
import time
@@ -27,7 +27,7 @@ class Moovee(NZBProvider):
return results
q = '%s %s' % (movie['library']['titles'][0]['title'], quality.get('identifier'))
url = self.urls['search'] % quote_plus(q)
url = self.urls['search'] % tryUrlencode(q)
cache_key = 'moovee.%s' % q
data = self.getCache(cache_key, url)
@@ -1,11 +1,10 @@
from BeautifulSoup import BeautifulSoup
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider
from couchpotato.environment import Env
import urllib
log = CPLog(__name__)
@@ -13,9 +12,9 @@ log = CPLog(__name__)
class Mysterbin(NZBProvider):
urls = {
'search': 'http://www.mysterbin.com/advsearch?%s',
'download': 'http://www.mysterbin.com/nzb?c=%s',
'nfo': 'http://www.mysterbin.com/nfo?c=%s',
'search': 'https://www.mysterbin.com/advsearch?%s',
'download': 'https://www.mysterbin.com/nzb?c=%s',
'nfo': 'https://www.mysterbin.com/nfo?c=%s',
}
http_time_between_calls = 1 #seconds
@@ -40,8 +39,8 @@ class Mysterbin(NZBProvider):
'nopasswd': 'on',
}
cache_key = 'mysterbin.%s' % q
data = self.getCache(cache_key, self.urls['search'] % urllib.urlencode(params))
cache_key = 'mysterbin.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
data = self.getCache(cache_key, self.urls['search'] % tryUrlencode(params))
if data:
try:
@@ -1,9 +1,9 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider
from dateutil.parser import parse
from urllib import urlencode
import base64
import time
import xml.etree.ElementTree as XMLTree
@@ -14,7 +14,7 @@ log = CPLog(__name__)
class Newzbin(NZBProvider, RSS):
urls = {
'download': 'http://www.newzbin2.es/api/dnzb/',
'download': 'https://www.newzbin2.es/api/dnzb/',
'search': 'https://www.newzbin2.es/search/',
}
@@ -45,7 +45,7 @@ class Newzbin(NZBProvider, RSS):
format_id = self.getFormatId(type)
cat_id = self.getCatId(type)
arguments = urlencode({
arguments = tryUrlencode({
'searchaction': 'Search',
'u_url_posts_only': '0',
'u_show_passworded': '0',
@@ -89,11 +89,14 @@ class Newzbin(NZBProvider, RSS):
title = self.getTextElement(nzb, "title")
if 'error' in title.lower(): continue
REPORT_NS = 'http://www.newzbin.com/DTD/2007/feeds/report/';
REPORT_NS = 'http://www.newzbin2.es/DTD/2007/feeds/report/';
# Add attributes to name
for attr in nzb.find('{%s}attributes' % REPORT_NS):
title += ' ' + attr.text
try:
for attr in nzb.find('{%s}attributes' % REPORT_NS):
title += ' ' + attr.text
except:
pass
id = int(self.getTextElement(nzb, '{%s}id' % REPORT_NS))
size = str(int(self.getTextElement(nzb, '{%s}size' % REPORT_NS)) / 1024 / 1024) + ' mb'
@@ -19,16 +19,16 @@ config = [{
},
{
'name': 'use',
'default': '0'
'default': '0,0,0'
},
{
'name': 'host',
'default': 'nzb.su',
'default': 'nzb.su,dognzb.cr,beta.nzbs.org',
'description': 'The hostname of your newznab provider',
},
{
'name': 'api_key',
'default': '',
'default': ',,',
'label': 'Api Key',
'description': 'Can be found on your profile page',
'type': 'combined',
@@ -1,10 +1,10 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider
from dateutil.parser import parse
from urllib import urlencode
import time
import xml.etree.ElementTree as XMLTree
@@ -48,7 +48,7 @@ class Newznab(NZBProvider, RSS):
if self.isDisabled(host) or not self.isAvailable(self.getUrl(host['host'], self.urls['search'])):
return results
arguments = urlencode({
arguments = tryUrlencode({
't': self.cat_backup_id,
'r': host['api_key'],
'i': 58,
@@ -81,11 +81,10 @@ class Newznab(NZBProvider, RSS):
return results
cat_id = self.getCatId(quality['identifier'])
arguments = urlencode({
arguments = tryUrlencode({
'imdbid': movie['library']['identifier'].replace('tt', ''),
'cat': cat_id[0],
'apikey': host['api_key'],
't': self.urls['search'],
'extended': 1
})
url = "%s&%s" % (self.getUrl(host['host'], self.urls['search']), arguments)
@@ -121,8 +120,8 @@ class Newznab(NZBProvider, RSS):
elif item.attrib.get('name') == 'usenetdate':
date = item.attrib.get('value')
if date is '': log.info('Date not parsed properly or not available for %s: %s' % (host, self.getTextElement(nzb, "title")))
if size is 0: log.info('Size not parsed properly or not available for %s: %s' % (host, self.getTextElement(nzb, "title")))
if date is '': log.debug('Date not parsed properly or not available for %s: %s' % (host['host'], self.getTextElement(nzb, "title")))
if size is 0: log.debug('Size not parsed properly or not available for %s: %s' % (host['host'], self.getTextElement(nzb, "title")))
id = self.getTextElement(nzb, "guid").split('/')[-1:].pop()
new = {
@@ -158,9 +157,9 @@ class Newznab(NZBProvider, RSS):
def getHosts(self):
uses = str(self.conf('use')).split(',')
hosts = self.conf('host').split(',')
api_keys = self.conf('api_key').split(',')
uses = [x.strip() for x in str(self.conf('use')).split(',')]
hosts = [x.strip() for x in self.conf('host').split(',')]
api_keys = [x.strip() for x in self.conf('api_key').split(',')]
list = []
for nr in range(len(hosts)):
@@ -1,6 +1,5 @@
from BeautifulSoup import BeautifulSoup, SoupStrainer
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
@@ -8,7 +7,6 @@ from couchpotato.core.providers.nzb.base import NZBProvider
from couchpotato.environment import Env
from dateutil.parser import parse
import time
import urllib
import xml.etree.ElementTree as XMLTree
log = CPLog(__name__)
@@ -17,7 +15,7 @@ log = CPLog(__name__)
class NZBClub(NZBProvider, RSS):
urls = {
'search': 'http://www.nzbclub.com/nzbfeed.aspx?%s',
'search': 'https://www.nzbclub.com/nzbfeed.aspx?%s',
}
http_time_between_calls = 3 #seconds
@@ -41,8 +39,8 @@ class NZBClub(NZBProvider, RSS):
'ns': 1,
}
cache_key = 'nzbclub.%s' % q
data = self.getCache(cache_key, self.urls['search'] % urllib.urlencode(params))
cache_key = 'nzbclub.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
data = self.getCache(cache_key, self.urls['search'] % tryUrlencode(params))
if data:
try:
try:
@@ -68,8 +66,8 @@ class NZBClub(NZBProvider, RSS):
'name': toUnicode(self.getTextElement(nzb, "title")),
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': tryInt(size) / 1024 / 1024,
'url': enclosure['url'],
'download': enclosure['url'].replace(' ', '_'),
'url': enclosure['url'].replace(' ', '_'),
'download': self.download,
'detail_url': self.getTextElement(nzb, "link"),
'description': description,
}
@@ -1,12 +1,11 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider
from couchpotato.environment import Env
from dateutil.parser import parse
from urllib import urlencode
import re
import time
import xml.etree.ElementTree as XMLTree
@@ -30,7 +29,7 @@ class NzbIndex(NZBProvider, RSS):
return results
q = '%s %s %s' % (movie['library']['titles'][0]['title'], movie['library']['year'], quality.get('identifier'))
arguments = urlencode({
arguments = tryUrlencode({
'q': q,
'age': Env.setting('retention', 'nzb'),
'sort': 'agedesc',
@@ -43,7 +42,7 @@ class NzbIndex(NZBProvider, RSS):
})
url = "%s?%s" % (self.urls['api'], arguments)
cache_key = 'nzbindex.%s' % q
cache_key = 'nzbindex.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
data = self.getCache(cache_key, url)
if data:
@@ -1,10 +1,10 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider
from couchpotato.environment import Env
from dateutil.parser import parse
from urllib import urlencode
import time
import xml.etree.ElementTree as XMLTree
@@ -16,7 +16,7 @@ class NZBMatrix(NZBProvider, RSS):
urls = {
'download': 'https://api.nzbmatrix.com/v1.1/download.php?id=%s',
'detail': 'https://nzbmatrix.com/nzb-details.php?id=%s&hit=1',
'search': 'http://rss.nzbmatrix.com/rss.php',
'search': 'https://rss.nzbmatrix.com/rss.php',
}
cat_ids = [
@@ -37,7 +37,7 @@ class NZBMatrix(NZBProvider, RSS):
cat_ids = ','.join(['%s' % x for x in self.getCatId(quality.get('identifier'))])
arguments = urlencode({
arguments = tryUrlencode({
'term': movie['library']['identifier'],
'subcat': cat_ids,
'username': self.conf('username'),
+6 -7
View File
@@ -1,10 +1,9 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import simplifyString
from couchpotato.core.helpers.encoding import simplifyString, tryUrlencode
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider
from dateutil.parser import parse
from urllib import urlencode
import time
import xml.etree.ElementTree as XMLTree
@@ -14,10 +13,10 @@ log = CPLog(__name__)
class Nzbs(NZBProvider, RSS):
urls = {
'download': 'http://nzbs.org/index.php?action=getnzb&nzbid=%s%s',
'nfo': 'http://nzbs.org/index.php?action=view&nzbid=%s&nfo=1',
'detail': 'http://nzbs.org/index.php?action=view&nzbid=%s',
'api': 'http://nzbs.org/rss.php',
'download': 'https://nzbs.org/index.php?action=getnzb&nzbid=%s%s',
'nfo': 'https://nzbs.org/index.php?action=view&nzbid=%s&nfo=1',
'detail': 'https://nzbs.org/index.php?action=view&nzbid=%s',
'api': 'https://nzbs.org/rss.php',
}
cat_ids = [
@@ -36,7 +35,7 @@ class Nzbs(NZBProvider, RSS):
return results
cat_id = self.getCatId(quality.get('identifier'))
arguments = urlencode({
arguments = tryUrlencode({
'action':'search',
'q': simplifyString(movie['library']['titles'][0]['title']),
'catid': cat_id[0],
+3 -3
View File
@@ -1,8 +1,8 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider
from urllib import quote_plus
import re
log = CPLog(__name__)
@@ -26,9 +26,9 @@ class X264(NZBProvider):
return results
q = '%s %s %s' % (movie['library']['titles'][0]['title'], movie['library']['year'], quality.get('identifier'))
url = self.urls['search'] % quote_plus(q)
url = self.urls['search'] % tryUrlencode(q)
cache_key = 'x264.%s' % q
cache_key = 'x264.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
data = self.getCache(cache_key, url)
if data:
match = re.compile(self.regex, re.DOTALL).finditer(data)
@@ -1,13 +0,0 @@
from couchpotato.core.event import addEvent
from couchpotato.core.providers.base import Provider
class SubtitleProvider(Provider):
type = 'subtitle'
def __init__(self):
addEvent('subtitle.search', self.search)
def search(self, group):
pass
@@ -1,6 +0,0 @@
from .main import SubliminalProvider
def start():
return SubliminalProvider()
config = []
@@ -1,22 +0,0 @@
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.subtitle.base import SubtitleProvider
from couchpotato.environment import Env
from libs import subliminal
log = CPLog(__name__)
class SubliminalProvider(SubtitleProvider):
plugins = ['OpenSubtitles', 'TheSubDB', 'SubsWiki']
def search(self, files = [], languages = []):
# download subtitles
with subliminal.Subliminal(cache_dir = Env.get('cache_dir'), multi = True,
languages = self.getLanguages(), plugins = self.plugins) as subli:
subtitles = subli.downloadSubtitles(files)
print subtitles
return subtitles
@@ -17,6 +17,7 @@ class KickAssTorrents(TorrentProvider):
'test': 'http://www.kat.ph/',
'detail': 'http://www.kat.ph/%s-t%s.html',
'search': 'http://www.kat.ph/%s-i%s/',
'download': 'http://torcache.net/',
}
cat_ids = [
@@ -36,7 +37,7 @@ class KickAssTorrents(TorrentProvider):
if self.isDisabled() or not self.isAvailable(self.urls['test']):
return results
cache_key = 'kickasstorrents.%s' % movie['library']['identifier']
cache_key = 'kickasstorrents.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
data = self.getCache(cache_key, self.urls['search'] % (movie['library']['titles'][0]['title'], movie['library']['identifier'].replace('tt', '')))
if data:
@@ -127,7 +128,7 @@ class KickAssTorrents(TorrentProvider):
return tryInt(age)
def download(self, url = '', nzb_id = ''):
compressed_data = super(KickAssTorrents, self).download(url = url, nzb_id = nzb_id)
compressed_data = self.urlopen(url = url, headers = {'Referer': 'http://kat.ph/'})
compressedstream = StringIO.StringIO(compressed_data)
gzipper = gzip.GzipFile(fileobj = compressedstream)
@@ -1,9 +1,9 @@
from BeautifulSoup import SoupStrainer, BeautifulSoup
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.trailer.base import TrailerProvider
from string import letters, digits
from urllib import urlencode
import re
log = CPLog(__name__)
@@ -44,7 +44,7 @@ class HDTrailers(TrailerProvider):
movie_name = group['library']['titles'][0]['title']
url = "%s?%s" % (self.url['backup'], urlencode({'s':movie_name}))
url = "%s?%s" % (self.url['backup'], tryUrlencode({'s':movie_name}))
data = self.getCache('hdtrailers.alt.%s' % group['library']['identifier'], url)
try:
+27
View File
@@ -4,9 +4,11 @@ from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import isInt, toUnicode
from couchpotato.core.helpers.request import getParams, jsonified
from couchpotato.core.helpers.variable import mergeDicts, tryInt
from couchpotato.core.settings.model import Properties
import ConfigParser
import os.path
import time
import traceback
class Settings(object):
@@ -190,3 +192,28 @@ class Settings(object):
return jsonified({
'success': True,
})
def getProperty(self, identifier):
from couchpotato import get_session
db = get_session()
try:
prop = db.query(Properties).filter_by(identifier = identifier).first()
return prop.value if prop else None
except:
return None
def setProperty(self, identifier, value = ''):
from couchpotato import get_session
db = get_session()
p = db.query(Properties).filter_by(identifier = identifier).first()
if not p:
p = Properties()
db.add(p)
p.identifier = identifier
p.value = toUnicode(value)
db.commit()
+14 -8
View File
@@ -44,8 +44,8 @@ class Movie(Entity):
library = ManyToOne('Library')
status = ManyToOne('Status')
profile = ManyToOne('Profile')
releases = OneToMany('Release')
files = ManyToMany('File')
releases = OneToMany('Release', cascade = 'all, delete-orphan')
files = ManyToMany('File', cascade = 'all, delete-orphan', single_parent = True)
class Library(Entity):
@@ -59,9 +59,9 @@ class Library(Entity):
info = Field(JsonType)
status = ManyToOne('Status')
movies = OneToMany('Movie')
titles = OneToMany('LibraryTitle')
files = ManyToMany('File')
movies = OneToMany('Movie', cascade = 'all, delete-orphan')
titles = OneToMany('LibraryTitle', cascade = 'all, delete-orphan')
files = ManyToMany('File', cascade = 'all, delete-orphan', single_parent = True)
class LibraryTitle(Entity):
@@ -94,9 +94,9 @@ class Release(Entity):
movie = ManyToOne('Movie')
status = ManyToOne('Status')
quality = ManyToOne('Quality')
files = ManyToMany('File')
history = OneToMany('History')
info = OneToMany('ReleaseInfo')
files = ManyToMany('File', cascade = 'all, delete-orphan', single_parent = True)
history = OneToMany('History', cascade = 'all, delete-orphan')
info = OneToMany('ReleaseInfo', cascade = 'all, delete-orphan')
class ReleaseInfo(Entity):
@@ -229,6 +229,12 @@ class Folder(Entity):
label = Field(Unicode(255))
class Properties(Entity):
identifier = Field(String(50), index = True)
value = Field(Unicode(255), nullable = False)
def setup():
"""Setup the database and create the tables that don't exists yet"""
from elixir import setup_all, create_all
+9
View File
@@ -61,6 +61,15 @@ class Env(object):
return s
@staticmethod
def prop(identifier, value = None, default = None):
s = Env.get('settings')
if value == None:
v = s.getProperty(identifier)
return v if v else default
s.setProperty(identifier, value)
@staticmethod
def getPermission(setting_type):
perm = Env.get('settings').get('permission_%s' % setting_type, default = '0777')
+1 -1
View File
@@ -52,7 +52,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
locale.setlocale(locale.LC_ALL, "")
encoding = locale.getpreferredencoding()
except (locale.Error, IOError):
pass
encoding = None
# for OSes that are poorly configured I'll just force UTF-8
if not encoding or encoding in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
Binary file not shown.

After

Width:  |  Height:  |  Size: 778 B

+40 -3
View File
@@ -70,13 +70,13 @@ var CouchPotato = new Class({
[new Element('a.orange', {
'text': 'Restart',
'events': {
'click': self.restart.bind(self)
'click': self.restartQA.bind(self)
}
}),
new Element('a.red', {
'text': 'Shutdown',
'events': {
'click': self.shutdown.bind(self)
'click': self.shutdownQA.bind(self)
}
}),
new Element('a', {
@@ -161,6 +161,25 @@ var CouchPotato = new Class({
self.checkAvailable(1000);
},
shutdownQA: function(e){
var self = this;
var q = new Question('Are you sure you want to shutdown CouchPotato?', '', [{
'text': 'Shutdown',
'class': 'shutdown red',
'events': {
'click': function(e){
(e).preventDefault();
self.shutdown();
q.close.delay(100, q);
}
}
}, {
'text': 'No, nevah!',
'cancel': true
}]);
},
restart: function(message, title){
var self = this;
@@ -169,6 +188,25 @@ var CouchPotato = new Class({
self.checkAvailable(1000);
},
restartQA: function(e, message, title){
var self = this;
var q = new Question('Are you sure you want to restart CouchPotato?', '', [{
'text': 'Restart',
'class': 'restart orange',
'events': {
'click': function(e){
(e).preventDefault();
self.restart(message, title);
q.close.delay(100, q);
}
}
}, {
'text': 'No, nevah!',
'cancel': true
}]);
},
checkForUpdate: function(func){
var self = this;
@@ -190,7 +228,6 @@ var CouchPotato = new Class({
},
'onSuccess': function(){
self.unBlockPage();
self.fireEvent('load');
}
});
@@ -25,20 +25,24 @@ var self = window.StyleFix = {
var url = link.href || link.getAttribute('data-href'),
base = url.replace(/[^\/]+$/, ''),
parent = link.parentNode,
xhr = new XMLHttpRequest();
xhr = new XMLHttpRequest(),
process;
xhr.open('GET', url);
xhr.onreadystatechange = function() {
if(xhr.readyState === 4) {
process();
}
};
process = function() {
var css = xhr.responseText;
if(css && link.parentNode) {
if(css && link.parentNode && (!xhr.status || xhr.status < 400 || xhr.status > 600)) {
css = self.fix(css, true, link);
// Convert relative URLs to absolute, if needed
if(base) {
css = css.replace(/url\(((?:"|')?)(.+?)\1\)/gi, function($0, quote, url) {
css = css.replace(/url\(\s*?((?:"|')?)(.+?)\1\s*?\)/gi, function($0, quote, url) {
if(!/^([a-z]{3,10}:|\/|#)/i.test(url)) { // If url not absolute & not a hash
// May contain sequences like /../ and /./ but those DO work
return 'url("' + base + url + '")';
@@ -60,10 +64,21 @@ var self = window.StyleFix = {
parent.insertBefore(style, link);
parent.removeChild(link);
}
}
};
xhr.send(null);
try {
xhr.open('GET', url);
xhr.send(null);
} catch (e) {
// Fallback to XDomainRequest if available
if (typeof XDomainRequest != "undefined") {
xhr = new XDomainRequest();
xhr.onerror = xhr.onprogress = function() {};
xhr.onload = process;
xhr.open("GET", url);
xhr.send(null);
}
}
link.setAttribute('data-inprogress', '');
},
@@ -135,7 +150,7 @@ function $(expr, con) {
})();
/**
* PrefixFree 1.0.4
* PrefixFree 1.0.5
* @author Lea Verou
* MIT license
*/
@@ -300,6 +315,10 @@ var functions = {
'element': {
property: 'backgroundImage',
params: '#foo'
},
'cross-fade': {
property: 'backgroundImage',
params: 'url(a.png), url(b.png), 50%'
}
};
@@ -18,13 +18,7 @@ var Question = new Class( {
createMask : function() {
var self = this
$(document.body).mask( {
'hideOnClick' : true,
'destroyOnHide' : true,
'onHide' : function() {
self.container.destroy();
}
}).show();
self.mask = new Element('div.mask').fade('hide').inject(document.body).fade('in');
},
createQuestion : function() {
@@ -71,7 +65,11 @@ var Question = new Class( {
},
close : function() {
$(document.body).get('mask').destroy();
var self = this;
self.mask.fade('out');
(function(){self.mask.destroy()}).delay(1000);
this.container.destroy();
},
toElement : function() {
@@ -38,7 +38,6 @@ Page.Manage = new Class({
refresh: function(full){
var self = this;
p(full)
Api.request('manage.update', {
'data': {
+2 -2
View File
@@ -574,7 +574,7 @@ Option.Directory = new Class({
self.el.adopt(
self.createLabel(),
new Element('span.directory.inlay', {
self.directory_inlay = new Element('span.directory.inlay', {
'events': {
'click': self.showBrowser.bind(self)
}
@@ -664,7 +664,7 @@ Option.Directory = new Class({
}
})
)
).inject(self.input, 'before');
).inject(self.directory_inlay, 'before');
new Form.Check(self.show_hidden);
}
+3 -2
View File
@@ -61,7 +61,7 @@ window.addEvent('domready', function(){
'name': 'profile'
}),
new Element('a.button.edit', {
'text': 'Save',
'text': 'Save & Search',
'events': {
'click': self.save.bind(self)
}
@@ -150,7 +150,7 @@ window.addEvent('domready', function(){
var self = this;
self.el = new Element('a.delete', {
'title': 'Remove the movie from your wanted list',
'title': 'Remove the movie from this CP list',
'events': {
'click': self.showConfirm.bind(self)
}
@@ -286,6 +286,7 @@ window.addEvent('domready', function(){
},
})
,'Delete': MovieActions.Wanted.Delete
};
})
+4 -4
View File
@@ -13,7 +13,6 @@ body {
background: #4e5969;
overflow-y: scroll;
height: 100%;
text-align: justify;
}
body.noscroll { overflow: hidden; }
@@ -154,6 +153,7 @@ body > .spinner, .mask{
.icon.refresh { background-image: url('../images/icon.refresh.png'); }
.icon.rating { background-image: url('../images/icon.rating.png'); }
.icon.files { background-image: url('../images/icon.files.png'); }
.icon.info { background-image: url('../images/icon.info.png'); }
/*** Navigation ***/
.header {
@@ -305,9 +305,9 @@ body > .spinner, .mask{
text-align: center;
position: relative;
top: -70px;
padding: 15px 0 20px;
padding: 2px 0;
background: #ff6134;
font-size: 26px;
font-size: 12px;
border-radius: 0 0 5px 5px;
box-shadow: 0 2px 1px rgba(0,0,0, 0.3);
}
@@ -491,7 +491,7 @@ body > .spinner, .mask{
width: auto;
}
.question .answer:hover {
background: #f1f1f1;
background: #000;
}
.question .answer.delete {
+2 -2
View File
@@ -208,7 +208,7 @@
z-index: 2;
position: absolute;
width: 450px;
margin: 28px 0 20px -4px;
margin: 28px 0 20px 18%;
background: #5c697b;
border-radius: 3px;
box-shadow: 0 0 50px rgba(0,0,0,0.55);
@@ -221,7 +221,7 @@
display: block;
position: absolute;
width: 0px;
margin: -6px 0 0 38%;
margin: -6px 0 0 22%;
}
.page .directory_list ul {
+2 -2
View File
@@ -66,8 +66,8 @@
'data': {
'type': 'error',
'message': Browser.name + ' ' + Browser.version + ': \n' + message,
'page': window.location.href,
'file': file,
'page': window.location.href.replace(window.location.host, 'HOST'),
'file': file.replace(window.location.host, 'HOST'),
'line': line
}
});
+6
View File
@@ -18,6 +18,12 @@
<br />
You can also use the API over another domain using JSONP, the callback function should be in 'callback_func'
<pre><a href="{{ fireEvent('app.api_url', single = True)|safe }}/updater.info/?callback_func=myfunction">{{ fireEvent('app.api_url', single = True)|safe }}/updater.info/?callback_func=myfunction</a></pre>
<br />
<br />
Get the API key:
<pre><a href="/getkey/?p=md5(password)&amp;u=md5(username)">/getkey/?p=md5(password)&amp;u=md5(username)</a></pre>
Will return {"api_key": "XXXXXXXXXX", "success": true}. When username or password is empty you don't need to md5 it.
<br />
</div>
{% for route in routes %}
+81
View File
@@ -0,0 +1,81 @@
#!/bin/sh
# Package
PACKAGE="couchpotato"
DNAME="CouchPotato"
# Others
INSTALL_DIR="/opt/${PACKAGE}"
PYTHON_DIR="/opt/bin"
PATH="${INSTALL_DIR}/bin:${INSTALL_DIR}/env/bin:${PYTHON_DIR}/bin:/usr/local/bin:/bin:/usr/bin:/usr/syno/bin"
RUNAS="couchpotato"
PYTHON="${PYTHON_DIR}/python2.7"
COUCHPOTATO="${INSTALL_DIR}/CouchPotato.py"
CFG_FILE="${INSTALL_DIR}/var/settings.conf"
PID_FILE="${INSTALL_DIR}/var/couchpotato.pid"
LOG_FILE="${INSTALL_DIR}/var/logs/CouchPotato.log"
start_daemon()
{
su ${RUNAS} -c "PATH=${PATH} ${PYTHON} ${COUCHPOTATO} --daemon --pid_file ${PID_FILE} --config ${CFG_FILE}"
}
stop_daemon()
{
kill `cat ${PID_FILE}`
wait_for_status 1 20
rm -f ${PID_FILE}
}
daemon_status()
{
if [ -f ${PID_FILE} ] && [ -d /proc/`cat ${PID_FILE}` ]; then
return 0
fi
return 1
}
wait_for_status()
{
counter=$2
while [ ${counter} -gt 0 ]; do
daemon_status
[ $? -eq $1 ] && break
let counter=counter-1
sleep 1
done
}
case $1 in
start)
if daemon_status; then
echo ${DNAME} is already running
else
echo Starting ${DNAME} ...
start_daemon
fi
;;
stop)
if daemon_status; then
echo Stopping ${DNAME} ...
stop_daemon
else
echo ${DNAME} is not running
fi
;;
status)
if daemon_status; then
echo ${DNAME} is running
exit 0
else
echo ${DNAME} is not running
exit 1
fi
;;
log)
echo ${LOG_FILE}
;;
*)
exit 1
;;
esac
+1 -1
View File
@@ -45,7 +45,7 @@ case "$1" in
start)
echo "Starting $DESC"
rm -rf $PID_PATH || return 1
install -d --mode=0755 -o $RUN_AS -g $RUN_AS $PID_PATH || return 1
install -d --mode=0755 -o $RUN_AS $PID_PATH || return 1
start-stop-daemon -d $APP_PATH -c $RUN_AS --start --background --pidfile $PID_FILE --exec $DAEMON -- $DAEMON_OPTS
;;
stop)
+3 -2
View File
@@ -56,7 +56,7 @@ class Event(object):
>> None
"""
def __init__(self, sender = None, asynch = False, exc_info = False,
def __init__(self, name = None, sender = None, asynch = False, exc_info = False,
lock = None, threads = 3, traceback = False):
""" Creates an event
@@ -96,6 +96,7 @@ class Event(object):
)
"""
self.in_order = False
self.name = name
self.asynchronous = asynch
self.exc_info = exc_info
self.lock = lock
@@ -178,7 +179,7 @@ class Event(object):
""" Executes all handlers stored in the queue """
while True:
try:
h_ = self.queue.get()
h_ = self.queue.get(timeout = 2)
handler, memoize, timeout = self.handlers[h_]
if self.lock and self.in_order:
+15 -8
View File
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
# enzyme - Video metadata parser
# Copyright (C) 2011 Antoine Bertin <diaoulael@gmail.com>
# Copyright (C) 2003-2006 Thomas Schueppel <stain@acm.org>
# Copyright (C) 2003-2006 Dirk Meyer <dischi@freevo.org>
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
# Copyright 2003-2006 Thomas Schueppel <stain@acm.org>
# Copyright 2003-2006 Dirk Meyer <dischi@freevo.org>
#
# This file is part of enzyme.
#
@@ -17,14 +17,13 @@
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# along with enzyme. If not, see <http://www.gnu.org/licenses/>.
import mimetypes
import os
import sys
from exceptions import *
PARSERS = [('asf', ['video/asf'], ['asf', 'wmv', 'wma']),
('flv', ['video/flv'], ['flv']),
('mkv', ['video/x-matroska', 'application/mkv'], ['mkv', 'mka', 'webm']),
@@ -32,10 +31,18 @@ PARSERS = [('asf', ['video/asf'], ['asf', 'wmv', 'wma']),
('mpeg', ['video/mpeg'], ['mpeg', 'mpg', 'mp4', 'ts']),
('ogm', ['application/ogg'], ['ogm', 'ogg', 'ogv']),
('real', ['video/real'], ['rm', 'ra', 'ram']),
('riff', ['video/avi'], ['wav', 'avi'])]
('riff', ['video/avi'], ['wav', 'avi'])
]
def parse(path):
"""Parse metadata of the given video
:param string path: path to the video file to parse
:return: a parser corresponding to the video's mimetype or extension
:rtype: :class:`~enzyme.core.AVContainer`
"""
if not os.path.isfile(path):
raise ValueError('Invalid path')
extension = os.path.splitext(path)[1][1:]
@@ -47,7 +54,7 @@ def parse(path):
parser_mime = parser_name
if extension in parser_extensions:
parser_ext = parser_name
parser = parser_mime or parser_ext
parser = parser_mime or parser_ext
if not parser:
raise NoParserError()
mod = __import__(parser, globals=globals(), locals=locals(), fromlist=[], level=-1)

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