Merge branch 'refs/heads/develop' into desktop

This commit is contained in:
Ruud
2012-04-28 23:14:59 +02:00
184 changed files with 5634 additions and 4017 deletions
+6 -2
View File
@@ -114,6 +114,7 @@ class Loader(object):
if __name__ == '__main__': if __name__ == '__main__':
l = None
try: try:
l = Loader() l = Loader()
l.daemonize() l.daemonize()
@@ -135,7 +136,10 @@ if __name__ == '__main__':
try: try:
# if this fails we will have two tracebacks # if this fails we will have two tracebacks
# one for failing to log, and one for the exception that got us here. # 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: except:
print traceback.format_exc() print traceback.format_exc()
raise raise
+34 -1
View File
@@ -2,4 +2,37 @@ CouchPotato Server
===== =====
CouchPotato (CP) is an automatic NZB and torrent downloader. You can keep a "movies I want"-list and it will search for NZBs/torrents of these movies every X hours. CouchPotato (CP) is an automatic NZB and torrent downloader. You can keep a "movies I want"-list and it will search for NZBs/torrents of these movies every X hours.
Once a movie is found, it will send it to SABnzbd or download the torrent to a specified directory. Once a movie is found, it will send it to SABnzbd or download the torrent to a specified directory.
## Running from Source
CouchPotatoServer can be run from source. This will use *git* as updater, so make sure that is installed also.
Windows:
* Install [PyWin32 2.7](http://sourceforge.net/projects/pywin32/files/pywin32/Build%20217/) and [GIT](http://git-scm.com/)
* If you come and ask on the forums 'why directory selection no work?', I will kill a kitten, also this is because you need PyWin32
* Open up `Git Bash` (or CMD) and go to the folder you want to install CP. Something like Program Files.
* Run `git clone https://RuudBurger@github.com/RuudBurger/CouchPotatoServer.git`.
* You can now start CP via `CouchPotatoServer\CouchPotato.py` to start
OSx:
* If you're on Leopard (10.5) install Python 2.6+: [Python 2.6.5](http://www.python.org/download/releases/2.6.5/)
* Install [GIT](http://git-scm.com/)
* Open up `Terminal`
* Go to your App folder `cd /Applications`
* Run `git clone https://RuudBurger@github.com/RuudBurger/CouchPotatoServer.git`
* Then do `python CouchPotatoServer/CouchPotato.py`
Linux (ubuntu / debian):
* Install [GIT](http://git-scm.com/) with `apt-get install git-core`
* 'cd' to the folder of your choosing.
* Run `git clone https://RuudBurger@github.com/RuudBurger/CouchPotatoServer.git`
* Then do `python CouchPotatoServer/CouchPotato.py` to start
* To run on boot copy the init script. `cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato`
* Change the paths inside the init script. `nano /etc/init.d/couchpotato`
* Make it executable. `chmod +x /etc/init.d/couchpotato`
* Add it to defaults. `sudo update-rc.d couchpotato defaults`
+18
View File
@@ -1,6 +1,8 @@
from couchpotato.api import api_docs, api_docs_missing from couchpotato.api import api_docs, api_docs_missing
from couchpotato.core.auth import requires_auth from couchpotato.core.auth import requires_auth
from couchpotato.core.event import fireEvent 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.core.logger import CPLog
from couchpotato.environment import Env from couchpotato.environment import Env
from flask.app import Flask from flask.app import Flask
@@ -51,6 +53,22 @@ def apiDocs():
del api_docs_missing[''] 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)) 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) @app.errorhandler(404)
def page_not_found(error): def page_not_found(error):
index_url = url_for('web.index') index_url = url_for('web.index')
+1 -1
View File
@@ -55,7 +55,7 @@ config = [{
'name': 'api_key', 'name': 'api_key',
'default': uuid4().hex, 'default': uuid4().hex,
'readonly': 1, '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', 'name': 'debug',
+19
View File
@@ -8,6 +8,7 @@ from couchpotato.environment import Env
from flask import request from flask import request
from uuid import uuid4 from uuid import uuid4
import os import os
import platform
import time import time
import traceback import traceback
import webbrowser import webbrowser
@@ -32,12 +33,16 @@ class Core(Plugin):
addApiView('app.available', self.available, docs = { addApiView('app.available', self.available, docs = {
'desc': 'Check if app available.' 'desc': 'Check if app available.'
}) })
addApiView('app.version', self.versionView, docs = {
'desc': 'Get version.'
})
addEvent('app.crappy_shutdown', self.crappyShutdown) addEvent('app.crappy_shutdown', self.crappyShutdown)
addEvent('app.crappy_restart', self.crappyRestart) addEvent('app.crappy_restart', self.crappyRestart)
addEvent('app.load', self.launchBrowser, priority = 1) addEvent('app.load', self.launchBrowser, priority = 1)
addEvent('app.base_url', self.createBaseUrl) addEvent('app.base_url', self.createBaseUrl)
addEvent('app.api_url', self.createApiUrl) addEvent('app.api_url', self.createApiUrl)
addEvent('app.version', self.version)
addEvent('setting.save.core.password', self.md5Password) addEvent('setting.save.core.password', self.md5Password)
addEvent('setting.save.core.api_key', self.checkApikey) addEvent('setting.save.core.api_key', self.checkApikey)
@@ -161,3 +166,17 @@ class Core(Plugin):
def createApiUrl(self): def createApiUrl(self):
return '%s/%s' % (self.createBaseUrl(), Env.setting('api_key')) return '%s/%s' % (self.createBaseUrl(), Env.setting('api_key'))
def version(self):
ver = fireEvent('updater.info', single = True)
if os.name == 'nt': platf = 'windows'
elif 'Darwin' in platform.platform(): platf = 'osx'
else: platf = 'linux'
return '%s - %s-%s - v2' % (platf, ver.get('version')['type'], ver.get('version')['hash'])
def versionView(self):
return jsonified({
'version': self.version()
})
+246 -80
View File
@@ -4,9 +4,13 @@ from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
from git.repository import LocalRepository
from datetime import datetime from datetime import datetime
from dateutil.parser import parse
from git.repository import LocalRepository
import json
import os import os
import shutil
import tarfile
import time import time
import traceback import traceback
@@ -15,27 +19,22 @@ log = CPLog(__name__)
class Updater(Plugin): class Updater(Plugin):
repo_name = 'RuudBurger/CouchPotatoServer'
version = None
update_failed = False
update_version = None
last_check = 0
def __init__(self): 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) fireEvent('schedule.interval', 'updater.check', self.check, hours = 6)
addEvent('app.load', self.check) addEvent('app.load', self.check)
addEvent('updater.info', self.info)
addApiView('updater.info', self.getInfo, docs = { addApiView('updater.info', self.getInfo, docs = {
'desc': 'Get updater information', 'desc': 'Get updater information',
'return': { 'return': {
'type': 'object', 'type': 'object',
'example': """{ 'example': """{
'repo_name': "Name of used repository",
'last_check': "last checked for update", 'last_check': "last checked for update",
'update_version': "available update version or empty", 'update_version': "available update version or empty",
'version': current_cp_version 'version': current_cp_version
@@ -47,75 +46,93 @@ class Updater(Plugin):
'return': {'type': 'see updater.info'} '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()) return jsonified(self.info())
def info(self): def info(self):
return { return {
'repo_name': self.repo_name,
'last_check': self.last_check, 'last_check': self.last_check,
'update_version': self.update_version, '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): def check(self):
pass
if self.update_version or self.isDisabled(): def deletePyc(self, only_excess = True):
return
log.info('Checking for new version on github for %s' % self.repo_name) for root, dirs, files in os.walk(Env.get('app_dir')):
if not Env.get('dev'):
self.repo.fetch()
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(): for excess_pyc_file in excess_pyc_files:
if current_branch == branch.name: 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() for dir_name in dirs:
remote = branch.getHead() 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): def __init__(self, git_command):
self.check() self.repo = LocalRepository(Env.get('app_dir'), command = git_command)
return self.getInfo()
def doUpdateView(self):
return jsonified({
'success': self.doUpdate()
})
def doUpdate(self): def doUpdate(self):
try: try:
@@ -141,29 +158,178 @@ class Updater(Plugin):
return False 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) return self.version
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)
for excess_pyc_file in excess_pyc_files: def check(self):
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()))
for dir_name in dirs: if self.update_version:
full_path = os.path.join(root, dir_name) return
if len(os.listdir(full_path)) == 0:
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: try:
os.rmdir(full_path) os.remove(tofile)
except: except:
log.error('Couldn\'t remove empty directory %s: %s' % (full_path, traceback.format_exc())) pass
def isEnabled(self): try:
return super(Updater, self).isEnabled() and Env.get('uses_git') 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){ if(json.success){
App.restart('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating'); App.restart('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
App.checkAvailable.delay(500, App); App.checkAvailable.delay(500, App);
if(self.message)
self.message.destroy();
} }
} }
}); });
+7
View File
@@ -40,3 +40,10 @@ class Downloader(Plugin):
log.debug("Downloader doesn't support this type") log.debug("Downloader doesn't support this type")
return is_correct 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', 'type': 'dropdown',
'values': [('nzbs & torrents', 'both'), ('nzb', 'nzb'), ('torrent', 'torrent')], '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'] type = ['nzb', 'torrent']
def download(self, data = {}, movie = {}): def download(self, data = {}, movie = {}, manual = False):
if self.isDisabled() or (not self.isCorrectType(data.get('type')) or (not self.conf('use_for') in ['both', data.get('type')])): if self.isDisabled(manual) or (not self.isCorrectType(data.get('type')) or (not self.conf('use_for') in ['both', data.get('type')])):
return return
directory = self.conf('directory') directory = self.conf('directory')
@@ -33,6 +33,13 @@ config = [{
'default': 'Movies', 'default': 'Movies',
'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>', 'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
}, },
{
'name': 'manual',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
},
], ],
} }
], ],
+2 -2
View File
@@ -14,9 +14,9 @@ class NZBGet(Downloader):
url = 'http://nzbget:%(password)s@%(host)s/xmlrpc' 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 return
log.info('Sending "%s" to NZBGet.' % data.get('name')) log.info('Sending "%s" to NZBGet.' % data.get('name'))
@@ -39,6 +39,13 @@ config = [{
'type': 'directory', 'type': 'directory',
'description': 'Your Post-Processing Script directory, set in Sabnzbd > Config > Directories.', 'description': 'Your Post-Processing Script directory, set in Sabnzbd > Config > Directories.',
}, },
{
'name': 'manual',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
},
], ],
} }
], ],
+4 -4
View File
@@ -1,9 +1,9 @@
from couchpotato.core.downloaders.base import Downloader from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import cleanHost from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from inspect import isfunction from inspect import isfunction
from tempfile import mkstemp from tempfile import mkstemp
from urllib import urlencode
import base64 import base64
import os import os
import re import re
@@ -15,9 +15,9 @@ class Sabnzbd(Downloader):
type = ['nzb'] 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 return
log.info("Sending '%s' to SABnzbd." % data.get('name')) log.info("Sending '%s' to SABnzbd." % data.get('name'))
@@ -58,7 +58,7 @@ class Sabnzbd(Downloader):
if pp: if pp:
params['script'] = pp_script_fn params['script'] = pp_script_fn
url = cleanHost(self.conf('host')) + "api?" + urlencode(params) url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
try: try:
if params.get('mode') is 'addfile': if params.get('mode') is 'addfile':
@@ -48,6 +48,13 @@ config = [{
'advanced': True, 'advanced': True,
'description': 'Stop transfer when reaching ratio', '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'] 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 return
log.info('Sending "%s" to Transmission.' % data.get('name')) log.info('Sending "%s" to Transmission.' % data.get('name'))
+3 -1
View File
@@ -19,7 +19,7 @@ def addEvent(name, handler, priority = 100):
if events.get(name): if events.get(name):
e = events[name] e = events[name]
else: 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): def createHandle(*args, **kwargs):
@@ -42,6 +42,8 @@ def removeEvent(name, handler):
e -= handler e -= handler
def fireEvent(name, *args, **kwargs): def fireEvent(name, *args, **kwargs):
if not events.get(name): return
#log.debug('Firing event %s' % name)
try: try:
# Fire after event # Fire after event
+19 -2
View File
@@ -1,5 +1,6 @@
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from string import ascii_letters, digits from string import ascii_letters, digits
from urllib import quote_plus
import re import re
import unicodedata import unicodedata
@@ -19,7 +20,7 @@ def simplifyString(original):
def toUnicode(original, *args): def toUnicode(original, *args):
try: try:
if type(original) is unicode: if isinstance(original, unicode):
return original return original
else: else:
try: try:
@@ -35,7 +36,7 @@ def toUnicode(original, *args):
return toUnicode(ascii_text) return toUnicode(ascii_text)
def ek(original, *args): def ek(original, *args):
if type(original) in [str, unicode]: if isinstance(original, (str, unicode)):
try: try:
from couchpotato.environment import Env from couchpotato.environment import Env
return original.decode(Env.get('encoding')) return original.decode(Env.get('encoding'))
@@ -53,3 +54,19 @@ def isInt(value):
def stripAccents(s): def stripAccents(s):
return ''.join((c for c in unicodedata.normalize('NFD', toUnicode(s)) if unicodedata.category(c) != 'Mn')) return ''.join((c for c in unicodedata.normalize('NFD', toUnicode(s)) if unicodedata.category(c) != 'Mn'))
def tryUrlencode(s):
new = u''
if isinstance(s, (dict)):
for key, value in s.iteritems():
new += u'&%s=%s' % (key, tryUrlencode(value))
return new[1:]
else:
for letter in toUnicode(s):
try:
new += quote_plus(letter)
except:
new += letter
return new
+4 -3
View File
@@ -1,3 +1,4 @@
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import natcmp from couchpotato.core.helpers.variable import natcmp
from flask.globals import current_app from flask.globals import current_app
from flask.helpers import json from flask.helpers import json
@@ -25,7 +26,7 @@ def getParams():
for item in nested: for item in nested:
if item is nested[-1]: if item is nested[-1]:
current[item] = unquote_plus(value) current[item] = toUnicode(unquote_plus(value)).encode('utf-8')
else: else:
try: try:
current[item] current[item]
@@ -34,7 +35,7 @@ def getParams():
current = current[item] current = current[item]
else: else:
temp[param] = unquote_plus(value) temp[param] = toUnicode(unquote_plus(value)).encode('utf-8')
return dictToList(temp) return dictToList(temp)
@@ -56,7 +57,7 @@ def dictToList(params):
def getParam(attr, default = None): def getParam(attr, default = None):
try: 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: except:
return None return None
+10
View File
@@ -9,6 +9,8 @@ def getDataDir():
if os.name == 'nt': if os.name == 'nt':
return os.path.join(os.environ['APPDATA'], 'CouchPotato') return os.path.join(os.environ['APPDATA'], 'CouchPotato')
import pwd
os.environ['HOME'] = pwd.getpwuid(os.geteuid()).pw_dir
user_dir = os.path.expanduser('~') user_dir = os.path.expanduser('~')
# OSX # OSX
@@ -36,10 +38,18 @@ def mergeDicts(a, b):
stack.append((current_dst[key], current_src[key])) stack.append((current_dst[key], current_src[key]))
elif isinstance(current_src[key], list) and isinstance(current_dst[key], list): elif isinstance(current_src[key], list) and isinstance(current_dst[key], list):
current_dst[key].extend(current_src[key]) current_dst[key].extend(current_src[key])
current_dst[key] = removeListDuplicates(current_dst[key])
else: else:
current_dst[key] = current_src[key] current_dst[key] = current_src[key]
return dst return dst
def removeListDuplicates(seq):
checked = []
for e in seq:
if e not in checked:
checked.append(e)
return checked
def flattenList(l): def flattenList(l):
if isinstance(l, list): if isinstance(l, list):
return sum(map(flattenList, l)) return sum(map(flattenList, l))
+2 -2
View File
@@ -56,7 +56,7 @@ class CoreNotifier(Notification):
addEvent('library.update_finish', lambda data: fireEvent('notify.frontend', type = 'library.update', data = data)) addEvent('library.update_finish', lambda data: fireEvent('notify.frontend', type = 'library.update', data = data))
def markAsRead(self): def markAsRead(self):
ids = getParam('ids').split(',') ids = [x.strip() for x in getParam('ids').split(',')]
db = get_session() db = get_session()
@@ -78,7 +78,7 @@ class CoreNotifier(Notification):
q = db.query(Notif) q = db.query(Notif)
if limit_offset: if limit_offset:
splt = limit_offset.split(',') splt = [x.strip() for x in limit_offset.split(',')]
limit = splt[0] limit = splt[0]
offset = 0 if len(splt) is 1 else splt[1] offset = 0 if len(splt) is 1 else splt[1]
q = q.limit(limit).offset(offset) q = q.limit(limit).offset(offset)
+2 -3
View File
@@ -1,12 +1,11 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.request import getParams, jsonified from couchpotato.core.helpers.request import getParams, jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from couchpotato.environment import Env
import re import re
import telnetlib import telnetlib
import urllib
try: try:
import xml.etree.cElementTree as etree import xml.etree.cElementTree as etree
@@ -90,7 +89,7 @@ class NMJ(Notification):
'arg2': 'background', 'arg2': 'background',
'arg3': '', 'arg3': '',
} }
params = urllib.urlencode(params) params = tryUrlencode(params)
UPDATE_URL = 'http://%(host)s:8008/metadata_database?%(params)s' UPDATE_URL = 'http://%(host)s:8008/metadata_database?%(params)s'
updateUrl = UPDATE_URL % {'host': host, 'params': params} updateUrl = UPDATE_URL % {'host': host, 'params': params}
@@ -11,7 +11,7 @@ class NotifyMyAndroid(Notification):
if self.isDisabled(): return if self.isDisabled(): return
nma = pynma.PyNMA() nma = pynma.PyNMA()
keys = self.conf('api_key').split(',') keys = [x.strip() for x in self.conf('api_key').split(',')]
nma.addkey(keys) nma.addkey(keys)
nma.developerkey(self.conf('dev_key')) nma.developerkey(self.conf('dev_key'))
@@ -10,7 +10,7 @@ class NotifyMyWP(Notification):
def notify(self, message = '', data = {}): def notify(self, message = '', data = {}):
if self.isDisabled(): return 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')) 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) response = p.push(application = self.default_title, event = message, description = message, priority = self.conf('priority'), batch_mode = len(keys) > 1)
+2 -3
View File
@@ -1,8 +1,7 @@
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from httplib import HTTPSConnection from httplib import HTTPSConnection
from urllib import urlencode
log = CPLog(__name__) log = CPLog(__name__)
@@ -24,7 +23,7 @@ class Prowl(Notification):
http_handler.request('POST', http_handler.request('POST',
'/publicapi/add', '/publicapi/add',
headers = {'Content-type': 'application/x-www-form-urlencoded'}, headers = {'Content-type': 'application/x-www-form-urlencoded'},
body = urlencode(data) body = tryUrlencode(data)
) )
response = http_handler.getresponse() response = http_handler.getresponse()
request_status = response.status 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.api import addApiView
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.request import jsonified, getParam from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import cleanHost from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
@@ -7,7 +8,6 @@ from flask.helpers import url_for
from pytwitter import Api, parse_qsl from pytwitter import Api, parse_qsl
from werkzeug.utils import redirect from werkzeug.utils import redirect
import oauth2 import oauth2
import urllib
log = CPLog(__name__) log = CPLog(__name__)
@@ -56,7 +56,7 @@ class Twitter(Notification):
oauth_consumer = oauth2.Consumer(self.consumer_key, self.consumer_secret) oauth_consumer = oauth2.Consumer(self.consumer_key, self.consumer_secret)
oauth_client = oauth2.Client(oauth_consumer) 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': if resp['status'] != '200':
log.error('Invalid response from Twitter requesting temp token: %s' % resp['status']) log.error('Invalid response from Twitter requesting temp token: %s' % resp['status'])
+2 -2
View File
@@ -1,7 +1,7 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
import base64 import base64
import urllib
log = CPLog(__name__) log = CPLog(__name__)
@@ -21,7 +21,7 @@ class XBMC(Notification):
def send(self, command, host): 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 = {} headers = {}
if self.conf('password'): if self.conf('password'):
@@ -16,21 +16,27 @@ config = [{
{ {
'name': 'year', 'name': 'year',
'default': 2011, 'default': 2011,
'Label': 'Year',
'type': 'int', 'type': 'int',
}, },
{ {
'name': 'votes', 'name': 'votes',
'default': 1000, 'default': 1000,
'Label': 'Votes',
'type': 'int', 'type': 'int',
}, },
{ {
'name': 'rating', 'name': 'rating',
'default': 6.0, 'default': 6.0,
'Label': 'Rating',
'type': 'float', '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): def __init__(self):
fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12))
if not Env.get('dev'): if not Env.get('dev'):
addEvent('app.load', self.addMovies) addEvent('app.load', self.addMovies)
+31 -4
View File
@@ -1,5 +1,6 @@
from couchpotato import addView from couchpotato import addView
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import getExt from couchpotato.core.helpers.variable import getExt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.environment import Env from couchpotato.environment import Env
@@ -11,10 +12,8 @@ import glob
import math import math
import os.path import os.path
import re import re
import socket
import time import time
import traceback import traceback
import urllib
import urllib2 import urllib2
log = CPLog(__name__) log = CPLog(__name__)
@@ -29,6 +28,8 @@ class Plugin(object):
http_last_use = {} http_last_use = {}
http_time_between_calls = 0 http_time_between_calls = 0
http_failed_request = {}
http_failed_disabled = {}
def registerPlugin(self): def registerPlugin(self):
addEvent('app.shutdown', self.doShutdown) 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' 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 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: try:
if multipart: if multipart:
@@ -115,13 +125,30 @@ class Plugin(object):
data = opener.open(request, timeout = timeout).read() data = opener.open(request, timeout = timeout).read()
else: else:
log.info('Opening url: %s, params: %s' % (url, [x for x in params.iterkeys()])) 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) request = urllib2.Request(url, data, headers)
data = urllib2.urlopen(request, timeout = timeout).read() data = urllib2.urlopen(request, timeout = timeout).read()
self.http_failed_request[host] = 0
except IOError: except IOError:
if show_error: if show_error:
log.error('Failed opening url in %s: %s %s' % (self.getName(), url, traceback.format_exc(1))) log.error('Failed opening url in %s: %s %s' % (self.getName(), url, traceback.format_exc(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 raise
self.http_last_use[host] = time.time() self.http_last_use[host] = time.time()
+24 -3
View File
@@ -1,10 +1,12 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.helpers.encoding import toUnicode, simplifyString from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library, LibraryTitle, File from couchpotato.core.settings.model import Library, LibraryTitle, File
from string import ascii_letters from string import ascii_letters
import time
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -16,6 +18,8 @@ class LibraryPlugin(Plugin):
def __init__(self): def __init__(self):
addEvent('library.add', self.add) addEvent('library.add', self.add)
addEvent('library.update', self.update) addEvent('library.update', self.update)
addEvent('library.update_release_date', self.updateReleaseDate)
def add(self, attrs = {}, update_after = True): def add(self, attrs = {}, update_after = True):
@@ -45,7 +49,7 @@ class LibraryPlugin(Plugin):
# Update library info # Update library info
if update_after is not False: if update_after is not False:
handle = fireEventAsync if update_after is 'async' else fireEvent 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) return l.to_dict(self.default_dict)
@@ -86,10 +90,11 @@ class LibraryPlugin(Plugin):
for title in titles: for title in titles:
if not title: if not title:
continue continue
title = toUnicode(title)
t = LibraryTitle( t = LibraryTitle(
title = toUnicode(title), title = title,
simple_title = self.simplifyTitle(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) library.titles.append(t)
@@ -117,6 +122,22 @@ class LibraryPlugin(Plugin):
return library_dict 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): def simplifyTitle(self, title):
title = toUnicode(title) title = toUnicode(title)
+19 -17
View File
@@ -1,10 +1,8 @@
from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
from couchpotato.core.helpers.request import jsonified, getParams from couchpotato.core.helpers.request import jsonified, getParams
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import File
from couchpotato.environment import Env from couchpotato.environment import Env
import os import os
import time import time
@@ -14,8 +12,6 @@ log = CPLog(__name__)
class Manage(Plugin): class Manage(Plugin):
last_update = 0
def __init__(self): def __init__(self):
fireEvent('scheduler.interval', identifier = 'manage.update_library', handle = self.updateLibrary, hours = 2) 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 return
directories = self.directories() directories = self.directories()
added_identifiers = []
for directory in directories: for directory in directories:
@@ -57,24 +55,28 @@ class Manage(Plugin):
continue continue
log.info('Updating manage library: %s' % directory) log.info('Updating manage library: %s' % directory)
fireEvent('scanner.folder', folder = directory) identifiers = fireEvent('scanner.folder', folder = directory, newer_than = last_update, single = True)
if identifiers:
# If cleanup option is enabled, remove offline files from database added_identifiers.extend(identifiers)
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()
# Break if CP wants to shut down # Break if CP wants to shut down
if self.shuttingDown(): if self.shuttingDown():
break 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): def directories(self):
try: try:
return self.conf('library', default = '').split('::') return [x.strip() for x in self.conf('library', default = '').split('::')]
except: except:
return [] return []
+41 -30
View File
@@ -1,8 +1,10 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode, \
from couchpotato.core.helpers.request import getParams, jsonified 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.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie, Library, LibraryTitle 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.orm import joinedload_all
from sqlalchemy.sql.expression import or_, asc, not_ from sqlalchemy.sql.expression import or_, asc, not_
from string import ascii_lowercase from string import ascii_lowercase
from urllib import urlencode
log = CPLog(__name__) 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.'}, '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', 'desc': 'Delete a movie from the wanted list',
'params': { 'params': {
'id': {'desc': 'Movie ID(s) you want to delete.', 'type': 'int (comma separated)'}, '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.add', self.add)
addEvent('movie.delete', self.delete)
addEvent('movie.get', self.get) addEvent('movie.get', self.get)
addEvent('movie.list', self.list) addEvent('movie.list', self.list)
addEvent('movie.restatus', self.restatus) addEvent('movie.restatus', self.restatus)
@@ -137,7 +139,7 @@ class MoviePlugin(Plugin):
if limit_offset: if limit_offset:
splt = limit_offset.split(',') splt = [x.strip() for x in limit_offset.split(',')]
limit = splt[0] limit = splt[0]
offset = 0 if len(splt) is 1 else splt[1] offset = 0 if len(splt) is 1 else splt[1]
q2 = q2.limit(limit).offset(offset) q2 = q2.limit(limit).offset(offset)
@@ -224,7 +226,6 @@ class MoviePlugin(Plugin):
if title.default: default_title = title.title if title.default: default_title = title.title
if movie: if movie:
#addEvent('library.update.after', )
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True) fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True)
fireEventAsync('searcher.single', movie.to_dict(self.default_dict)) fireEventAsync('searcher.single', movie.to_dict(self.default_dict))
@@ -234,12 +235,16 @@ class MoviePlugin(Plugin):
def search(self): def search(self):
params = getParams() q = getParam('q')
cache_key = '%s/%s' % (__name__, urlencode(params)) cache_key = u'%s/%s' % (__name__, simplifyString(q))
movies = Env.get('cache').get(cache_key) movies = Env.get('cache').get(cache_key)
if not movies: 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) Env.get('cache').set(cache_key, movies)
return jsonified({ return jsonified({
@@ -264,7 +269,8 @@ class MoviePlugin(Plugin):
if not m: if not m:
m = Movie( m = Movie(
library_id = library.get('id'), 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) db.add(m)
fireEvent('library.update', params.get('identifier'), default_title = params.get('title', '')) 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) 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: for movie_id in ids:
m = db.query(Movie).filter_by(id = movie_id).first() m = db.query(Movie).filter_by(id = movie_id).first()
@@ -333,7 +339,7 @@ class MoviePlugin(Plugin):
# Default title # Default title
if params.get('default_title'): if params.get('default_title'):
for title in m.library.titles: 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() db.commit()
@@ -346,23 +352,29 @@ class MoviePlugin(Plugin):
'success': True, 'success': True,
}) })
def delete(self): def deleteView(self):
params = getParams() params = getParams()
db = get_session()
status = fireEvent('status.add', 'deleted', single = True) ids = [x.strip() for x in params.get('id').split(',')]
ids = params.get('id').split(',')
for movie_id in ids: for movie_id in ids:
movie = db.query(Movie).filter_by(id = movie_id).first() self.delete(movie_id)
movie.status_id = status.get('id')
db.commit()
return jsonified({ return jsonified({
'success': True, '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): def restatus(self, movie_id):
active_status = fireEvent('status.get', 'active', single = True) 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() 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)) 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: m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
for release in m.releases:
if t.quality.identifier is release.quality.identifier and (release.status_id is done_status.get('id') and t.finish):
move_to_wanted = False
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
db.commit() db.commit()
@@ -279,10 +279,11 @@
border: 0; border: 0;
} }
.movies .options .table .provider { .movies .options .table .provider {
width: 130px; width: 120px;
text-overflow: ellipsis;
} }
.movies .options .table .name { .movies .options .table .name {
width: 370px; width: 350px;
overflow: hidden; overflow: hidden;
text-align: left; text-align: left;
padding: 0 10px; padding: 0 10px;
@@ -290,6 +291,8 @@
.movies .options .table.files .name { width: 605px; } .movies .options .table.files .name { width: 605px; }
.movies .options .table .type { width: 130px; } .movies .options .table .type { width: 130px; }
.movies .options .table .is_available { width: 90px; } .movies .options .table .is_available { width: 90px; }
.movies .options .table .age,
.movies .options .table .size { width: 40px; }
.movies .options .table a { .movies .options .table a {
width: 30px !important; width: 30px !important;
+15 -4
View File
@@ -79,7 +79,7 @@ var Movie = new Class({
var q = self.quality.getElement('.q_id'+ release.quality_id), var q = self.quality.getElement('.q_id'+ release.quality_id),
status = Status.get(release.status_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) var q = self.addQuality(release.quality_id)
if (q) if (q)
q.addClass(status.identifier); q.addClass(status.identifier);
@@ -270,8 +270,9 @@ var ReleaseAction = new Class({
// Header // Header
new Element('div.item.head').adopt( new Element('div.item.head').adopt(
new Element('span.name', {'text': 'Release name'}), new Element('span.name', {'text': 'Release name'}),
new Element('span.status', {'text': 'Status'}),
new Element('span.quality', {'text': 'Quality'}), 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.age', {'text': 'Age'}),
new Element('span.score', {'text': 'Score'}), new Element('span.score', {'text': 'Score'}),
new Element('span.provider', {'text': 'Provider'}) new Element('span.provider', {'text': 'Provider'})
@@ -280,17 +281,27 @@ var ReleaseAction = new Class({
Array.each(self.movie.data.releases, function(release){ Array.each(self.movie.data.releases, function(release){
var status = Status.get(release.status_id), 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', { new Element('div', {
'class': 'item ' + status.identifier 'class': 'item'
}).adopt( }).adopt(
new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}), 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.quality', {'text': quality.get('label')}),
new Element('span.size', {'text': (self.get(release, 'size') || 'unknown')}), new Element('span.size', {'text': (self.get(release, 'size') || 'unknown')}),
new Element('span.age', {'text': self.get(release, 'age')}), new Element('span.age', {'text': self.get(release, 'age')}),
new Element('span.score', {'text': self.get(release, 'score')}), new Element('span.score', {'text': self.get(release, 'score')}),
new Element('span.provider', {'text': self.get(release, 'provider')}), 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', { new Element('a.download.icon', {
'events': { 'events': {
'click': function(e){ 'click': function(e){
@@ -148,6 +148,9 @@ Block.Search = new Class({
$(m).inject(self.results) $(m).inject(self.results)
self.movies[movie.imdb || 'r-'+Math.floor(Math.random()*10000)] = m self.movies[movie.imdb || 'r-'+Math.floor(Math.random()*10000)] = m
if(q == movie.imdb)
m.showOptions()
}); });
if(q != self.q()) if(q != self.q())
@@ -158,7 +161,7 @@ Block.Search = new Class({
rc = self.result_container.getCoordinates(); rc = self.result_container.getCoordinates();
self.results.setStyle('max-height', (w.y - rc.top - 50) + 'px') 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', { self.mask = new Element('span.mask', {
'styles': { 'styles': {
'position': 'relative',
'width': s.x, 'width': s.x,
'height': s.y 'height': s.y,
'top': -s.y,
'display': 'block'
} }
}).inject(self.options).fade('hide').position({ }).inject(self.options).fade('hide')
'relativeTo': self.options
})
createSpinner(self.mask) createSpinner(self.mask)
self.mask.fade('in') self.mask.fade('in')
+14
View File
@@ -21,9 +21,23 @@ class ProfilePlugin(Plugin):
addApiView('profile.save', self.save) addApiView('profile.save', self.save)
addApiView('profile.save_order', self.saveOrder) addApiView('profile.save_order', self.saveOrder)
addApiView('profile.delete', self.delete) 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) addEvent('app.initialize', self.fill, priority = 90)
def allView(self):
return jsonified({
'success': True,
'list': self.all()
})
def all(self): def all(self):
db = get_session() db = get_session()
+24 -5
View File
@@ -1,7 +1,7 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent 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.request import jsonified, getParams
from couchpotato.core.helpers.variable import mergeDicts, md5, getExt from couchpotato.core.helpers.variable import mergeDicts, md5, getExt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
@@ -16,9 +16,9 @@ log = CPLog(__name__)
class QualityPlugin(Plugin): class QualityPlugin(Plugin):
qualities = [ 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': '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': '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': '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']}, {'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.all', self.all)
addEvent('quality.single', self.single) addEvent('quality.single', self.single)
addEvent('quality.guess', self.guess) addEvent('quality.guess', self.guess)
addEvent('quality.pre_releases', self.preReleases)
addApiView('quality.size.save', self.saveSize) 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) 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): def all(self):
db = get_session() db = get_session()
@@ -138,7 +156,7 @@ class QualityPlugin(Plugin):
# Create hash for cache # Create hash for cache
hash = md5(str([f.replace('.' + getExt(f), '') for f in files])) hash = md5(str([f.replace('.' + getExt(f), '') for f in files]))
cached = self.getCache(hash) cached = self.getCache(hash)
if cached: return cached if cached and extra is {}: return cached
for cur_file in files: for cur_file in files:
size = (os.path.getsize(cur_file) / 1024 / 1024) if os.path.isfile(cur_file) else 0 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', []): for tag in quality.get('tags', []):
if isinstance(tag, tuple) and '.'.join(tag) in '.'.join(words): 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) return self.setCache(hash, quality)
if list(set(quality.get('tags', [])) & set(words)): if list(set(quality.get('tags', [])) & set(words)):
@@ -182,5 +201,5 @@ class QualityPlugin(Plugin):
if quality: if quality:
return self.setCache(hash, 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')) return self.setCache(hash, self.single('dvdrip'))
+1 -1
View File
@@ -147,7 +147,7 @@ class Release(Plugin):
'releases': {'status': {}, 'quality': {}}, 'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files':{}}, 'library': {'titles': {}, 'files':{}},
'files': {} 'files': {}
})) }), manual = True)
return jsonified({ return jsonified({
'success': True 'success': True
+17 -4
View File
@@ -192,11 +192,16 @@ class Renamer(Plugin):
if file_type is 'leftover': if file_type is 'leftover':
if self.conf('move_leftover'): if self.conf('move_leftover'):
rename_files[current_file] = os.path.join(destination, final_folder_name, os.path.basename(current_file)) 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) rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
# Check for extra subtitle files # Check for extra subtitle files
if file_type is 'subtitle': 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( rename_extras = self.getRenameExtras(
extra_type = 'subtitle_extra', extra_type = 'subtitle_extra',
replacements = replacements, replacements = replacements,
@@ -206,6 +211,14 @@ class Renamer(Plugin):
group = group, group = group,
current_file = current_file 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) rename_files = mergeDicts(rename_files, rename_extras)
# Filename without cd etc # Filename without cd etc
@@ -315,7 +328,7 @@ class Renamer(Plugin):
try: try:
os.remove(src) os.remove(src)
except: except:
log.error('Failed removing %s: %s', (src, traceback.format_exc())) log.error('Failed removing %s: %s' % (src, traceback.format_exc()))
# Remove matching releases # Remove matching releases
for release in remove_releases: for release in remove_releases:
@@ -323,14 +336,14 @@ class Renamer(Plugin):
try: try:
db.delete(release) db.delete(release)
except: 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']: if group['dirname'] and group['parentdir']:
try: try:
log.info('Deleting folder: %s' % group['parentdir']) log.info('Deleting folder: %s' % group['parentdir'])
self.deleteEmptyFolder(group['parentdir']) self.deleteEmptyFolder(group['parentdir'])
except: 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 # Search for trailers etc
fireEventAsync('renamer.after', group) fireEventAsync('renamer.after', group)
+86 -24
View File
@@ -7,6 +7,8 @@ from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import File from couchpotato.core.settings.model import File
from couchpotato.environment import Env from couchpotato.environment import Env
from enzyme.exceptions import NoParserError, ParseError from enzyme.exceptions import NoParserError, ParseError
from guessit import guess_movie_info
from subliminal.videos import scan
import enzyme import enzyme
import logging import logging
import os 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 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'] ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate']
extensions = { 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'], 'movie_extra': ['mds'],
'dvd': ['vts_*', 'vob'], 'dvd': ['vts_*', 'vob'],
'nfo': ['nfo', 'txt', 'tag'], 'nfo': ['nfo', 'txt', 'tag'],
@@ -95,6 +97,10 @@ class Scanner(Plugin):
addEvent('rename.after', after_rename) 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): def scanFilesToLibrary(self, folder = None, files = None):
groups = self.scan(folder = folder, files = files) groups = self.scan(folder = folder, files = files)
@@ -103,21 +109,14 @@ class Scanner(Plugin):
if group['library']: if group['library']:
fireEvent('release.add', group = group) 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): if not os.path.isdir(folder):
return return
groups = self.scan(folder = folder) groups = self.scan(folder = folder)
# Open up the db added_identifier = []
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()
while True and not self.shuttingDown(): while True and not self.shuttingDown():
try: try:
identifier, group = groups.popitem() identifier, group = groups.popitem()
@@ -129,9 +128,11 @@ class Scanner(Plugin):
# Add release # Add release
fireEvent('release.add', group = group) 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 = []): def scan(self, folder = None, files = []):
@@ -164,6 +165,9 @@ class Scanner(Plugin):
for file_path in files: for file_path in files:
if not os.path.exists(file_path):
continue
# Remove ignored files # Remove ignored files
if self.isSampleFile(file_path): if self.isSampleFile(file_path):
leftovers.append(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 = self.createStringIdentifier(file_path, folder, exclude_filename = is_dvd_file)
# Identifier with quality # 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', '')) identifier_with_quality = '%s %s' % (identifier, quality.get('identifier', ''))
if not movie_files.get(identifier): if not movie_files.get(identifier):
@@ -208,7 +212,7 @@ class Scanner(Plugin):
for identifier, group in movie_files.iteritems(): for identifier, group in movie_files.iteritems():
if identifier not in group['identifiers'] and len(identifier) > 0: group['identifiers'].append(identifier) 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']: for file_path in group['unsorted_files']:
wo_ext = file_path[:-(len(getExt(file_path)) + 1)] wo_ext = file_path[:-(len(getExt(file_path)) + 1)]
@@ -233,7 +237,7 @@ class Scanner(Plugin):
# Group the files based on the identifier # Group the files based on the identifier
for identifier, group in movie_files.iteritems(): 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, [])) found_files = set(self.path_identifiers.get(identifier, []))
group['unsorted_files'].extend(found_files) group['unsorted_files'].extend(found_files)
@@ -291,6 +295,9 @@ class Scanner(Plugin):
log.debug('Getting metadata for %s' % identifier) log.debug('Getting metadata for %s' % identifier)
group['meta_data'] = self.getMetaData(group) group['meta_data'] = self.getMetaData(group)
# Subtitle meta
group['subtitle_language'] = self.getSubtitleLanguage(group)
# Get parent dir from movie files # Get parent dir from movie files
for movie_file in group['files']['movie']: for movie_file in group['files']['movie']:
group['parentdir'] = os.path.dirname(movie_file) group['parentdir'] = os.path.dirname(movie_file)
@@ -381,6 +388,42 @@ class Scanner(Plugin):
return {} 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): def determineMovie(self, group):
imdb_id = None imdb_id = None
@@ -433,12 +476,17 @@ class Scanner(Plugin):
for identifier in group['identifiers']: for identifier in group['identifiers']:
if len(identifier) > 2: 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: name_year = self.getReleaseNameYear(identifier, file_name = filename)
imdb_id = movie[0]['imdb'] if name_year.get('name') and name_year.get('year'):
log.debug('Found movie via search: %s' % cur_file) movie = fireEvent('movie.search', q = '%(name)s %(year)s' % name_year, merge = True, limit = 1)
if imdb_id: break
if len(movie) > 0:
imdb_id = movie[0]['imdb']
log.debug('Found movie via search: %s' % cur_file)
if imdb_id: break
else: else:
log.debug('Identifier to short to use for search: %s' % identifier) log.debug('Identifier to short to use for search: %s' % identifier)
@@ -645,13 +693,27 @@ class Scanner(Plugin):
return None return None
def findYear(self, text): 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: if matches:
return matches.group('year') return matches.group('year')
return '' 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 = ' '.join(re.split('\W+', simplifyString(release_name)))
cleaned = re.sub(self.clean, ' ', cleaned) cleaned = re.sub(self.clean, ' ', cleaned)
year = self.findYear(cleaned) year = self.findYear(cleaned)
@@ -661,7 +723,7 @@ class Scanner(Plugin):
movie_name = cleaned.split(year).pop(0).strip() movie_name = cleaned.split(year).pop(0).strip()
return { return {
'name': movie_name, 'name': movie_name,
'year': year, 'year': int(year),
} }
except: except:
pass pass
@@ -670,7 +732,7 @@ class Scanner(Plugin):
movie_name = cleaned.split(' ').pop(0).strip() movie_name = cleaned.split(' ').pop(0).strip()
return { return {
'name': movie_name, 'name': movie_name,
'year': year, 'year': int(year),
} }
except: except:
pass pass
+1 -1
View File
@@ -40,7 +40,7 @@ def nameScore(name, year):
# Contains preferred word # Contains preferred word
nzb_words = re.split('\W+', simplifyString(name)) 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: for word in preferred_words:
if word.strip() and word.strip().lower() in nzb_words: if word.strip() and word.strip().lower() in nzb_words:
score = score + 100 score = score + 100
@@ -77,7 +77,7 @@ config = [{
'options': [ 'options': [
{ {
'name': 'retention', 'name': 'retention',
'default': 350, 'default': 1000,
'type': 'int', 'type': 'int',
'unit': 'days' 'unit': 'days'
}, },
+78 -11
View File
@@ -7,7 +7,9 @@ from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
from couchpotato.environment import Env from couchpotato.environment import Env
from sqlalchemy.exc import InterfaceError from sqlalchemy.exc import InterfaceError
import datetime
import re import re
import time
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -15,6 +17,8 @@ log = CPLog(__name__)
class Searcher(Plugin): class Searcher(Plugin):
in_progress = False
def __init__(self): def __init__(self):
addEvent('searcher.all', self.all_movies) addEvent('searcher.all', self.all_movies)
addEvent('searcher.single', self.single) addEvent('searcher.single', self.single)
@@ -26,6 +30,12 @@ class Searcher(Plugin):
def all_movies(self): def all_movies(self):
if self.in_progress:
log.info('Search already in progress')
return
self.in_progress = True
db = get_session() db = get_session()
movies = db.query(Movie).filter( movies = db.query(Movie).filter(
@@ -51,12 +61,19 @@ class Searcher(Plugin):
if self.shuttingDown(): if self.shuttingDown():
break break
self.in_progress = False
def single(self, movie): 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) available_status = fireEvent('status.get', 'available', single = True)
default_title = movie['library']['titles'][0]['title'] default_title = movie['library']['titles'][0]['title']
for quality_type in movie['profile']['types']: 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 has_better_quality = 0
@@ -72,6 +89,8 @@ class Searcher(Plugin):
quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True) quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True)
results = fireEvent('yarr.search', movie, quality, merge = True) results = fireEvent('yarr.search', movie, quality, merge = True)
sorted_results = sorted(results, key = lambda k: k['score'], reverse = 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 # Add them to this movie releases list
for nzb in sorted_results: for nzb in sorted_results:
@@ -104,7 +123,11 @@ class Searcher(Plugin):
for nzb in sorted_results: 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: else:
log.info('Better quality (%s) already available or snatched for %s' % (quality_type['quality']['label'], default_title)) log.info('Better quality (%s) already available or snatched for %s' % (quality_type['quality']['label'], default_title))
fireEvent('movie.restatus', movie['id']) fireEvent('movie.restatus', movie['id'])
@@ -116,10 +139,10 @@ class Searcher(Plugin):
return False return False
def download(self, data, movie): def download(self, data, movie, manual = False):
snatched_status = fireEvent('status.get', 'snatched', single = True) 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: 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'])) log.info('Wrong: Outside retention, age is %s, needs %s or lower: %s' % (nzb['age'], retention, nzb['name']))
return False return False
nzb_words = re.split('\W+', simplifyString(nzb['name'])) movie_name = simplifyString(nzb['name'])
required_words = self.conf('required_words').split(',') 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)): 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.") log.info("NZB doesn't contain any of the required words.")
return False 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)) blacklisted = list(set(nzb_words) & set(ignored_words))
if self.conf('ignored_words') and blacklisted: if self.conf('ignored_words') and blacklisted:
log.info("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted))) log.info("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted)))
return False 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) #qualities = fireEvent('quality.all', single = True)
preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True) preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True)
# Contains lower quality string # 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'])) log.info('Wrong: %s, looking for %s' % (nzb['name'], quality['label']))
return False 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'])) log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie['library']['titles'][0]['title'], movie['library']['year']))
return False 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)) nzb_words = re.split('\W+', simplifyString(name))
qualities = fireEvent('quality.all', single = True) qualities = fireEvent('quality.all', single = True)
@@ -246,6 +278,14 @@ class Searcher(Plugin):
if list(set(nzb_words) & set(quality['alternative'])): if list(set(nzb_words) & set(quality['alternative'])):
found[quality['identifier']] = True 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 # Allow other qualities
for allowed in preferred_quality.get('allow'): for allowed in preferred_quality.get('allow'):
if found.get(allowed): if found.get(allowed):
@@ -284,10 +324,10 @@ class Searcher(Plugin):
check_movie = fireEvent('scanner.name_year', check_name, single = True) check_movie = fireEvent('scanner.name_year', check_name, single = True)
try: try:
check_words = re.split('\W+', check_movie.get('name', '')) check_words = filter(None, re.split('\W+', check_movie.get('name', '')))
movie_words = re.split('\W+', simplifyString(movie_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 return True
except: except:
pass pass
@@ -306,3 +346,30 @@ class Searcher(Plugin):
pass pass
return nfo and getImdb(nfo) == imdb_id 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', 'name': 'languages',
'description': 'The languages you want to download the sub', 'description': 'Comma separated, 2 letter country code. Example: en, nl',
},
{
'name': 'automatic',
'default': True,
'type': 'bool',
'description': 'Automaticly search & download for movies in library',
}, },
# {
# 'name': 'automatic',
# 'default': True,
# 'type': 'bool',
# 'description': 'Automaticly search & download for movies in library',
# },
], ],
}, },
], ],
+16 -11
View File
@@ -3,15 +3,18 @@ from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library, FileType from couchpotato.core.settings.model import Library, FileType
from couchpotato.environment import Env
import subliminal
log = CPLog(__name__) log = CPLog(__name__)
class Subtitle(Plugin): class Subtitle(Plugin):
services = ['opensubtitles', 'thesubdb', 'subswiki']
def __init__(self): def __init__(self):
pass addEvent('renamer.before', self.searchSingle)
#addEvent('renamer.before', self.searchSingle)
def searchLibrary(self): def searchLibrary(self):
@@ -33,20 +36,22 @@ class Subtitle(Plugin):
files.append(file.path); files.append(file.path);
# get subtitles for those files # get subtitles for those files
subtitles = fireEvent('subtitle.search', files = files, languages = self.getLanguages(), merge = True) subliminal.list_subtitles(files, cache_dir = Env.get('cache_dir'), multi = True, languages = self.getLanguages(), services = self.services)
# do something with the returned subtitles
print subtitles
def searchSingle(self, group): def searchSingle(self, group):
if self.isDisabled(): return 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 for d_sub in downloaded:
print subtitles group['files']['subtitle'].add(d_sub.path)
group['subtitle_language'][d_sub.path] = [d_sub.language]
def getLanguages(self): 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 // Styles
GM_addStyle('\ 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 { 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#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#close_button { cursor:pointer; float: right; padding:120px 10px 10px; } \
#cp_popup a img { vertical-align: middle; } \ #cp_popup a img { vertical-align: middle; } \
@@ -98,12 +98,14 @@ var osd = function(){
catch(e){} catch(e){}
popup.innerHTML = ''; popup.innerHTML = '';
popup.setAttribute('class', 'opened');
popup.appendChild(create('a', { popup.appendChild(create('a', {
'innerHTML': '<img src="' + close_img + '" />', 'innerHTML': '<img src="' + close_img + '" />',
'id': 'close_button', 'id': 'close_button',
'onclick': function(){ 'onclick': function(){
popup.innerHTML = ''; popup.innerHTML = '';
popup.appendChild(add_button); popup.appendChild(add_button);
popup.setAttribute('class', '');
} }
})); }));
popup.appendChild(iframe) popup.appendChild(iframe)
@@ -10,7 +10,7 @@ config = [{
'tab': 'automation', 'tab': 'automation',
'name': 'imdb_automation', 'name': 'imdb_automation',
'label': 'IMDB', 'label': 'IMDB',
'description': 'From any public IMDB watchlists', 'description': 'From any <strong>public</strong> IMDB watchlists',
'options': [ 'options': [
{ {
'name': 'automation_enabled', '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', 'tab': 'automation',
'name': 'trakt_automation', 'name': 'trakt_automation',
'label': 'Trakt', 'label': 'Trakt',
'description': 'from watchlist', 'description': 'import movies from your own watchlist',
'options': [ 'options': [
{ {
'name': 'automation_enabled', 'name': 'automation_enabled',
+7 -3
View File
@@ -19,7 +19,7 @@ class Provider(Plugin):
def isAvailable(self, test_url): def isAvailable(self, test_url):
if Env.get('debug'): return True if Env.get('dev'): return True
now = time.time() now = time.time()
host = urlparse(test_url).hostname host = urlparse(test_url).hostname
@@ -65,9 +65,13 @@ class YarrProvider(Provider):
def belongsTo(self, url, host = None): def belongsTo(self, url, host = None):
try: try:
hostname = urlparse(url).hostname hostname = urlparse(url).hostname
download_url = host if host else self.urls['download'] if host and hostname in host:
if hostname in download_url:
return self return self
else:
for url_type in self.urls:
download_url = self.urls[url_type]
if hostname in download_url:
return self
except: except:
log.debug('Url % s doesn\'t belong to %s' % (url, self.getName())) 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', 'name': 'meta_nfo',
'label': 'NFO',
'default': True, 'default': True,
'type': 'bool', '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', 'name': 'meta_fanart',
'label': 'Fanart',
'default': True, 'default': True,
'type': 'bool', 'type': 'bool',
}, },
{
'name': 'meta_fanart_name',
'label': 'Fanart filename',
'default': '%s-fanart.jpg',
'advanced': True,
},
{ {
'name': 'meta_thumbnail', 'name': 'meta_thumbnail',
'label': 'Thumbnail',
'default': True, 'default': True,
'type': 'bool', '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']) return os.path.join(data['destination_dir'], data['filename'])
def getFanartName(self, root): def getFanartName(self, root):
return '%s-fanart.jpg' % root return self.conf('meta_fanart_name') % root
def getThumbnailName(self, root): def getThumbnailName(self, root):
return '%s.tbn' % root return self.conf('meta_thumbnail_name') % root
def getNfoName(self, root): def getNfoName(self, root):
return '%s.nfo' % root return self.conf('meta_nfo_name') % root
def getNfo(self, movie_info = {}, data = {}): def getNfo(self, movie_info = {}, data = {}):
nfoxml = Element('movie') nfoxml = Element('movie')
@@ -1,6 +1,5 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.request import jsonified, getParams from couchpotato.core.helpers.request import jsonified, getParams
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.movie.base import MovieProvider from couchpotato.core.providers.movie.base import MovieProvider
@@ -12,26 +11,37 @@ log = CPLog(__name__)
class CouchPotatoApi(MovieProvider): class CouchPotatoApi(MovieProvider):
apiUrl = 'http://couchpota.to/api/%s/' api_url = 'http://couchpota.to/api/%s/'
http_time_between_calls = 0
def __init__(self): 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: 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) 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: 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 = []): def suggest(self, movies = [], ignore = []):
try: 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) suggestions = json.loads(data)
log.info('Found Suggestions for %s' % (suggestions)) log.info('Found Suggestions for %s' % (suggestions))
except Exception, e: except Exception, e:
@@ -1,8 +1,8 @@
from couchpotato.core.event import addEvent, fireEvent 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.helpers.variable import tryInt, tryFloat
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.movie.base import MovieProvider from couchpotato.core.providers.movie.base import MovieProvider
from urllib import urlencode
import json import json
import re import re
import traceback import traceback
@@ -27,8 +27,11 @@ class IMDBAPI(MovieProvider):
name_year = fireEvent('scanner.name_year', q, single = True) 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 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: if cached:
result = self.parseMovie(cached) result = self.parseMovie(cached)
@@ -42,6 +45,9 @@ class IMDBAPI(MovieProvider):
def getInfo(self, identifier = None): def getInfo(self, identifier = None):
if not identifier:
return {}
cache_key = 'imdbapi.cache.%s' % identifier cache_key = 'imdbapi.cache.%s' % identifier
cached = self.getCache(cache_key, self.urls['info'] % 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': if tmp_movie.get(key).lower() == 'n/a':
del movie[key] del movie[key]
year = tryInt(movie.get('Year', ''))
movie_data = { movie_data = {
'titles': [movie.get('Title')] if movie.get('Title') else [], 'titles': [movie.get('Title')] if movie.get('Title') else [],
'original_title': movie.get('Title', ''), 'original_title': movie.get('Title', ''),
@@ -82,7 +90,7 @@ class IMDBAPI(MovieProvider):
'imdb': str(movie.get('ID', '')), 'imdb': str(movie.get('ID', '')),
'runtime': self.runtimeToMinutes(movie.get('Runtime', '')), 'runtime': self.runtimeToMinutes(movie.get('Runtime', '')),
'released': movie.get('Released', ''), 'released': movie.get('Released', ''),
'year': movie.get('Year', ''), 'year': year if isinstance(year, (int)) else None,
'plot': movie.get('Plot', ''), 'plot': movie.get('Plot', ''),
'genres': movie.get('Genre', '').split(','), 'genres': movie.get('Genre', '').split(','),
'directors': movie.get('Director', '').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.logger import CPLog
from couchpotato.core.providers.movie.base import MovieProvider from couchpotato.core.providers.movie.base import MovieProvider
from libs.themoviedb import tmdb from libs.themoviedb import tmdb
import re
log = CPLog(__name__) log = CPLog(__name__)
@@ -67,8 +68,14 @@ class TheMovieDb(MovieProvider):
if raw: if raw:
try: try:
nr = 0 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)) results.append(self.parseMovie(movie))
nr += 1 nr += 1
@@ -1,8 +1,8 @@
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider from couchpotato.core.providers.nzb.base import NZBProvider
from dateutil.parser import parse from dateutil.parser import parse
from urllib import quote_plus
import re import re
import time import time
@@ -27,7 +27,7 @@ class Moovee(NZBProvider):
return results return results
q = '%s %s' % (movie['library']['titles'][0]['title'], quality.get('identifier')) 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 cache_key = 'moovee.%s' % q
data = self.getCache(cache_key, url) data = self.getCache(cache_key, url)
@@ -1,11 +1,10 @@
from BeautifulSoup import BeautifulSoup from BeautifulSoup import BeautifulSoup
from couchpotato.core.event import fireEvent 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.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider from couchpotato.core.providers.nzb.base import NZBProvider
from couchpotato.environment import Env from couchpotato.environment import Env
import urllib
log = CPLog(__name__) log = CPLog(__name__)
@@ -13,9 +12,9 @@ log = CPLog(__name__)
class Mysterbin(NZBProvider): class Mysterbin(NZBProvider):
urls = { urls = {
'search': 'http://www.mysterbin.com/advsearch?%s', 'search': 'https://www.mysterbin.com/advsearch?%s',
'download': 'http://www.mysterbin.com/nzb?c=%s', 'download': 'https://www.mysterbin.com/nzb?c=%s',
'nfo': 'http://www.mysterbin.com/nfo?c=%s', 'nfo': 'https://www.mysterbin.com/nfo?c=%s',
} }
http_time_between_calls = 1 #seconds http_time_between_calls = 1 #seconds
@@ -40,8 +39,8 @@ class Mysterbin(NZBProvider):
'nopasswd': 'on', 'nopasswd': 'on',
} }
cache_key = 'mysterbin.%s' % q cache_key = 'mysterbin.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
data = self.getCache(cache_key, self.urls['search'] % urllib.urlencode(params)) data = self.getCache(cache_key, self.urls['search'] % tryUrlencode(params))
if data: if data:
try: try:
@@ -1,9 +1,9 @@
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.rss import RSS
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider from couchpotato.core.providers.nzb.base import NZBProvider
from dateutil.parser import parse from dateutil.parser import parse
from urllib import urlencode
import base64 import base64
import time import time
import xml.etree.ElementTree as XMLTree import xml.etree.ElementTree as XMLTree
@@ -14,7 +14,7 @@ log = CPLog(__name__)
class Newzbin(NZBProvider, RSS): class Newzbin(NZBProvider, RSS):
urls = { urls = {
'download': 'http://www.newzbin2.es/api/dnzb/', 'download': 'https://www.newzbin2.es/api/dnzb/',
'search': 'https://www.newzbin2.es/search/', 'search': 'https://www.newzbin2.es/search/',
} }
@@ -45,7 +45,7 @@ class Newzbin(NZBProvider, RSS):
format_id = self.getFormatId(type) format_id = self.getFormatId(type)
cat_id = self.getCatId(type) cat_id = self.getCatId(type)
arguments = urlencode({ arguments = tryUrlencode({
'searchaction': 'Search', 'searchaction': 'Search',
'u_url_posts_only': '0', 'u_url_posts_only': '0',
'u_show_passworded': '0', 'u_show_passworded': '0',
@@ -89,11 +89,14 @@ class Newzbin(NZBProvider, RSS):
title = self.getTextElement(nzb, "title") title = self.getTextElement(nzb, "title")
if 'error' in title.lower(): continue 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 # Add attributes to name
for attr in nzb.find('{%s}attributes' % REPORT_NS): try:
title += ' ' + attr.text for attr in nzb.find('{%s}attributes' % REPORT_NS):
title += ' ' + attr.text
except:
pass
id = int(self.getTextElement(nzb, '{%s}id' % REPORT_NS)) id = int(self.getTextElement(nzb, '{%s}id' % REPORT_NS))
size = str(int(self.getTextElement(nzb, '{%s}size' % REPORT_NS)) / 1024 / 1024) + ' mb' size = str(int(self.getTextElement(nzb, '{%s}size' % REPORT_NS)) / 1024 / 1024) + ' mb'
@@ -19,16 +19,16 @@ config = [{
}, },
{ {
'name': 'use', 'name': 'use',
'default': '0' 'default': '0,0,0'
}, },
{ {
'name': 'host', 'name': 'host',
'default': 'nzb.su', 'default': 'nzb.su,dognzb.cr,beta.nzbs.org',
'description': 'The hostname of your newznab provider', 'description': 'The hostname of your newznab provider',
}, },
{ {
'name': 'api_key', 'name': 'api_key',
'default': '', 'default': ',,',
'label': 'Api Key', 'label': 'Api Key',
'description': 'Can be found on your profile page', 'description': 'Can be found on your profile page',
'type': 'combined', 'type': 'combined',
@@ -1,10 +1,10 @@
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import cleanHost from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider from couchpotato.core.providers.nzb.base import NZBProvider
from dateutil.parser import parse from dateutil.parser import parse
from urllib import urlencode
import time import time
import xml.etree.ElementTree as XMLTree 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'])): if self.isDisabled(host) or not self.isAvailable(self.getUrl(host['host'], self.urls['search'])):
return results return results
arguments = urlencode({ arguments = tryUrlencode({
't': self.cat_backup_id, 't': self.cat_backup_id,
'r': host['api_key'], 'r': host['api_key'],
'i': 58, 'i': 58,
@@ -81,11 +81,10 @@ class Newznab(NZBProvider, RSS):
return results return results
cat_id = self.getCatId(quality['identifier']) cat_id = self.getCatId(quality['identifier'])
arguments = urlencode({ arguments = tryUrlencode({
'imdbid': movie['library']['identifier'].replace('tt', ''), 'imdbid': movie['library']['identifier'].replace('tt', ''),
'cat': cat_id[0], 'cat': cat_id[0],
'apikey': host['api_key'], 'apikey': host['api_key'],
't': self.urls['search'],
'extended': 1 'extended': 1
}) })
url = "%s&%s" % (self.getUrl(host['host'], self.urls['search']), arguments) 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': elif item.attrib.get('name') == 'usenetdate':
date = item.attrib.get('value') 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 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.info('Size not parsed properly or not available for %s: %s' % (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() id = self.getTextElement(nzb, "guid").split('/')[-1:].pop()
new = { new = {
@@ -158,9 +157,9 @@ class Newznab(NZBProvider, RSS):
def getHosts(self): def getHosts(self):
uses = str(self.conf('use')).split(',') uses = [x.strip() for x in str(self.conf('use')).split(',')]
hosts = self.conf('host').split(',') hosts = [x.strip() for x in self.conf('host').split(',')]
api_keys = self.conf('api_key').split(',') api_keys = [x.strip() for x in self.conf('api_key').split(',')]
list = [] list = []
for nr in range(len(hosts)): for nr in range(len(hosts)):
@@ -1,6 +1,5 @@
from BeautifulSoup import BeautifulSoup, SoupStrainer
from couchpotato.core.event import fireEvent 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.rss import RSS
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
@@ -8,7 +7,6 @@ from couchpotato.core.providers.nzb.base import NZBProvider
from couchpotato.environment import Env from couchpotato.environment import Env
from dateutil.parser import parse from dateutil.parser import parse
import time import time
import urllib
import xml.etree.ElementTree as XMLTree import xml.etree.ElementTree as XMLTree
log = CPLog(__name__) log = CPLog(__name__)
@@ -17,7 +15,7 @@ log = CPLog(__name__)
class NZBClub(NZBProvider, RSS): class NZBClub(NZBProvider, RSS):
urls = { urls = {
'search': 'http://www.nzbclub.com/nzbfeed.aspx?%s', 'search': 'https://www.nzbclub.com/nzbfeed.aspx?%s',
} }
http_time_between_calls = 3 #seconds http_time_between_calls = 3 #seconds
@@ -41,8 +39,8 @@ class NZBClub(NZBProvider, RSS):
'ns': 1, 'ns': 1,
} }
cache_key = 'nzbclub.%s' % q cache_key = 'nzbclub.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
data = self.getCache(cache_key, self.urls['search'] % urllib.urlencode(params)) data = self.getCache(cache_key, self.urls['search'] % tryUrlencode(params))
if data: if data:
try: try:
try: try:
@@ -68,8 +66,8 @@ class NZBClub(NZBProvider, RSS):
'name': toUnicode(self.getTextElement(nzb, "title")), 'name': toUnicode(self.getTextElement(nzb, "title")),
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))), 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': tryInt(size) / 1024 / 1024, 'size': tryInt(size) / 1024 / 1024,
'url': enclosure['url'], 'url': enclosure['url'].replace(' ', '_'),
'download': enclosure['url'].replace(' ', '_'), 'download': self.download,
'detail_url': self.getTextElement(nzb, "link"), 'detail_url': self.getTextElement(nzb, "link"),
'description': description, 'description': description,
} }
@@ -1,12 +1,11 @@
from couchpotato.core.event import fireEvent 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.rss import RSS
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider from couchpotato.core.providers.nzb.base import NZBProvider
from couchpotato.environment import Env from couchpotato.environment import Env
from dateutil.parser import parse from dateutil.parser import parse
from urllib import urlencode
import re import re
import time import time
import xml.etree.ElementTree as XMLTree import xml.etree.ElementTree as XMLTree
@@ -30,7 +29,7 @@ class NzbIndex(NZBProvider, RSS):
return results return results
q = '%s %s %s' % (movie['library']['titles'][0]['title'], movie['library']['year'], quality.get('identifier')) q = '%s %s %s' % (movie['library']['titles'][0]['title'], movie['library']['year'], quality.get('identifier'))
arguments = urlencode({ arguments = tryUrlencode({
'q': q, 'q': q,
'age': Env.setting('retention', 'nzb'), 'age': Env.setting('retention', 'nzb'),
'sort': 'agedesc', 'sort': 'agedesc',
@@ -43,7 +42,7 @@ class NzbIndex(NZBProvider, RSS):
}) })
url = "%s?%s" % (self.urls['api'], arguments) 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) data = self.getCache(cache_key, url)
if data: if data:
@@ -1,10 +1,10 @@
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.rss import RSS
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider from couchpotato.core.providers.nzb.base import NZBProvider
from couchpotato.environment import Env from couchpotato.environment import Env
from dateutil.parser import parse from dateutil.parser import parse
from urllib import urlencode
import time import time
import xml.etree.ElementTree as XMLTree import xml.etree.ElementTree as XMLTree
@@ -16,7 +16,7 @@ class NZBMatrix(NZBProvider, RSS):
urls = { urls = {
'download': 'https://api.nzbmatrix.com/v1.1/download.php?id=%s', 'download': 'https://api.nzbmatrix.com/v1.1/download.php?id=%s',
'detail': 'https://nzbmatrix.com/nzb-details.php?id=%s&hit=1', '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 = [ cat_ids = [
@@ -37,7 +37,7 @@ class NZBMatrix(NZBProvider, RSS):
cat_ids = ','.join(['%s' % x for x in self.getCatId(quality.get('identifier'))]) cat_ids = ','.join(['%s' % x for x in self.getCatId(quality.get('identifier'))])
arguments = urlencode({ arguments = tryUrlencode({
'term': movie['library']['identifier'], 'term': movie['library']['identifier'],
'subcat': cat_ids, 'subcat': cat_ids,
'username': self.conf('username'), 'username': self.conf('username'),
+6 -7
View File
@@ -1,10 +1,9 @@
from couchpotato.core.event import fireEvent 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.helpers.rss import RSS
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider from couchpotato.core.providers.nzb.base import NZBProvider
from dateutil.parser import parse from dateutil.parser import parse
from urllib import urlencode
import time import time
import xml.etree.ElementTree as XMLTree import xml.etree.ElementTree as XMLTree
@@ -14,10 +13,10 @@ log = CPLog(__name__)
class Nzbs(NZBProvider, RSS): class Nzbs(NZBProvider, RSS):
urls = { urls = {
'download': 'http://nzbs.org/index.php?action=getnzb&nzbid=%s%s', 'download': 'https://nzbs.org/index.php?action=getnzb&nzbid=%s%s',
'nfo': 'http://nzbs.org/index.php?action=view&nzbid=%s&nfo=1', 'nfo': 'https://nzbs.org/index.php?action=view&nzbid=%s&nfo=1',
'detail': 'http://nzbs.org/index.php?action=view&nzbid=%s', 'detail': 'https://nzbs.org/index.php?action=view&nzbid=%s',
'api': 'http://nzbs.org/rss.php', 'api': 'https://nzbs.org/rss.php',
} }
cat_ids = [ cat_ids = [
@@ -36,7 +35,7 @@ class Nzbs(NZBProvider, RSS):
return results return results
cat_id = self.getCatId(quality.get('identifier')) cat_id = self.getCatId(quality.get('identifier'))
arguments = urlencode({ arguments = tryUrlencode({
'action':'search', 'action':'search',
'q': simplifyString(movie['library']['titles'][0]['title']), 'q': simplifyString(movie['library']['titles'][0]['title']),
'catid': cat_id[0], 'catid': cat_id[0],
+3 -3
View File
@@ -1,8 +1,8 @@
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider from couchpotato.core.providers.nzb.base import NZBProvider
from urllib import quote_plus
import re import re
log = CPLog(__name__) log = CPLog(__name__)
@@ -26,9 +26,9 @@ class X264(NZBProvider):
return results return results
q = '%s %s %s' % (movie['library']['titles'][0]['title'], movie['library']['year'], quality.get('identifier')) 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) data = self.getCache(cache_key, url)
if data: if data:
match = re.compile(self.regex, re.DOTALL).finditer(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/', 'test': 'http://www.kat.ph/',
'detail': 'http://www.kat.ph/%s-t%s.html', 'detail': 'http://www.kat.ph/%s-t%s.html',
'search': 'http://www.kat.ph/%s-i%s/', 'search': 'http://www.kat.ph/%s-i%s/',
'download': 'http://torcache.net/',
} }
cat_ids = [ cat_ids = [
@@ -36,7 +37,7 @@ class KickAssTorrents(TorrentProvider):
if self.isDisabled() or not self.isAvailable(self.urls['test']): if self.isDisabled() or not self.isAvailable(self.urls['test']):
return results 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', ''))) data = self.getCache(cache_key, self.urls['search'] % (movie['library']['titles'][0]['title'], movie['library']['identifier'].replace('tt', '')))
if data: if data:
@@ -127,7 +128,7 @@ class KickAssTorrents(TorrentProvider):
return tryInt(age) return tryInt(age)
def download(self, url = '', nzb_id = ''): 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) compressedstream = StringIO.StringIO(compressed_data)
gzipper = gzip.GzipFile(fileobj = compressedstream) gzipper = gzip.GzipFile(fileobj = compressedstream)
@@ -1,9 +1,9 @@
from BeautifulSoup import SoupStrainer, BeautifulSoup from BeautifulSoup import SoupStrainer, BeautifulSoup
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import mergeDicts from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.trailer.base import TrailerProvider from couchpotato.core.providers.trailer.base import TrailerProvider
from string import letters, digits from string import letters, digits
from urllib import urlencode
import re import re
log = CPLog(__name__) log = CPLog(__name__)
@@ -44,7 +44,7 @@ class HDTrailers(TrailerProvider):
movie_name = group['library']['titles'][0]['title'] 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) data = self.getCache('hdtrailers.alt.%s' % group['library']['identifier'], url)
try: try:
+27
View File
@@ -4,9 +4,11 @@ from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import isInt, toUnicode from couchpotato.core.helpers.encoding import isInt, toUnicode
from couchpotato.core.helpers.request import getParams, jsonified from couchpotato.core.helpers.request import getParams, jsonified
from couchpotato.core.helpers.variable import mergeDicts, tryInt from couchpotato.core.helpers.variable import mergeDicts, tryInt
from couchpotato.core.settings.model import Properties
import ConfigParser import ConfigParser
import os.path import os.path
import time import time
import traceback
class Settings(object): class Settings(object):
@@ -190,3 +192,28 @@ class Settings(object):
return jsonified({ return jsonified({
'success': True, 'success': True,
}) })
def getProperty(self, identifier):
from couchpotato import get_session
db = get_session()
try:
prop = db.query(Properties).filter_by(identifier = identifier).first()
return prop.value if prop else None
except:
return None
def setProperty(self, identifier, value = ''):
from couchpotato import get_session
db = get_session()
p = db.query(Properties).filter_by(identifier = identifier).first()
if not p:
p = Properties()
db.add(p)
p.identifier = identifier
p.value = toUnicode(value)
db.commit()
+14 -8
View File
@@ -44,8 +44,8 @@ class Movie(Entity):
library = ManyToOne('Library') library = ManyToOne('Library')
status = ManyToOne('Status') status = ManyToOne('Status')
profile = ManyToOne('Profile') profile = ManyToOne('Profile')
releases = OneToMany('Release') releases = OneToMany('Release', cascade = 'all, delete-orphan')
files = ManyToMany('File') files = ManyToMany('File', cascade = 'all, delete-orphan', single_parent = True)
class Library(Entity): class Library(Entity):
@@ -59,9 +59,9 @@ class Library(Entity):
info = Field(JsonType) info = Field(JsonType)
status = ManyToOne('Status') status = ManyToOne('Status')
movies = OneToMany('Movie') movies = OneToMany('Movie', cascade = 'all, delete-orphan')
titles = OneToMany('LibraryTitle') titles = OneToMany('LibraryTitle', cascade = 'all, delete-orphan')
files = ManyToMany('File') files = ManyToMany('File', cascade = 'all, delete-orphan', single_parent = True)
class LibraryTitle(Entity): class LibraryTitle(Entity):
@@ -94,9 +94,9 @@ class Release(Entity):
movie = ManyToOne('Movie') movie = ManyToOne('Movie')
status = ManyToOne('Status') status = ManyToOne('Status')
quality = ManyToOne('Quality') quality = ManyToOne('Quality')
files = ManyToMany('File') files = ManyToMany('File', cascade = 'all, delete-orphan', single_parent = True)
history = OneToMany('History') history = OneToMany('History', cascade = 'all, delete-orphan')
info = OneToMany('ReleaseInfo') info = OneToMany('ReleaseInfo', cascade = 'all, delete-orphan')
class ReleaseInfo(Entity): class ReleaseInfo(Entity):
@@ -229,6 +229,12 @@ class Folder(Entity):
label = Field(Unicode(255)) label = Field(Unicode(255))
class Properties(Entity):
identifier = Field(String(50), index = True)
value = Field(Unicode(255), nullable = False)
def setup(): def setup():
"""Setup the database and create the tables that don't exists yet""" """Setup the database and create the tables that don't exists yet"""
from elixir import setup_all, create_all from elixir import setup_all, create_all
+9
View File
@@ -61,6 +61,15 @@ class Env(object):
return s 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 @staticmethod
def getPermission(setting_type): def getPermission(setting_type):
perm = Env.get('settings').get('permission_%s' % setting_type, default = '0777') perm = Env.get('settings').get('permission_%s' % setting_type, default = '0777')
+1 -1
View File
@@ -52,7 +52,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
locale.setlocale(locale.LC_ALL, "") locale.setlocale(locale.LC_ALL, "")
encoding = locale.getpreferredencoding() encoding = locale.getpreferredencoding()
except (locale.Error, IOError): except (locale.Error, IOError):
pass encoding = None
# for OSes that are poorly configured I'll just force UTF-8 # 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'): if not encoding or encoding in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
Binary file not shown.

After

Width:  |  Height:  |  Size: 778 B

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