Merge branch 'refs/heads/develop' into desktop
This commit is contained in:
+6
-2
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -147,7 +147,7 @@ class Release(Plugin):
|
||||
'releases': {'status': {}, 'quality': {}},
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'files': {}
|
||||
}))
|
||||
}), manual = True)
|
||||
|
||||
return jsonified({
|
||||
'success': True
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
# },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 |
@@ -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': {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
})
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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)&u=md5(username)">/getkey/?p=md5(password)&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 %}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
Reference in New Issue
Block a user